learning_ai_common_plat/packages/kotlin-platform-sdk
saravanakumardb1 70635ba80e feat(kotlin-sdk): add ByteLystPlatform unified entry point + 2 new test files (4.2)
New source:
- ByteLystPlatform.kt — unified entry point wiring all services via Context + Config
  (secureStore, client, auth, telemetry, flags, killSwitch, auditLog)
- start(userId?) / stop() lifecycle for telemetry + flags
- Mirrors Swift ByteLystPlatform API

New tests (2 files):
- BLKillSwitchResultTest — ok(), disabled, default, copy
- BLTelemetryEventTest — serialize, deserialize, optional fields
2026-03-19 21:09:24 -07:00
..
src feat(kotlin-sdk): add ByteLystPlatform unified entry point + 2 new test files (4.2) 2026-03-19 21:09:24 -07:00
.gitignore chore(kotlin-sdk): add .gitignore and remove tracked build artifacts 2026-03-19 19:02:44 -07:00
build.gradle.kts feat(kotlin-sdk): restore 13 deferred UI files — diagnostics, clients, Compose UI, passkeys, deep links 2026-03-19 18:25:35 -07:00
consumer-rules.pro feat(sdk): add kotlin-platform-sdk (13 components) + 4 new TS client packages (32 tests) 2026-03-01 18:15:57 -08:00
gradle.properties feat(sdk): add kotlin-platform-sdk (13 components) + 4 new TS client packages (32 tests) 2026-03-01 18:15:57 -08:00
README.md docs(swift,kotlin): Add comprehensive SDK READMEs with broadcast and survey examples 2026-03-03 08:30:26 -08:00
settings.gradle.kts fix(gradle): add corporate proxy SSL truststore + Compose deps for kotlin-platform-sdk 2026-03-19 15:13:50 -07:00

ByteLyst Platform SDK — Android (Kotlin)

Kotlin SDK for the ByteLyst platform. Provides broadcast messaging, surveys, authentication, telemetry, and more.

Installation

Gradle

Add to your build.gradle.kts:

dependencies {
    implementation("com.bytelyst:platform-sdk:1.0.0")
}

Maven

<dependency>
    <groupId>com.bytelyst</groupId>
    <artifactId>platform-sdk</artifactId>
    <version>1.0.0</version>
</dependency>

Quick Start

import com.bytelyst.platform.*

// Configure the SDK
val config = BLPlatformConfig(
    productId = "lysnrai",
    baseURL = "https://api.bytelyst.io/v1",
    getAuthToken = { authRepository.getToken() }
)

// Create clients
val broadcastClient = BLBroadcastClient(config)
val surveyClient = BLSurveyClient(config)

Broadcast Client

Basic Usage

import com.bytelyst.platform.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class BroadcastManager(
    private val client: BLBroadcastClient
) {
    private val _messages = MutableStateFlow<List<InAppMessage>>(emptyList())
    val messages: StateFlow<List<InAppMessage>> = _messages

    private val _unreadCount = MutableStateFlow(0)
    val unreadCount: StateFlow<Int> = _unreadCount

    fun startListening() {
        client.startPolling(60000L) { messages ->
            _messages.value = messages
            _unreadCount.value = messages.count { it.status == MessageStatus.UNREAD }
        }
    }

    fun stopListening() {
        client.stopPolling()
    }

    suspend fun markRead(messageId: String) {
        client.markRead(messageId)
    }

    suspend fun dismiss(messageId: String) {
        client.markDismissed(messageId)
    }

    suspend fun handleTap(message: InAppMessage) {
        client.trackClick(message.id)

        message.ctaUrl?.let { url ->
            // Open URL with your navigation system
            navigationService.openUrl(url)
        }

        markRead(message.id)
    }
}

Jetpack Compose Integration

import com.bytelyst.platform.ui.*

@Composable
fun AppContent() {
    val broadcastManager = remember { BroadcastManager(broadcastClient) }
    val messages by broadcastManager.messages.collectAsState()
    val unreadCount by broadcastManager.unreadCount.collectAsState()

    LaunchedEffect(Unit) {
        broadcastManager.startListening()
    }

    Scaffold(
        topBar = {
            // Banner for unread messages
            InAppMessageBanner(
                client = broadcastClient,
                position = BannerPosition.TOP
            )
        }
    ) { padding ->
        MainContent(modifier = Modifier.padding(padding))
    }
}

Modal Messages

@Composable
fun AppRoot() {
    val broadcastClient = remember { BLBroadcastClient(config) }

    Box(modifier = Modifier.fillMaxSize()) {
        NavigationHost()

        // Modal overlay
        BroadcastModal(client = broadcastClient)
    }
}

Survey Client

Basic Usage

import com.bytelyst.platform.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class SurveyManager(
    private val client: BLSurveyClient
) {
    private val _activeSurvey = MutableStateFlow<ActiveSurvey?>(null)
    val activeSurvey: StateFlow<ActiveSurvey?> = _activeSurvey

    private val _currentQuestionIndex = MutableStateFlow(0)
    val currentQuestionIndex: StateFlow<Int> = _currentQuestionIndex

    private val _answers = MutableStateFlow<Map<String, SurveyAnswer>>(emptyMap())
    val answers: StateFlow<Map<String, SurveyAnswer>> = _answers

    private val _isComplete = MutableStateFlow(false)
    val isComplete: StateFlow<Boolean> = _isComplete

    suspend fun checkForSurveys() {
        val result = client.getActiveSurvey()
        result.onSuccess { survey ->
            survey?.let {
                _activeSurvey.value = it
                client.startSurvey(it.id)
            }
        }
    }

    suspend fun submitAnswer(question: Question, answer: SurveyAnswer) {
        val survey = _activeSurvey.value ?: return

        val result = client.submitAnswer(
            surveyId = survey.id,
            questionId = question.id,
            answer = answer
        )

        result.onSuccess { response ->
            _currentQuestionIndex.value = response.currentQuestionIndex
            _answers.value = _answers.value + (question.id to answer)

            if (response.isComplete) {
                completeSurvey()
            }
        }
    }

    suspend fun completeSurvey() {
        val survey = _activeSurvey.value ?: return

        val result = client.completeSurvey(survey.id)
        result.onSuccess { completion ->
            if (completion.success) {
                _isComplete.value = true

                if (completion.incentiveClaimed) {
                    showIncentiveToast(
                        amount = completion.incentiveAmount,
                        type = completion.incentiveType
                    )
                }
            }
        }
    }

    suspend fun dismiss() {
        val survey = _activeSurvey.value ?: return
        client.dismissSurvey(survey.id)

        _activeSurvey.value = null
        _currentQuestionIndex.value = 0
        _answers.value = emptyMap()
    }
}

Jetpack Compose Survey Modal

@Composable
fun AppRoot() {
    val surveyClient = remember { BLSurveyClient(config) }

    Box(modifier = Modifier.fillMaxSize()) {
        NavigationHost()

        // Survey modal overlay
        SurveyModal(client = surveyClient)
    }
}

Custom Survey UI

@Composable
fun CustomSurveyView(
    manager: SurveyManager = viewModel()
) {
    val survey by manager.activeSurvey.collectAsState()
    val currentIndex by manager.currentQuestionIndex.collectAsState()
    val isComplete by manager.isComplete.collectAsState()

    if (survey != null && !isComplete) {
        val question = survey!!.questions[currentIndex]
        val isLast = currentIndex == survey!!.questions.size - 1

        Column(modifier = Modifier.padding(16.dp)) {
            // Progress
            LinearProgressIndicator(
                progress = (currentIndex + 1) / survey!!.questions.size.toFloat(),
                modifier = Modifier.fillMaxWidth()
            )

            Text(
                text = "Question ${currentIndex + 1} of ${survey!!.questions.size}",
                style = MaterialTheme.typography.labelSmall
            )

            // Question
            Text(
                text = question.text,
                style = MaterialTheme.typography.titleLarge
            )

            question.description?.let {
                Text(
                    text = it,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }

            // Answer input based on type
            QuestionInput(
                question = question,
                onAnswer = { answer ->
                    coroutineScope.launch {
                        manager.submitAnswer(question, answer)
                    }
                }
            )

            // Navigation
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                if (!question.required) {
                    TextButton(onClick = { /* Skip */ }) {
                        Text("Skip")
                    }
                }

                Button(
                    onClick = { /* Submit */ },
                    enabled = canSubmit(question)
                ) {
                    Text(if (isLast) "Complete" else "Next")
                }
            }
        }
    }
}

@Composable
fun QuestionInput(
    question: Question,
    onAnswer: (SurveyAnswer) -> Unit
) {
    when (question.type) {
        QuestionType.SINGLE_CHOICE, QuestionType.DROPDOWN -> {
            var selected by remember { mutableStateOf<String?>(null) }

            Column {
                question.options?.forEach { option ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable {
                                selected = option.id
                                onAnswer(SurveyAnswer(
                                    type = "single_choice",
                                    value = JsonObject(mapOf("value" to JsonPrimitive(option.id)))
                                ))
                            }
                            .padding(12.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Text(text = option.emoji ?: "")
                        Text(text = option.text, modifier = Modifier.weight(1f))
                        RadioButton(
                            selected = selected == option.id,
                            onClick = null
                        )
                    }
                }
            }
        }

        QuestionType.MULTIPLE_CHOICE -> {
            var selected by remember { mutableStateOf<Set<String>>(emptySet()) }

            Column {
                question.options?.forEach { option ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable {
                                selected = if (selected.contains(option.id)) {
                                    selected - option.id
                                } else {
                                    selected + option.id
                                }
                                onAnswer(SurveyAnswer(
                                    type = "multiple_choice",
                                    value = JsonObject(mapOf("values" to JsonArray(selected.map { JsonPrimitive(it) })))
                                ))
                            }
                            .padding(12.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Text(text = option.emoji ?: "")
                        Text(text = option.text, modifier = Modifier.weight(1f))
                        Checkbox(
                            checked = selected.contains(option.id),
                            onCheckedChange = null
                        )
                    }
                }
            }
        }

        QuestionType.NPS, QuestionType.RATING, QuestionType.SCALE -> {
            var rating by remember { mutableIntStateOf(0) }
            val minValue = question.minValue ?: if (question.type == QuestionType.NPS) 0 else 1
            val maxValue = question.maxValue ?: if (question.type == QuestionType.NPS) 10 else 5

            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                (minValue..maxValue).forEach { value ->
                    Button(
                        onClick = {
                            rating = value
                            onAnswer(SurveyAnswer(
                                type = "rating",
                                value = JsonObject(mapOf("value" to JsonPrimitive(value)))
                            ))
                        },
                        colors = ButtonDefaults.buttonColors(
                            containerColor = if (rating == value) MaterialTheme.colorScheme.primary
                            else MaterialTheme.colorScheme.surfaceVariant
                        )
                    ) {
                        Text("$value")
                    }
                }
            }
        }

        QuestionType.TEXT_SHORT -> {
            var text by remember { mutableStateOf("") }
            OutlinedTextField(
                value = text,
                onValueChange = {
                    text = it
                    onAnswer(SurveyAnswer(
                        type = "text",
                        value = JsonObject(mapOf("value" to JsonPrimitive(it)))
                    ))
                },
                modifier = Modifier.fillMaxWidth()
            )
        }

        QuestionType.TEXT_LONG -> {
            var text by remember { mutableStateOf("") }
            OutlinedTextField(
                value = text,
                onValueChange = {
                    text = it
                    onAnswer(SurveyAnswer(
                        type = "text",
                        value = JsonObject(mapOf("value" to JsonPrimitive(it)))
                    ))
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .heightIn(min = 120.dp),
                maxLines = 6
            )
        }

        else -> { /* Handle other types */ }
    }
}

Push Notifications

Firebase Cloud Messaging

import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.bytelyst.platform.*

class PushNotificationService : FirebaseMessagingService() {
    private lateinit var broadcastClient: BLBroadcastClient

    override fun onCreate() {
        super.onCreate()
        val config = BLPlatformConfig(
            productId = "lysnrai",
            baseURL = "https://api.bytelyst.io/v1",
            getAuthToken = { authRepository.getToken() }
        )
        broadcastClient = BLBroadcastClient(config)
    }

    override fun onNewToken(token: String) {
        super.onNewToken(token)

        // Register device token
        coroutineScope.launch {
            broadcastClient.registerDeviceToken(token, Platform.ANDROID)
        }
    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        // Handle broadcast notification
        message.data["broadcastId"]?.let { broadcastId ->
            showNotification(message)
        }
    }
}

Register Device Token

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
    if (task.isSuccessful) {
        val token = task.result
        coroutineScope.launch {
            broadcastClient.registerDeviceToken(token, Platform.ANDROID)
        }
    }
}

Error Handling

val result = client.getActiveSurvey()

result.onSuccess { survey ->
    survey?.let { showSurvey(it) }
}.onError { error ->
    when (error) {
        is BLApiError.Unauthorized -> {
            // Re-authenticate user
            authRepository.refreshToken()
        }
        is BLApiError.NetworkError -> {
            // Retry with exponential backoff
            Log.w("Survey", "Network error, will retry")
        }
        is BLApiError.ServerError -> {
            Log.e("Survey", "Server error: ${error.code}")
        }
        else -> {
            Log.e("Survey", "Unknown error: ${error.message}")
        }
    }
}

Configuration

Environment-Based Config

sealed class Environment(
    val baseURL: String
) {
    object Development : Environment("http://localhost:4003")
    object Staging : Environment("https://api-staging.bytelyst.io/v1")
    object Production : Environment("https://api.bytelyst.io/v1")
}

val config = BLPlatformConfig(
    productId = "lysnrai",
    baseURL = Environment.Production.baseURL,
    getAuthToken = { authRepository.getToken() },
    enableLogging = BuildConfig.DEBUG
)

Offline Support

The SDK automatically caches survey responses offline:

val client = BLSurveyClient(
    config = config,
    enableOfflineCache = true  // Enabled by default
)

// Responses are queued when offline
// Flush manually or on network restore
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        coroutineScope.launch {
            client.flushOfflineQueue()
        }
    }
})

Requirements

  • Android 8.0+ (API 26+)
  • Kotlin 1.9+
  • Jetpack Compose (optional, for UI components)

License

MIT © ByteLyst