Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tolu/link/clean am2 #9985

Closed
wants to merge 4 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ interface ErrorReporter : FraudDetectionErrorReporter {
LINK_WEB_FAILED_TO_PARSE_RESULT_URI(
partialEventName = "link.web.result.parsing_failed"
),
LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER(
partialEventName = "link.native.integrity.preparation_failed"
),
PAYMENT_SHEET_AUTHENTICATORS_NOT_FOUND(
partialEventName = "paymentsheet.authenticators.not_found"
),
Expand Down
1 change: 1 addition & 0 deletions paymentsheet/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation project(':payments-ui-core')
implementation project(':stripe-ui-core')
compileOnly project(':financial-connections')
implementation project(":stripe-attestation")

// Kotlin
implementation libs.kotlin.coroutines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.VisibleForTesting
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
Expand Down Expand Up @@ -40,6 +41,7 @@ internal class LinkActivity : ComponentActivity() {

@VisibleForTesting
internal lateinit var navController: NavHostController
private var webLauncher: ActivityResultLauncher<LinkActivityContract.Args>? = null

@OptIn(ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -59,6 +61,10 @@ internal class LinkActivity : ComponentActivity() {
lifecycleOwner = this,
)

webLauncher = registerForActivityResult(vm.activityRetainedComponent.webLinkActivityContract) { result ->
dismissWithResult(result)
}

setContent {
var bottomSheetContent by remember { mutableStateOf<BottomSheetContent?>(null) }
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Expand All @@ -79,6 +85,7 @@ internal class LinkActivity : ComponentActivity() {
viewModel?.let {
it.navController = navController
it.dismissWithResult = ::dismissWithResult
it.launchWebFlow = ::launchWebFlow
lifecycle.addObserver(it)
}
}
Expand Down Expand Up @@ -131,6 +138,10 @@ internal class LinkActivity : ComponentActivity() {
}
}

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

companion object {
internal const val EXTRA_ARGS = "native_link_args"
internal const val RESULT_COMPLETE = 73563
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.navigation.NavHostController
import com.stripe.android.link.LinkActivity.Companion.getArgs
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.injection.DaggerNativeLinkComponent
import com.stripe.android.link.injection.NativeLinkComponent
import com.stripe.android.link.model.AccountStatus
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.LinkAppBarState
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.attestation.IntegrityRequestManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
Expand All @@ -34,6 +37,9 @@ internal class LinkActivityViewModel @Inject constructor(
confirmationHandlerFactory: ConfirmationHandler.Factory,
private val linkAccountManager: LinkAccountManager,
val eventReporter: EventReporter,
private val integrityRequestManager: IntegrityRequestManager,
private val linkGate: LinkGate,
private val errorReporter: ErrorReporter,
) : ViewModel(), DefaultLifecycleObserver {
val confirmationHandler = confirmationHandlerFactory.create(viewModelScope)
private val _linkState = MutableStateFlow(
Expand All @@ -51,13 +57,21 @@ internal class LinkActivityViewModel @Inject constructor(

var navController: NavHostController? = null
var dismissWithResult: ((LinkActivityResult) -> Unit)? = null
var launchWebFlow: ((LinkConfiguration) -> Unit)? = null

fun handleViewAction(action: LinkAction) {
when (action) {
LinkAction.BackPressed -> handleBackPressed()
}
}

private fun moveToWeb() {
launchWebFlow?.let { launcher ->
navigate(LinkScreen.Loading, clearStack = true)
launcher.invoke(activityRetainedComponent.configuration)
}
}

private fun handleBackPressed() {
navController?.let { navController ->
if (!navController.popBackStack()) {
Expand Down Expand Up @@ -98,11 +112,14 @@ internal class LinkActivityViewModel @Inject constructor(
fun unregisterActivity() {
navController = null
dismissWithResult = null
launchWebFlow = null
}

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
viewModelScope.launch {
if (warmUpIntegrityManager().not()) return@launch

val accountStatus = linkAccountManager.accountStatus.first()
val screen = when (accountStatus) {
AccountStatus.Verified -> LinkScreen.Wallet
Expand All @@ -113,6 +130,22 @@ internal class LinkActivityViewModel @Inject constructor(
}
}

private suspend fun warmUpIntegrityManager(): Boolean {
if (linkGate.useAttestationEndpoints.not()) return true

val result = integrityRequestManager.prepare()
val error = result.exceptionOrNull()
if (error != null) {
moveToWeb()
errorReporter.report(
errorEvent = ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER,
stripeException = LinkEventException(error)
)
return false
}
return true
}

companion object {
fun factory(savedStateHandle: SavedStateHandle? = null): ViewModelProvider.Factory = viewModelFactory {
initializer {
Expand All @@ -127,6 +160,7 @@ internal class LinkActivityViewModel @Inject constructor(
.stripeAccountIdProvider { args.stripeAccountId }
.savedStateHandle(handle)
.context(app)
.application(app)
.build()
.viewModel
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stripe.android.link

import com.stripe.android.core.exception.StripeException

internal class LinkEventException(override val cause: Throwable) : StripeException(cause = cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.stripe.android.link.account

import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.model.AccountStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import javax.inject.Inject

internal class AutoLoginLinkAccountManager @Inject constructor(
private val configuration: LinkConfiguration,
private val linkAccountManager: DefaultLinkAccountManager
): LinkAccountManager by linkAccountManager {
override val accountStatus: Flow<AccountStatus> = linkAccount.map { account ->
if (account != null) return@map account.accountStatus

val customerEmail = configuration.customerInfo.email
?: return@map AccountStatus.SignedOut

val lookupResult = lookupConsumer(customerEmail)
lookupResult.getOrNull()?.accountStatus ?: AccountStatus.Error
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class DefaultLinkAccountManager @Inject constructor(
@VisibleForTesting
override var consumerPublishableKey: String? = null

override val accountStatus = linkAccount.map { it.fetchAccountStatus() }
override val accountStatus = linkAccount.map { it.safeAccountStatus }

override suspend fun lookupConsumer(
email: String,
Expand Down Expand Up @@ -107,7 +107,7 @@ internal class DefaultLinkAccountManager @Inject constructor(
val currentAccount = _linkAccount.value
val currentEmail = currentAccount?.email ?: config.customerInfo.email

return when (val status = currentAccount.fetchAccountStatus()) {
return when (val status = currentAccount.safeAccountStatus) {
AccountStatus.Verified -> {
linkEventsReporter.onInvalidSessionState(LinkEventsReporter.SessionState.Verified)

Expand Down Expand Up @@ -310,20 +310,6 @@ internal class DefaultLinkAccountManager @Inject constructor(
}
}

private suspend fun LinkAccount?.fetchAccountStatus(): AccountStatus =
/**
* If we already fetched an account, return its status, otherwise if a customer email was passed in,
* lookup the account.
*/
this?.accountStatus
?: config.customerInfo.email?.let { customerEmail ->
lookupConsumer(customerEmail).map {
it?.accountStatus
}.getOrElse {
AccountStatus.Error
}
} ?: AccountStatus.SignedOut

private val SignUpConsentAction.consumerAction: ConsumerSignUpConsentAction
get() = when (this) {
SignUpConsentAction.Checkbox ->
Expand All @@ -337,4 +323,7 @@ internal class DefaultLinkAccountManager @Inject constructor(
SignUpConsentAction.ImpliedWithPrefilledEmail ->
ConsumerSignUpConsentAction.ImpliedWithPrefilledEmail
}

private val LinkAccount?.safeAccountStatus
get() = this?.accountStatus ?: AccountStatus.SignedOut
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package com.stripe.android.link.injection

import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.paymentelement.confirmation.DefaultConfirmationHandler
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.attestation.IntegrityRequestManager
import dagger.Module
import dagger.Provides

Expand All @@ -15,13 +18,19 @@ internal object LinkViewModelModule {
component: NativeLinkComponent,
defaultConfirmationHandlerFactory: DefaultConfirmationHandler.Factory,
linkAccountManager: LinkAccountManager,
eventReporter: EventReporter
eventReporter: EventReporter,
integrityRequestManager: IntegrityRequestManager,
linkGate: LinkGate,
errorReporter: ErrorReporter
): LinkActivityViewModel {
return LinkActivityViewModel(
activityRetainedComponent = component,
confirmationHandlerFactory = defaultConfirmationHandlerFactory,
linkAccountManager = linkAccountManager,
eventReporter = eventReporter
eventReporter = eventReporter,
integrityRequestManager = integrityRequestManager,
linkGate = linkGate,
errorReporter = errorReporter
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.link.injection

import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.stripe.android.cards.CardAccountRangeRepository
Expand All @@ -8,6 +9,7 @@ import com.stripe.android.core.injection.PUBLISHABLE_KEY
import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.WebLinkActivityContract
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.analytics.LinkEventsReporter
import com.stripe.android.link.confirmation.LinkConfirmationHandler
Expand Down Expand Up @@ -36,6 +38,7 @@ internal interface NativeLinkComponent {
val linkEventsReporter: LinkEventsReporter
val logger: Logger
val linkConfirmationHandlerFactory: LinkConfirmationHandler.Factory
val webLinkActivityContract: WebLinkActivityContract
val cardAccountRangeRepositoryFactory: CardAccountRangeRepository.Factory
val viewModel: LinkActivityViewModel

Expand All @@ -59,6 +62,9 @@ internal interface NativeLinkComponent {
@BindsInstance
fun statusBarColor(@Named(STATUS_BAR_COLOR) statusBarColor: Int?): Builder

@BindsInstance
fun application(application: Application): Builder

fun build(): NativeLinkComponent
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.link.injection

import android.app.Application
import android.content.Context
import androidx.core.os.LocaleListCompat
import com.stripe.android.BuildConfig
Expand All @@ -26,6 +27,8 @@ import com.stripe.android.link.analytics.DefaultLinkEventsReporter
import com.stripe.android.link.analytics.LinkEventsReporter
import com.stripe.android.link.confirmation.DefaultLinkConfirmationHandler
import com.stripe.android.link.confirmation.LinkConfirmationHandler
import com.stripe.android.link.gate.DefaultLinkGate
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.repositories.LinkApiRepository
import com.stripe.android.link.repositories.LinkRepository
import com.stripe.android.networking.StripeApiRepository
Expand All @@ -38,6 +41,9 @@ import com.stripe.android.paymentsheet.analytics.DefaultEventReporter
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.android.repository.ConsumersApiService
import com.stripe.android.repository.ConsumersApiServiceImpl
import com.stripe.attestation.IntegrityRequestManager
import com.stripe.attestation.IntegrityStandardRequestManager
import com.stripe.attestation.RealStandardIntegrityManagerFactory
import dagger.Binds
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -77,6 +83,10 @@ internal interface NativeLinkModule {
@NativeLinkScope
fun bindsEventReporter(eventReporter: DefaultEventReporter): EventReporter

@Binds
@NativeLinkScope
fun bindsLinkGate(linkGate: DefaultLinkGate): LinkGate

@SuppressWarnings("TooManyFunctions")
companion object {
@Provides
Expand Down Expand Up @@ -161,6 +171,18 @@ internal interface NativeLinkModule {
factory: DefaultLinkConfirmationHandler.Factory
): LinkConfirmationHandler.Factory = factory

@Provides
@NativeLinkScope
fun providesIntegrityStandardRequestManager(
context: Application
): IntegrityRequestManager = IntegrityStandardRequestManager(
cloudProjectNumber = 577365562050, // stripe-payments-sdk-prod
logError = { message, error ->
Logger.getInstance(BuildConfig.DEBUG).error(message, error)
},
factory = RealStandardIntegrityManagerFactory(context)
)

@Provides
@NativeLinkScope
fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.stripe.android.link

import app.cash.turbine.Turbine
import com.stripe.attestation.IntegrityRequestManager

internal class FakeIntegrityRequestManager : IntegrityRequestManager {
var prepareResult: Result<Unit> = Result.success(Unit)
var requestResult: Result<String> = Result.success(TestFactory.VERIFICATION_TOKEN)
private val prepareCalls = Turbine<Unit>()
private val requestTokenCalls = Turbine<String?>()

override suspend fun prepare(): Result<Unit> {
prepareCalls.add(Unit)
return prepareResult
}

override suspend fun requestToken(requestIdentifier: String?): Result<String> {
requestTokenCalls.add(requestIdentifier)
return requestResult
}

suspend fun awaitPrepareCall() {
return prepareCalls.awaitItem()
}

suspend fun awaitRequestTokenCall(): String? {
return requestTokenCalls.awaitItem()
}

fun ensureAllEventsConsumed() {
prepareCalls.ensureAllEventsConsumed()
requestTokenCalls.ensureAllEventsConsumed()
}
}
Loading
Loading