-
Notifications
You must be signed in to change notification settings - Fork 662
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
LinkActivity, ViewModel and Navigator
- Loading branch information
1 parent
a9a8ea4
commit f4375bd
Showing
8 changed files
with
331 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
link/src/main/java/com/stripe/android/link/LinkActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package com.stripe.android.link | ||
|
||
import android.os.Bundle | ||
import androidx.activity.compose.setContent | ||
import androidx.activity.viewModels | ||
import androidx.annotation.VisibleForTesting | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.lifecycle.ViewModelProvider | ||
import androidx.navigation.NavHostController | ||
import androidx.navigation.compose.NavHost | ||
import androidx.navigation.compose.composable | ||
import androidx.navigation.compose.rememberNavController | ||
import com.stripe.android.link.ui.cardedit.CardEditScreen | ||
import com.stripe.android.link.ui.paymentmenthod.PaymentMethodScreen | ||
import com.stripe.android.link.ui.signup.SignUpScreen | ||
import com.stripe.android.link.ui.verification.VerificationScreen | ||
import com.stripe.android.link.ui.wallet.WalletScreen | ||
|
||
internal class LinkActivity : AppCompatActivity() { | ||
@VisibleForTesting | ||
internal var viewModelFactory: ViewModelProvider.Factory = LinkActivityViewModel.Factory() | ||
private val viewModel: LinkActivityViewModel by viewModels { viewModelFactory } | ||
|
||
@VisibleForTesting | ||
internal lateinit var navController: NavHostController | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
setContent { | ||
navController = rememberNavController() | ||
|
||
LaunchedEffect("LinkEffects") { | ||
viewModel.effect.collect { effect -> | ||
when (effect) { | ||
LinkEffect.GoBack -> navController.popBackStack() | ||
is LinkEffect.NavigateTo -> { | ||
navController.navigate(effect.screen.route) | ||
} | ||
} | ||
} | ||
} | ||
|
||
NavHost( | ||
navController = navController, | ||
startDestination = LinkScreen.SignUp.route | ||
) { | ||
composable(LinkScreen.SignUp.route) { | ||
SignUpScreen() | ||
} | ||
|
||
composable(LinkScreen.Verification.route) { | ||
VerificationScreen() | ||
} | ||
|
||
composable(LinkScreen.Wallet.route) { | ||
WalletScreen() | ||
} | ||
|
||
composable(LinkScreen.CardEdit.route) { | ||
CardEditScreen() | ||
} | ||
|
||
composable(LinkScreen.PaymentMethod.route) { | ||
PaymentMethodScreen() | ||
} | ||
} | ||
} | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
link/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.stripe.android.link | ||
|
||
import androidx.lifecycle.ViewModel | ||
import androidx.lifecycle.ViewModelProvider | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.flowOf | ||
|
||
internal class LinkActivityViewModel : LinkViewModel<LinkState, LinkAction, LinkResult, LinkEffect>( | ||
initialState = LinkState, | ||
) { | ||
override fun actionToResult(action: LinkAction): Flow<LinkResult> { | ||
return when (action) { | ||
LinkAction.BackPressed -> handleBackPressed() | ||
LinkAction.WalletClicked -> handleWalletClicked() | ||
} | ||
} | ||
|
||
private fun handleBackPressed(): Flow<LinkResult> { | ||
return flowOf(LinkResult.SendEffect(LinkEffect.GoBack)) | ||
} | ||
|
||
private fun handleWalletClicked(): Flow<LinkResult> { | ||
return flowOf( | ||
value = LinkResult.SendEffect( | ||
effect = LinkEffect.NavigateTo( | ||
screen = LinkScreen.Wallet | ||
) | ||
) | ||
) | ||
} | ||
|
||
override fun resultToState(currentState: LinkState, result: LinkResult) = currentState | ||
|
||
override fun resultToEffect(result: LinkResult): LinkEffect? { | ||
return when (result) { | ||
is LinkResult.SendEffect -> result.effect | ||
} | ||
} | ||
|
||
internal class Factory : ViewModelProvider.Factory { | ||
override fun <T : ViewModel> create(modelClass: Class<T>): T { | ||
return LinkActivityViewModel() as T | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
package com.stripe.android.link | ||
|
||
internal sealed interface LinkScreen { | ||
data object Verification : LinkScreen | ||
data object Wallet : LinkScreen | ||
data object PaymentMethod : LinkScreen | ||
data object CardEdit : LinkScreen | ||
data object SignUp : LinkScreen | ||
internal sealed class LinkScreen(val route: String) { | ||
data object Verification : LinkScreen("verification") | ||
data object Wallet : LinkScreen("wallet") | ||
data object PaymentMethod : LinkScreen("paymentMethod") | ||
data object CardEdit : LinkScreen("cardEdit") | ||
data object SignUp : LinkScreen("signUp") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.stripe.android.link | ||
|
||
internal object LinkState | ||
|
||
internal sealed interface LinkAction { | ||
data object BackPressed : LinkAction | ||
data object WalletClicked : LinkAction | ||
} | ||
|
||
internal sealed interface LinkResult { | ||
data class SendEffect(val effect: LinkEffect) : LinkResult | ||
} | ||
|
||
internal sealed interface LinkEffect { | ||
data object GoBack : LinkEffect | ||
data class NavigateTo(val screen: LinkScreen) : LinkEffect | ||
} |
52 changes: 52 additions & 0 deletions
52
link/src/main/java/com/stripe/android/link/LinkViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.stripe.android.link | ||
|
||
import androidx.lifecycle.ViewModel | ||
import androidx.lifecycle.viewModelScope | ||
import kotlinx.coroutines.channels.Channel | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.flatMapConcat | ||
import kotlinx.coroutines.flow.receiveAsFlow | ||
import kotlinx.coroutines.flow.update | ||
import kotlinx.coroutines.launch | ||
|
||
// This will be used as a base for all Link screen viewmodels to create a consistent architecture | ||
// There should be no more logic here besides updating state and sending effects | ||
internal abstract class LinkViewModel<State, Action, Result, Effect>( | ||
initialState: State, | ||
) : ViewModel() { | ||
private val actionChannel = Channel<Action>(Channel.UNLIMITED) | ||
|
||
private val _state = MutableStateFlow(initialState) | ||
val state: StateFlow<State> = _state | ||
|
||
private val _effect = MutableSharedFlow<Effect>() | ||
val effect: Flow<Effect> = _effect | ||
|
||
init { | ||
viewModelScope.launch { | ||
actionChannel.receiveAsFlow() | ||
.flatMapConcat(::actionToResult) | ||
.collect { result -> | ||
_state.update { currentState -> | ||
resultToState(currentState, result) | ||
} | ||
resultToEffect(result)?.let { newEffect -> | ||
_effect.emit(newEffect) | ||
} | ||
} | ||
} | ||
} | ||
|
||
fun handleAction(action: Action) { | ||
actionChannel.trySend(action) | ||
} | ||
|
||
protected abstract fun actionToResult(action: Action): Flow<Result> | ||
|
||
protected abstract fun resultToState(currentState: State, result: Result): State | ||
|
||
protected abstract fun resultToEffect(result: Result): Effect? | ||
} |
88 changes: 88 additions & 0 deletions
88
link/src/test/java/com/stripe/android/link/LinkActivityTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.stripe.android.link | ||
|
||
import android.content.Context | ||
import android.content.Intent | ||
import android.os.Build | ||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule | ||
import androidx.compose.ui.test.junit4.createAndroidComposeRule | ||
import androidx.navigation.NavHostController | ||
import androidx.test.core.app.ApplicationProvider | ||
import com.stripe.android.link.utils.InjectableActivityScenario | ||
import com.stripe.android.link.utils.injectableActivityScenario | ||
import com.stripe.android.link.utils.viewModelFactoryFor | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.test.resetMain | ||
import org.junit.Rule | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.mockito.kotlin.mock | ||
import org.mockito.kotlin.verify | ||
import org.robolectric.RobolectricTestRunner | ||
import org.robolectric.annotation.Config | ||
import kotlin.test.AfterTest | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
@Config(sdk = [Build.VERSION_CODES.Q]) | ||
internal class LinkActivityTest { | ||
|
||
@get:Rule | ||
val rule = InstantTaskExecutorRule() | ||
|
||
@get:Rule | ||
val composeTestRule = createAndroidComposeRule<LinkActivity>() | ||
|
||
private val navHostController: NavHostController = mock() | ||
|
||
private val context = ApplicationProvider.getApplicationContext<Context>() | ||
|
||
@AfterTest | ||
fun cleanup() { | ||
Dispatchers.resetMain() | ||
} | ||
|
||
@Test | ||
fun `test that navigator pops back on back pressed`() { | ||
val vm = LinkActivityViewModel() | ||
val scenario = activityScenario(viewModel = vm) | ||
scenario.launchTest { | ||
vm.handleAction(LinkAction.BackPressed) | ||
|
||
composeTestRule.waitForIdle() | ||
|
||
verify(navHostController).popBackStack() | ||
} | ||
} | ||
|
||
@Test | ||
fun `test that navigator navigates to wallet on wallet clicked`() { | ||
val vm = LinkActivityViewModel() | ||
val scenario = activityScenario(viewModel = vm) | ||
scenario.launchTest { | ||
vm.handleAction(LinkAction.WalletClicked) | ||
|
||
composeTestRule.waitForIdle() | ||
|
||
verify(navHostController).navigate(LinkScreen.Wallet) | ||
} | ||
} | ||
|
||
private fun InjectableActivityScenario<LinkActivity>.launchTest( | ||
startIntent: Intent = Intent(context, LinkActivity::class.java), | ||
block: (LinkActivity) -> Unit | ||
) { | ||
launch(startIntent).onActivity { activity -> | ||
activity.navController = navHostController | ||
block(activity) | ||
} | ||
} | ||
|
||
private fun activityScenario( | ||
viewModel: LinkActivityViewModel = LinkActivityViewModel(), | ||
): InjectableActivityScenario<LinkActivity> { | ||
return injectableActivityScenario { | ||
injectActivity { | ||
viewModelFactory = viewModelFactoryFor(viewModel) | ||
} | ||
} | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
link/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.stripe.android.link | ||
|
||
import app.cash.turbine.test | ||
import com.google.common.truth.Truth | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
import kotlinx.coroutines.test.resetMain | ||
import kotlinx.coroutines.test.runTest | ||
import kotlinx.coroutines.test.setMain | ||
import org.junit.After | ||
import org.junit.Before | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.robolectric.RobolectricTestRunner | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
internal class LinkActivityViewModelTest { | ||
private val dispatcher = UnconfinedTestDispatcher() | ||
private val vm = LinkActivityViewModel() | ||
|
||
@Before | ||
fun setUp() { | ||
Dispatchers.setMain(dispatcher) | ||
} | ||
|
||
@After | ||
fun tearDown() { | ||
Dispatchers.resetMain() | ||
} | ||
|
||
@Test | ||
fun `test that viewmodel has correct initial state`() = runTest(dispatcher) { | ||
vm.state.test { | ||
Truth.assertThat(awaitItem()).isEqualTo(LinkState) | ||
} | ||
} | ||
|
||
@Test | ||
fun `test that correct effect is emitted on back pressed`() = runTest(dispatcher) { | ||
vm.effect.test { | ||
vm.handleAction(LinkAction.BackPressed) | ||
Truth.assertThat(awaitItem()).isEqualTo(LinkEffect.GoBack) | ||
} | ||
} | ||
} |