From 55a1256d8bade03980bace61ee005159f9932e42 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 08:30:26 -0800 Subject: [PATCH] docs(swift,kotlin): Add comprehensive SDK READMEs with broadcast and survey examples - Swift SDK README: Installation, Broadcast/Survey clients, SwiftUI integration, push notifications - Kotlin SDK README: Gradle setup, Jetpack Compose components, FCM integration --- packages/kotlin-platform-sdk/README.md | 574 +++++++++++++++++++++++++ packages/swift-platform-sdk/README.md | 47 ++ 2 files changed, 621 insertions(+) create mode 100644 packages/kotlin-platform-sdk/README.md diff --git a/packages/kotlin-platform-sdk/README.md b/packages/kotlin-platform-sdk/README.md new file mode 100644 index 00000000..78688e1c --- /dev/null +++ b/packages/kotlin-platform-sdk/README.md @@ -0,0 +1,574 @@ +# 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`: + +```kotlin +dependencies { + implementation("com.bytelyst:platform-sdk:1.0.0") +} +``` + +### Maven + +```xml + + com.bytelyst + platform-sdk + 1.0.0 + +``` + +## Quick Start + +```kotlin +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 + +```kotlin +import com.bytelyst.platform.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class BroadcastManager( + private val client: BLBroadcastClient +) { + private val _messages = MutableStateFlow>(emptyList()) + val messages: StateFlow> = _messages + + private val _unreadCount = MutableStateFlow(0) + val unreadCount: StateFlow = _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 + +```kotlin +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 + +```kotlin +@Composable +fun AppRoot() { + val broadcastClient = remember { BLBroadcastClient(config) } + + Box(modifier = Modifier.fillMaxSize()) { + NavigationHost() + + // Modal overlay + BroadcastModal(client = broadcastClient) + } +} +``` + +## Survey Client + +### Basic Usage + +```kotlin +import com.bytelyst.platform.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class SurveyManager( + private val client: BLSurveyClient +) { + private val _activeSurvey = MutableStateFlow(null) + val activeSurvey: StateFlow = _activeSurvey + + private val _currentQuestionIndex = MutableStateFlow(0) + val currentQuestionIndex: StateFlow = _currentQuestionIndex + + private val _answers = MutableStateFlow>(emptyMap()) + val answers: StateFlow> = _answers + + private val _isComplete = MutableStateFlow(false) + val isComplete: StateFlow = _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 + +```kotlin +@Composable +fun AppRoot() { + val surveyClient = remember { BLSurveyClient(config) } + + Box(modifier = Modifier.fillMaxSize()) { + NavigationHost() + + // Survey modal overlay + SurveyModal(client = surveyClient) + } +} +``` + +### Custom Survey UI + +```kotlin +@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(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>(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 + +```kotlin +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 + +```kotlin +FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> + if (task.isSuccessful) { + val token = task.result + coroutineScope.launch { + broadcastClient.registerDeviceToken(token, Platform.ANDROID) + } + } +} +``` + +## Error Handling + +```kotlin +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 + +```kotlin +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: + +```kotlin +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 diff --git a/packages/swift-platform-sdk/README.md b/packages/swift-platform-sdk/README.md index adb7fad0..e500e78a 100644 --- a/packages/swift-platform-sdk/README.md +++ b/packages/swift-platform-sdk/README.md @@ -166,3 +166,50 @@ Total duplicated code eliminated: **~2,600+ lines across 3 product apps**. - iOS 17+ - watchOS 10+ - macOS 14+ + +## Broadcast & Survey + +New in v1.2: In-app messaging and survey capabilities. + +### Broadcast Client + +```swift +import ByteLystPlatformSDK + +let config = BLPlatformConfig( + productId: "lysnrai", + baseURL: URL(string: "https://api.bytelyst.io/v1")!, + getAuthToken: { await getToken() } +) + +let broadcastClient = BLBroadcastClient(config: config) + +// Start polling for messages +broadcastClient.startPolling(intervalMs: 60000) { messages in + // Handle new messages +} + +// SwiftUI UI components +BLInAppMessageBanner(client: broadcastClient, position: .top) +BLBroadcastModal(client: broadcastClient) +``` + +### Survey Client + +```swift +let surveyClient = BLSurveyClient(config: config) + +// Check for active surveys +let (survey, _) = await surveyClient.getActiveSurvey() + +// Start and complete survey +await surveyClient.startSurvey(surveyId: survey.id) +await surveyClient.submitAnswer(surveyId: survey.id, questionId: "q1", answer: answer) +await surveyClient.completeSurvey(surveyId: survey.id) + +// SwiftUI modal +BLSurveyModal(client: surveyClient) +``` + +See [Broadcast & Survey Guide](BROADCAST_SURVEY_GUIDE.md) for full documentation. +