Skip to content

Commit

Permalink
Link 2FA v2
Browse files Browse the repository at this point in the history
  • Loading branch information
toluo-stripe committed Jan 28, 2025
1 parent 0cf1809 commit a4335c8
Show file tree
Hide file tree
Showing 28 changed files with 216 additions and 83 deletions.
1 change: 0 additions & 1 deletion paymentsheet/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<style name="StripeLinkBaseTheme" parent="@android:style/Theme.Translucent">
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@color/stripe_link_window_background</item>
<item name="windowNoTitle">true</item>
</style>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ interface CustomerAdapter {
}

is Link -> {
PaymentSelection.Link
PaymentSelection.Link()
}

is StripeId -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ internal class DefaultCustomerSheetLoader(
val paymentSelection = customerSheetSession.savedSelection?.let { selection ->
when (selection) {
is SavedSelection.GooglePay -> PaymentSelection.GooglePay
is SavedSelection.Link -> PaymentSelection.Link
is SavedSelection.Link -> PaymentSelection.Link()
is SavedSelection.PaymentMethod -> {
paymentMethods.find { paymentMethod ->
paymentMethod.id == selection.id
Expand Down
115 changes: 85 additions & 30 deletions paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,32 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.VisibleForTesting
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Dialog
import androidx.core.os.bundleOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.stripe.android.core.Logger
import com.stripe.android.link.theme.DefaultLinkTheme
import com.stripe.android.link.ui.BottomSheetContent
import com.stripe.android.link.ui.LinkContent
import com.stripe.android.link.ui.verification.VerificationScreen
import com.stripe.android.link.ui.verification.VerificationViewModel
import com.stripe.android.paymentsheet.BuildConfig
import com.stripe.android.paymentsheet.utils.EventReporterProvider
import com.stripe.android.uicore.utils.collectAsState
Expand All @@ -46,7 +54,7 @@ internal class LinkActivity : ComponentActivity() {
viewModel = ViewModelProvider(this, LinkActivityViewModel.factory())[LinkActivityViewModel::class.java]
} catch (e: NoArgsException) {
Logger.getInstance(BuildConfig.DEBUG).error("Failed to create LinkActivityViewModel", e)
setResult(Activity.RESULT_CANCELED)
setResult(RESULT_CANCELED)
finish()
}

Expand All @@ -61,21 +69,7 @@ internal class LinkActivity : ComponentActivity() {
}

setContent {
var bottomSheetContent by remember { mutableStateOf<BottomSheetContent?>(null) }
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
val appBarState by vm.linkState.collectAsState()

if (bottomSheetContent != null) {
DisposableEffect(bottomSheetContent) {
coroutineScope.launch { sheetState.show() }
onDispose {
coroutineScope.launch { sheetState.hide() }
}
}
}
navController = rememberNavController()

LaunchedEffect(Unit) {
viewModel?.let {
it.navController = navController
Expand All @@ -85,22 +79,83 @@ internal class LinkActivity : ComponentActivity() {
}
}

EventReporterProvider(
eventReporter = vm.eventReporter
) {
LinkContent(
viewModel = vm,
navController = navController,
appBarState = appBarState,
sheetState = sheetState,
bottomSheetContent = bottomSheetContent,
onUpdateSheetContent = {
bottomSheetContent = it
},
onBackPressed = onBackPressedDispatcher::onBackPressed
)
val screenState by vm.linkScreenState.collectAsState()

when (val state = screenState) {
State.Link -> {
LinkScreenFlow(vm)
}
State.Loading -> {
// CircularProgressIndicator()
}
is State.VerificationDialog -> {
Dialog(
onDismissRequest = {}
) {
val viewModel = linkViewModel<VerificationViewModel> { parentComponent ->
VerificationViewModel.factory(
parentComponent = parentComponent,
linkAccount = state.linkAccount,
onVerificationSucceeded = {
vm.onVerificationSucceeded()
},
onChangeEmailClicked = {

},
onDismissClicked = {
vm.goBack()
}
)

}
DefaultLinkTheme {
VerificationScreen(viewModel)
}
}
}
}
}
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun LinkScreenFlow(
vm: LinkActivityViewModel,
) {
var bottomSheetContent by remember { mutableStateOf<BottomSheetContent?>(null) }
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
val appBarState by vm.linkAppBarState.collectAsState()

if (bottomSheetContent != null) {
DisposableEffect(bottomSheetContent) {
coroutineScope.launch { sheetState.show() }
onDispose {
coroutineScope.launch { sheetState.hide() }
}
}
}


EventReporterProvider(
eventReporter = vm.eventReporter
) {
LinkContent(
viewModel = vm,
navController = navController,
appBarState = appBarState,
sheetState = sheetState,
bottomSheetContent = bottomSheetContent,
onUpdateSheetContent = {
bottomSheetContent = it
},
onBackPressed = onBackPressedDispatcher::onBackPressed
)
}

LaunchedEffect(Unit) {
vm.linkScreenScreenCreated()
}
}

private fun dismissWithResult(result: LinkActivityResult) {
Expand All @@ -120,7 +175,7 @@ internal class LinkActivity : ComponentActivity() {
}

fun launchWebFlow(configuration: LinkConfiguration) {
webLauncher?.launch(LinkActivityContract.Args(configuration))
webLauncher?.launch(LinkActivityContract.Args(configuration, false))
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ internal class LinkActivityContract @Inject internal constructor(
}

data class Args internal constructor(
internal val configuration: LinkConfiguration
internal val configuration: LinkConfiguration,
internal val use2faDialog: Boolean
)

data class Result(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@ internal class LinkActivityViewModel @Inject constructor(
private val errorReporter: ErrorReporter,
) : ViewModel(), DefaultLifecycleObserver {
val confirmationHandler = confirmationHandlerFactory.create(viewModelScope)
private val _linkState = MutableStateFlow(
private val _linkAppBarState = MutableStateFlow(
value = LinkAppBarState(
navigationIcon = R.drawable.stripe_link_close,
showHeader = true,
showOverflowMenu = false,
email = null,
)
)
val linkState: StateFlow<LinkAppBarState> = _linkState
val linkAppBarState: StateFlow<LinkAppBarState> = _linkAppBarState

private val _linkScreenState = MutableStateFlow<State>(State.Loading)
val linkScreenState: StateFlow<State> = _linkScreenState

val linkAccount: LinkAccount?
get() = linkAccountManager.linkAccount.value
Expand Down Expand Up @@ -115,12 +118,20 @@ internal class LinkActivityViewModel @Inject constructor(
launchWebFlow = null
}

fun onVerificationSucceeded() {
_linkScreenState.value = State.Link
}

fun onDismissClicked() {
dismissWithResult?.invoke(LinkActivityResult.Canceled())
}

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
viewModelScope.launch {
warmUpIntegrityManager().fold(
onSuccess = {
navigateToInitialScreen()
navigateToInitialState()
},
onFailure = { error ->
moveToWeb()
Expand All @@ -133,12 +144,38 @@ internal class LinkActivityViewModel @Inject constructor(
}
}

private suspend fun navigateToInitialScreen() {
fun linkScreenScreenCreated() {
viewModelScope.launch {
navigateToLinkScreen()
}
}

private suspend fun navigateToInitialState() {
activityRetainedComponent.configuration.customerInfo.email?.let {
linkAccountManager.lookupConsumer(it).getOrThrow()
}
val linkAccount = linkAccountManager.linkAccount.value
if (linkAccount != null && _linkScreenState.value is State.Loading) {
_linkScreenState.value = State.VerificationDialog(linkAccount)
return
}

_linkScreenState.value = State.Link
navigateToLinkScreen()
}

private suspend fun navigateToLinkScreen() {
val accountStatus = linkAccountManager.accountStatus.first()
val screen = when (accountStatus) {
AccountStatus.Verified -> LinkScreen.Wallet
AccountStatus.NeedsVerification, AccountStatus.VerificationStarted -> LinkScreen.Verification
AccountStatus.SignedOut, AccountStatus.Error -> LinkScreen.SignUp
AccountStatus.Verified -> {
LinkScreen.Wallet
}
AccountStatus.NeedsVerification, AccountStatus.VerificationStarted -> {
LinkScreen.Verification
}
AccountStatus.SignedOut, AccountStatus.Error -> {
LinkScreen.SignUp
}
}
navigate(screen, clearStack = true, launchSingleTop = true)
}
Expand All @@ -158,6 +195,7 @@ internal class LinkActivityViewModel @Inject constructor(
DaggerNativeLinkComponent
.builder()
.configuration(args.configuration)
.eagerLaunch(args.use2faDialog)
.publishableKeyProvider { args.publishableKey }
.stripeAccountIdProvider { args.stripeAccountId }
.savedStateHandle(handle)
Expand All @@ -170,4 +208,10 @@ internal class LinkActivityViewModel @Inject constructor(
}
}

internal sealed interface State {
data class VerificationDialog(val linkAccount: LinkAccount) : State
data object Link : State
data object Loading : State
}

internal class NoArgsException : IllegalArgumentException("NativeLinkArgs not found")
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ internal class LinkPaymentLauncher @Inject internal constructor(
*/
fun present(
configuration: LinkConfiguration,
eagerLaunch: Boolean
) {
val args = LinkActivityContract.Args(
configuration,
configuration = configuration,
use2faDialog = eagerLaunch
)
linkActivityResultLauncher?.launch(args)
analyticsHelper.onLinkLaunched()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ internal class NativeLinkActivityContract @Inject constructor() :
args = NativeLinkArgs(
configuration = input.configuration,
stripeAccountId = paymentConfiguration.stripeAccountId,
publishableKey = paymentConfiguration.publishableKey
publishableKey = paymentConfiguration.publishableKey,
use2faDialog = input.use2faDialog
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ import kotlinx.parcelize.Parcelize
internal data class NativeLinkArgs(
val configuration: LinkConfiguration,
val publishableKey: String,
val stripeAccountId: String?
val stripeAccountId: String?,
val use2faDialog: Boolean
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal annotation class NativeLinkScope
internal interface NativeLinkComponent {
val linkAccountManager: LinkAccountManager
val configuration: LinkConfiguration
val eagerLaunch: Boolean
val linkEventsReporter: LinkEventsReporter
val logger: Logger
val linkConfirmationHandlerFactory: LinkConfirmationHandler.Factory
Expand All @@ -47,6 +48,9 @@ internal interface NativeLinkComponent {
@BindsInstance
fun configuration(configuration: LinkConfiguration): Builder

@BindsInstance
fun eagerLaunch(eagerLaunch: Boolean): Builder

@BindsInstance
fun publishableKeyProvider(@Named(PUBLISHABLE_KEY) publishableKeyProvider: () -> String): Builder

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.stripe.android.link.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
Expand All @@ -27,8 +28,13 @@ internal fun DefaultLinkTheme(
colors = colors.materialColors,
typography = Typography,
shapes = MaterialTheme.shapes,
content = content
)
) {
Surface(
color = MaterialTheme.colors.background
) {
content()
}
}
}
}

Expand Down
Loading

0 comments on commit a4335c8

Please sign in to comment.