Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions dev-app/src/main/java/com/uid2/dev/ui/MainScreenViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import com.uid2.UID2Manager
import com.uid2.UID2ManagerState.Established
import com.uid2.UID2ManagerState.Expired
import com.uid2.UID2ManagerState.Loading
import com.uid2.UID2ManagerState.NoIdentity
import com.uid2.UID2ManagerState.OptOut
import com.uid2.UID2ManagerState.RefreshExpired
Expand All @@ -33,12 +34,12 @@ import kotlinx.coroutines.launch

sealed interface MainScreenAction : ViewModelAction {
data class EmailChanged(val address: String) : MainScreenAction
object ResetButtonPressed : MainScreenAction
object RefreshButtonPressed : MainScreenAction
data object ResetButtonPressed : MainScreenAction
data object RefreshButtonPressed : MainScreenAction
}

sealed interface MainScreenState : ViewState {
object LoadingState : MainScreenState
data object LoadingState : MainScreenState
data class UserUpdatedState(val identity: UID2Identity?, val status: IdentityStatus) : MainScreenState
data class ErrorState(val error: Throwable) : MainScreenState
}
Expand All @@ -59,6 +60,7 @@ class MainScreenViewModel(
Log.d(TAG, "State Update: $state")

when (state) {
is Loading -> Unit
is Established -> _viewState.emit(UserUpdatedState(state.identity, ESTABLISHED))
is Refreshed -> _viewState.emit(UserUpdatedState(state.identity, REFRESHED))
is NoIdentity -> _viewState.emit(UserUpdatedState(null, NO_IDENTITY))
Expand Down
24 changes: 23 additions & 1 deletion sdk/src/main/java/com/uid2/UID2Manager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import com.uid2.UID2ManagerState.Established
import com.uid2.UID2ManagerState.Expired
import com.uid2.UID2ManagerState.Invalid
import com.uid2.UID2ManagerState.Loading
import com.uid2.UID2ManagerState.NoIdentity
import com.uid2.UID2ManagerState.OptOut
import com.uid2.UID2ManagerState.RefreshExpired
Expand Down Expand Up @@ -58,6 +59,7 @@ public interface UID2ManagerIdentityChangedListener {
* A interface defining the flow of state communicated by the [UID2Manager].
*/
public sealed interface UID2ManagerState {
public data object Loading : UID2ManagerState
public data class Established(val identity: UID2Identity) : UID2ManagerState
public data class Refreshed(val identity: UID2Identity) : UID2ManagerState
public data object NoIdentity : UID2ManagerState
Expand Down Expand Up @@ -93,7 +95,23 @@ public class UID2Manager internal constructor(
*/
public var onIdentityChangedListener: UID2ManagerIdentityChangedListener? = null

private val _state = MutableStateFlow<UID2ManagerState>(NoIdentity)
/**
* Gets or sets a listener which can be used to determine if the [UID2Manager] instance has finished initializing.
* Initializing includes any time required to restore a previously persisted [UID2Identity] from storage.
*
* If this property is set *after* initialization is complete, the callback will be invoked immediately.
*/
public var onInitialized: (() -> Unit)? = null
set(value) {
field = value

// If we've already finished initializing, we should immediately invoke the callback.
if (initialized.isCompleted) {
value?.invoke()
}
}

private val _state = MutableStateFlow<UID2ManagerState>(Loading)

/**
* The flow representing the state of the UID2Manager.
Expand Down Expand Up @@ -135,6 +153,7 @@ public class UID2Manager internal constructor(
*/
public val currentIdentityStatus: IdentityStatus
get() = when (_state.value) {
is Loading -> NO_IDENTITY // Not available yet.
is Established -> ESTABLISHED
is Refreshed -> REFRESHED
is NoIdentity -> NO_IDENTITY
Expand Down Expand Up @@ -164,6 +183,9 @@ public class UID2Manager internal constructor(

validateAndSetIdentity(it.first, it.second, false)
}

// If we have a callback provided, invoke it.
onInitialized?.invoke()
}
}

Expand Down
35 changes: 35 additions & 0 deletions sdk/src/test/java/com/uid2/UID2ManagerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,28 @@ class UID2ManagerTest {
manager = withManager(client, storageManager, timeUtils, testDispatcher, false, listener)
}

@Test
fun `reports when initialization is complete`() = runTest(testDispatcher) {
var isInitialized = false
val onInitialized = { isInitialized = true }

val manager = UID2Manager(client, storageManager, timeUtils, testDispatcher, false, logger).apply {
this.checkExpiration = false
this.onInitialized = onInitialized
}

// Verify that the manager invokes our callback after it's been able to load the identity from storage.
assertFalse(isInitialized)
testDispatcher.scheduler.advanceUntilIdle()
assertTrue(isInitialized)

// Reset our state and re-assign the manager's callback. Verify that even though initialization is complete, our
// callback is invoked immediately.
isInitialized = false
manager.onInitialized = onInitialized
assertTrue(isInitialized)
}

@Test
fun `restores identity from storage`() = runTest(testDispatcher) {
// Verify that the initial state of the manager reflects the restored Identity.
Expand All @@ -75,6 +97,19 @@ class UID2ManagerTest {
assertEquals(initialStatus, manager.currentIdentityStatus)
}

@Test
fun `set identity immediately available`() = runTest(testDispatcher) {
val identity = withRandomIdentity()

// By default, the Manager will have restored the previously persisted Identity. Let's reset our state so this
// will be a new Identity.
manager.resetIdentity()

// Verify that immediately after setting an identity, it's immediately available via currentIdentity.
manager.setIdentity(identity)
assertEquals(identity, manager.currentIdentity)
}

@Test
fun `updates identity when set`() = runTest(testDispatcher) {
val identity = withRandomIdentity()
Expand Down