Skip to content

Commit b28d084

Browse files
authored
Pass complete user data when connecting (#39)
1 parent 18cac78 commit b28d084

File tree

7 files changed

+105
-27
lines changed

7 files changed

+105
-27
lines changed

.github/workflows/pr-quality.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ permissions:
1010
pull-requests: write
1111
issues: write
1212

13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
1317
jobs:
1418
pr-checklist:
1519
uses: GetStream/android-ci-actions/.github/workflows/pr-quality.yml@main

app/src/main/java/io/getstream/android/core/sample/SampleApp.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.os.Build
2222
import android.os.StrictMode
2323
import io.getstream.android.core.api.StreamClient
2424
import io.getstream.android.core.api.authentication.StreamTokenProvider
25+
import io.getstream.android.core.api.model.StreamUser
2526
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
2627
import io.getstream.android.core.api.model.value.StreamApiKey
2728
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
@@ -36,7 +37,7 @@ import kotlinx.coroutines.SupervisorJob
3637
class SampleApp : Application() {
3738

3839
lateinit var streamClient: StreamClient
39-
private val userId = StreamUserId.fromString("sample-user")
40+
private val user = StreamUser(StreamUserId.fromString("sample-user"))
4041
private val token =
4142
StreamToken.fromString(
4243
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicGV0YXIifQ.mZFi4iSblaIoyo9JDdcxIkGkwI-tuApeSBawxpz42rs"
@@ -60,7 +61,7 @@ class SampleApp : Application() {
6061
context = this.applicationContext,
6162
scope = coroutinesScope,
6263
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
63-
userId = userId,
64+
user = user,
6465
products = listOf("feeds", "chat", "video"),
6566
wsUrl =
6667
StreamWsUrl.fromString(

stream-android-core/src/main/java/io/getstream/android/core/api/StreamClient.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.getstream.android.core.api.authentication.StreamTokenProvider
2424
import io.getstream.android.core.api.components.StreamAndroidComponentsProvider
2525
import io.getstream.android.core.api.http.StreamOkHttpInterceptors
2626
import io.getstream.android.core.api.log.StreamLoggerProvider
27+
import io.getstream.android.core.api.model.StreamUser
2728
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
2829
import io.getstream.android.core.api.model.config.StreamHttpConfig
2930
import io.getstream.android.core.api.model.config.StreamSocketConfig
@@ -33,7 +34,6 @@ import io.getstream.android.core.api.model.connection.lifecycle.StreamLifecycleS
3334
import io.getstream.android.core.api.model.connection.network.StreamNetworkState
3435
import io.getstream.android.core.api.model.value.StreamApiKey
3536
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
36-
import io.getstream.android.core.api.model.value.StreamUserId
3737
import io.getstream.android.core.api.model.value.StreamWsUrl
3838
import io.getstream.android.core.api.observers.lifecycle.StreamLifecycleMonitor
3939
import io.getstream.android.core.api.observers.network.StreamNetworkMonitor
@@ -145,9 +145,9 @@ public interface StreamClient : StreamObservable<StreamClientListener> {
145145
/**
146146
* ### Overview
147147
*
148-
* Creates a [StreamClient] with the given [apiKey], [userId], [tokenProvider] and [scope]. The
149-
* client is created in a disconnected state. You must call `connect()` to establish a connection.
150-
* The client is automatically disconnected when the [scope] is cancelled.
148+
* Creates a [StreamClient] with the given [apiKey], [user], [tokenProvider] and [scope]. The client
149+
* is created in a disconnected state. You must call `connect()` to establish a connection. The
150+
* client is automatically disconnected when the [scope] is cancelled.
151151
*
152152
* **Important**: The client instance **must be kept alive for the duration of the connection**. Do
153153
* not create a new client for every operation.
@@ -185,7 +185,7 @@ public interface StreamClient : StreamObservable<StreamClientListener> {
185185
* ```
186186
*
187187
* @param apiKey The API key.
188-
* @param userId The user ID.
188+
* @param user The user ID.
189189
* @param wsUrl The WebSocket URL.
190190
* @param products Stream product codes (for feature gates / telemetry) negotiated with the socket.
191191
* @param clientInfoHeader The client info header.
@@ -215,7 +215,7 @@ public fun StreamClient(
215215

216216
// Client config
217217
apiKey: StreamApiKey,
218-
userId: StreamUserId,
218+
user: StreamUser,
219219
wsUrl: StreamWsUrl,
220220
products: List<String>,
221221
clientInfoHeader: StreamHttpClientInfoHeader,
@@ -249,7 +249,7 @@ public fun StreamClient(
249249
StreamRetryProcessor(logger = logProvider.taggedLogger("SCRetryProcessor")),
250250

251251
// Token
252-
tokenManager: StreamTokenManager = StreamTokenManager(userId, tokenProvider, singleFlight),
252+
tokenManager: StreamTokenManager = StreamTokenManager(user.id, tokenProvider, singleFlight),
253253

254254
// Socket
255255
connectionIdHolder: StreamConnectionIdHolder = StreamConnectionIdHolder(),
@@ -344,7 +344,7 @@ public fun StreamClient(
344344

345345
val mutableConnectionState = MutableStateFlow<StreamConnectionState>(StreamConnectionState.Idle)
346346
return StreamClientImpl(
347-
userId = userId,
347+
user = user,
348348
scope = clientScope,
349349
tokenManager = tokenManager,
350350
singleFlight = singleFlight,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-core-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.android.core.api.model
18+
19+
import io.getstream.android.core.annotations.StreamInternalApi
20+
import io.getstream.android.core.api.model.value.StreamUserId
21+
22+
/**
23+
* Represents a user in the Stream system.
24+
*
25+
* @property id The unique identifier for the user.
26+
* @property name The name of the user (optional).
27+
* @property imageURL The URL of the user's image (optional).
28+
* @property customData Custom data associated with the user, represented as a map (default empty
29+
* map).
30+
*/
31+
@StreamInternalApi
32+
public data class StreamUser(
33+
public val id: StreamUserId,
34+
public val name: String? = null,
35+
public val imageURL: String? = null,
36+
public val customData: Map<String, Any> = emptyMap(),
37+
)

stream-android-core/src/main/java/io/getstream/android/core/internal/client/StreamClientImpl.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import io.getstream.android.core.api.StreamClient
2020
import io.getstream.android.core.api.authentication.StreamTokenManager
2121
import io.getstream.android.core.api.log.StreamLogger
2222
import io.getstream.android.core.api.model.StreamTypedKey.Companion.randomExecutionKey
23+
import io.getstream.android.core.api.model.StreamUser
2324
import io.getstream.android.core.api.model.connection.StreamConnectedUser
2425
import io.getstream.android.core.api.model.connection.StreamConnectionState
2526
import io.getstream.android.core.api.model.connection.lifecycle.StreamLifecycleState
2627
import io.getstream.android.core.api.model.connection.network.StreamNetworkState
2728
import io.getstream.android.core.api.model.connection.recovery.Recovery
2829
import io.getstream.android.core.api.model.value.StreamToken
29-
import io.getstream.android.core.api.model.value.StreamUserId
3030
import io.getstream.android.core.api.processing.StreamSerialProcessingQueue
3131
import io.getstream.android.core.api.processing.StreamSingleFlightProcessor
3232
import io.getstream.android.core.api.recovery.StreamConnectionRecoveryEvaluator
@@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.asStateFlow
4949
import kotlinx.coroutines.launch
5050

5151
internal class StreamClientImpl<T>(
52-
private val userId: StreamUserId,
52+
private val user: StreamUser,
5353
private val tokenManager: StreamTokenManager,
5454
private val singleFlight: StreamSingleFlightProcessor,
5555
private val serialQueue: StreamSerialProcessingQueue,
@@ -182,13 +182,13 @@ internal class StreamClientImpl<T>(
182182
): Result<StreamConnectionState.Connected> {
183183
val data =
184184
ConnectUserData(
185-
userId = userId.rawValue,
185+
userId = user.id.rawValue,
186186
token = token.rawValue,
187-
name = null,
188-
image = null,
187+
name = user.name,
188+
image = user.imageURL,
189189
invisible = false,
190190
language = null,
191-
custom = null,
191+
custom = user.customData,
192192
)
193193
return socketSession.connect(data).onTokenError { error, code ->
194194
logger.e(error) { "Token error: $code" }

stream-android-core/src/test/java/io/getstream/android/core/api/StreamClientFactoryTest.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import io.getstream.android.core.api.authentication.StreamTokenManager
2525
import io.getstream.android.core.api.authentication.StreamTokenProvider
2626
import io.getstream.android.core.api.log.StreamLogger
2727
import io.getstream.android.core.api.log.StreamLoggerProvider
28+
import io.getstream.android.core.api.model.StreamUser
2829
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
2930
import io.getstream.android.core.api.model.config.StreamHttpConfig
3031
import io.getstream.android.core.api.model.config.StreamSocketConfig
@@ -92,7 +93,7 @@ internal class StreamClientFactoryTest {
9293

9394
private data class Dependencies(
9495
val apiKey: StreamApiKey,
95-
val userId: StreamUserId,
96+
val user: StreamUser,
9697
val wsUrl: StreamWsUrl,
9798
val clientInfo: StreamHttpClientInfoHeader,
9899
val clientSubscriptionManager: StreamSubscriptionManager<StreamClientListener>,
@@ -116,7 +117,7 @@ internal class StreamClientFactoryTest {
116117
val deps =
117118
Dependencies(
118119
apiKey = StreamApiKey.fromString("key123"),
119-
userId = StreamUserId.fromString("user-123"),
120+
user = StreamUser(id = StreamUserId.fromString("user-123")),
120121
wsUrl = StreamWsUrl.fromString("wss://test.stream/video"),
121122
clientInfo =
122123
StreamHttpClientInfoHeader.create(
@@ -147,7 +148,7 @@ internal class StreamClientFactoryTest {
147148
StreamClient(
148149
context = mockk(relaxed = true),
149150
apiKey = deps.apiKey,
150-
userId = deps.userId,
151+
user = deps.user,
151152
wsUrl = deps.wsUrl,
152153
products = listOf("feeds"),
153154
clientInfoHeader = deps.clientInfo,
@@ -185,7 +186,7 @@ internal class StreamClientFactoryTest {
185186
assertTrue(client.connectionState.value is StreamConnectionState.Idle)
186187

187188
// Verify client level wiring
188-
client.assertFieldEquals("userId", deps.userId.rawValue)
189+
client.assertFieldEquals("user", deps.user)
189190
client.assertFieldEquals("tokenManager", deps.tokenManager)
190191
client.assertFieldEquals("singleFlight", deps.singleFlight)
191192
client.assertFieldEquals("serialQueue", deps.serialQueue)
@@ -323,12 +324,19 @@ internal class StreamClientFactoryTest {
323324
StreamToken.fromString("token")
324325
}
325326

327+
val user =
328+
StreamUser(
329+
id = StreamUserId.fromString("user-123"),
330+
name = "name",
331+
imageURL = "image",
332+
customData = mapOf("custom" to "data"),
333+
)
326334
val client =
327335
StreamClient(
328336
scope = testScope,
329337
context = context,
330338
apiKey = StreamApiKey.fromString("key123"),
331-
userId = StreamUserId.fromString("user-123"),
339+
user = user,
332340
wsUrl = StreamWsUrl.fromString("wss://test.stream/video"),
333341
products = listOf("feeds"),
334342
clientInfoHeader =

stream-android-core/src/test/java/io/getstream/android/core/internal/client/StreamClientIImplTest.kt

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package io.getstream.android.core.internal.client
2121
import io.getstream.android.core.api.authentication.StreamTokenManager
2222
import io.getstream.android.core.api.log.StreamLogger
2323
import io.getstream.android.core.api.model.StreamTypedKey
24+
import io.getstream.android.core.api.model.StreamUser
2425
import io.getstream.android.core.api.model.connection.StreamConnectedUser
2526
import io.getstream.android.core.api.model.connection.StreamConnectionState
2627
import io.getstream.android.core.api.model.connection.lifecycle.StreamLifecycleState
@@ -48,8 +49,18 @@ import io.getstream.android.core.internal.observers.StreamNetworkAndLifeCycleMon
4849
import io.getstream.android.core.internal.observers.StreamNetworkAndLifecycleMonitorListener
4950
import io.getstream.android.core.internal.recovery.StreamConnectionRecoveryEvaluatorImpl
5051
import io.getstream.android.core.internal.socket.StreamSocketSession
52+
import io.getstream.android.core.internal.socket.model.ConnectUserData
5153
import io.getstream.android.core.testing.TestLogger
52-
import io.mockk.*
54+
import io.mockk.Awaits
55+
import io.mockk.MockKAnnotations
56+
import io.mockk.coEvery
57+
import io.mockk.coJustRun
58+
import io.mockk.coVerify
59+
import io.mockk.every
60+
import io.mockk.just
61+
import io.mockk.justRun
62+
import io.mockk.mockk
63+
import io.mockk.verify
5364
import kotlin.time.ExperimentalTime
5465
import kotlinx.coroutines.CompletableDeferred
5566
import kotlinx.coroutines.CoroutineScope
@@ -61,14 +72,23 @@ import kotlinx.coroutines.suspendCancellableCoroutine
6172
import kotlinx.coroutines.test.advanceTimeBy
6273
import kotlinx.coroutines.test.advanceUntilIdle
6374
import kotlinx.coroutines.test.runTest
64-
import org.junit.Assert.*
75+
import org.junit.Assert.assertEquals
76+
import org.junit.Assert.assertNull
77+
import org.junit.Assert.assertSame
78+
import org.junit.Assert.assertTrue
6579
import org.junit.Before
6680
import org.junit.Test
6781

6882
@OptIn(ExperimentalTime::class)
6983
class StreamClientIImplTest {
7084

71-
private var userId: StreamUserId = StreamUserId.fromString("u1")
85+
private var user: StreamUser =
86+
StreamUser(
87+
id = StreamUserId.fromString("u1"),
88+
name = "name",
89+
imageURL = "image",
90+
customData = mapOf("custom" to "data"),
91+
)
7292
private lateinit var tokenManager: StreamTokenManager
7393
private lateinit var singleFlight: StreamSingleFlightProcessor
7494
private lateinit var serialQueue: StreamSerialProcessingQueue
@@ -122,7 +142,7 @@ class StreamClientIImplTest {
122142
connectionRecoveryEvaluator: StreamConnectionRecoveryEvaluator = mockk(relaxed = true),
123143
) =
124144
StreamClientImpl(
125-
userId = userId,
145+
user = user,
126146
tokenManager = tokenManager,
127147
singleFlight = singleFlight,
128148
serialQueue = serialQueue,
@@ -372,6 +392,14 @@ class StreamClientIImplTest {
372392
coEvery { socketSession.connect(any()) } returns Result.success(connectedState)
373393

374394
every { connectionIdHolder.setConnectionId("conn-1") } returns Result.success("conn-1")
395+
val expectedConnectUserData =
396+
ConnectUserData(
397+
userId = user.id.rawValue,
398+
token = token.rawValue,
399+
name = user.name,
400+
image = user.imageURL,
401+
custom = user.customData,
402+
)
375403

376404
val result = client.connect()
377405

@@ -385,7 +413,7 @@ class StreamClientIImplTest {
385413
// interactions
386414
verify(exactly = 1) { socketSession.subscribe(any<StreamClientListener>(), any()) }
387415
coVerify(exactly = 1) { tokenManager.loadIfAbsent() }
388-
coVerify(exactly = 1) { socketSession.connect(match { it.token == "tok" }) }
416+
coVerify(exactly = 1) { socketSession.connect(expectedConnectUserData) }
389417
verify(exactly = 1) { connectionIdHolder.setConnectionId("conn-1") }
390418
}
391419

@@ -629,7 +657,7 @@ class StreamClientIImplTest {
629657
{
630658
if (firstCall) {
631659
firstCall = false
632-
connFlow.update { StreamConnectionState.Connecting.Opening(userId.rawValue) }
660+
connFlow.update { StreamConnectionState.Connecting.Opening(user.id.rawValue) }
633661
firstConnectDeferred.await()
634662
} else {
635663
Result.success(connectedState)

0 commit comments

Comments
 (0)