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
This commit is contained in:
parent
80df7c1c1e
commit
55a1256d8b
574
packages/kotlin-platform-sdk/README.md
Normal file
574
packages/kotlin-platform-sdk/README.md
Normal file
@ -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
|
||||
<dependency>
|
||||
<groupId>com.bytelyst</groupId>
|
||||
<artifactId>platform-sdk</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 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<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
|
||||
|
||||
```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<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
|
||||
|
||||
```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<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
|
||||
|
||||
```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
|
||||
@ -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.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user