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+
|
- iOS 17+
|
||||||
- watchOS 10+
|
- watchOS 10+
|
||||||
- macOS 14+
|
- 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