diff --git a/.idea/codestyles/Project.xml b/.idea/codestyles/Project.xml
index aa7276802a9..90ee029868b 100644
--- a/.idea/codestyles/Project.xml
+++ b/.idea/codestyles/Project.xml
@@ -32,8 +32,6 @@
-
-
@@ -160,4 +158,4 @@
-
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16e717f3f90..b82ef05ba41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
## XX.XX.XX - 20XX-XX-XX
+### Financial Connections
+- [FIXED] Fixes a rare crash that could occur when presenting a bottom sheet.
+
## 21.3.1 - 2025-01-06
### PaymentSheet
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt
index 663828529ba..4797ded5c35 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt
@@ -36,6 +36,7 @@ import com.stripe.android.financialconnections.features.partnerauth.PartnerAuthS
import com.stripe.android.financialconnections.features.reset.ResetScreen
import com.stripe.android.financialconnections.features.success.SuccessScreen
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.navigation.bottomsheet.LifecycleAwareContent
import com.stripe.android.financialconnections.navigation.bottomsheet.bottomSheet
import com.stripe.android.financialconnections.presentation.parentViewModel
@@ -283,6 +284,10 @@ internal fun NavGraphBuilder.bottomSheet(
route = destination.fullRoute,
arguments = destination.arguments,
deepLinks = deepLinks,
- content = { destination.Composable(navBackStackEntry = it) }
+ content = { navBackStackEntry ->
+ LifecycleAwareContent(navBackStackEntry) {
+ destination.Composable(navBackStackEntry = navBackStackEntry)
+ }
+ }
)
}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BackstackSafeContent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BackstackSafeContent.kt
new file mode 100644
index 00000000000..6d4e8c7cded
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BackstackSafeContent.kt
@@ -0,0 +1,53 @@
+package com.stripe.android.financialconnections.navigation.bottomsheet
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * Prevents the content from being rendered until the lifecycle is ready.
+ *
+ * Addresses: You cannot access the NavBackStackEntry's ViewModels after the NavBackStackEntry is destroyed.
+ * https://github.com/google/accompanist/issues/1487#top
+ */
+@Composable
+internal fun LifecycleAwareContent(
+ lifecycleOwner: LifecycleOwner,
+ content: @Composable () -> Unit
+) {
+ val isReady = remember { mutableStateOf(false) }
+
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> isReady.value = true
+ Lifecycle.Event.ON_DESTROY -> isReady.value = false
+ else -> Unit
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ // This will typically be used on bottom sheets, where we see the NavBackStackEntry issue.
+ // animating the content height will ensure we respect the bottom sheet open animation.
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .animateContentSize(animationSpec = tween())
+ ) {
+ if (isReady.value) { content() }
+ }
+}
diff --git a/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt b/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt
index 17dbe03b939..f38160721ed 100644
--- a/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt
+++ b/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt
@@ -19,6 +19,7 @@ data class ElementsSession(
val cardBrandChoice: CardBrandChoice?,
val isGooglePayEnabled: Boolean,
val sessionsError: Throwable? = null,
+ val sessionId: String?,
) : StripeModel {
val linkPassthroughModeEnabled: Boolean
@@ -37,6 +38,9 @@ data class ElementsSession(
return (allowsLink && hasValidFundingSource) || linkPassthroughModeEnabled
}
+ val useAttestationEndpointsForLink: Boolean
+ get() = linkSettings?.useAttestationEndpoints ?: false
+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class LinkSettings(
@@ -46,6 +50,7 @@ data class ElementsSession(
val linkFlags: Map,
val disableLinkSignup: Boolean,
val linkConsumerIncentive: LinkConsumerIncentive?,
+ val useAttestationEndpoints: Boolean
) : StripeModel
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -127,6 +132,7 @@ data class ElementsSession(
cardBrandChoice = null,
isGooglePayEnabled = true,
sessionsError = sessionsError,
+ sessionId = null
)
}
}
diff --git a/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt b/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt
index cb11f8b3882..9fd9b97daa9 100644
--- a/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt
+++ b/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt
@@ -69,6 +69,7 @@ internal class ElementsSessionJsonParser(
cardBrandChoice = cardBrandChoice,
isGooglePayEnabled = googlePayPreference != "disabled",
externalPaymentMethodData = externalPaymentMethodData,
+ sessionId = elementsSessionId
)
} else {
null
@@ -140,6 +141,7 @@ internal class ElementsSessionJsonParser(
): ElementsSession.LinkSettings {
val disableLinkSignup = json?.optBoolean(FIELD_DISABLE_LINK_SIGNUP) ?: false
val linkPassthroughModeEnabled = json?.optBoolean(FIELD_LINK_PASSTHROUGH_MODE_ENABLED) ?: false
+ val useLinkAttestationEndpoints = json?.optBoolean(FIELD_USE_LINK_ATTESTATION_ENDPOINTS) ?: false
val linkMode = json?.optString(FIELD_LINK_MODE)?.let { mode ->
LinkMode.entries.firstOrNull { it.value == mode }
@@ -162,6 +164,7 @@ internal class ElementsSessionJsonParser(
linkFlags = linkFlags,
disableLinkSignup = disableLinkSignup,
linkConsumerIncentive = linkConsumerIncentive,
+ useAttestationEndpoints = useLinkAttestationEndpoints
)
}
@@ -327,6 +330,7 @@ internal class ElementsSessionJsonParser(
private const val FIELD_LINK_PASSTHROUGH_MODE_ENABLED = "link_passthrough_mode_enabled"
private const val FIELD_LINK_MODE = "link_mode"
private const val FIELD_DISABLE_LINK_SIGNUP = "link_mobile_disable_signup"
+ private const val FIELD_USE_LINK_ATTESTATION_ENDPOINTS = "link_mobile_use_attestation_endpoints"
private const val FIELD_MERCHANT_COUNTRY = "merchant_country"
private const val FIELD_PAYMENT_METHOD_PREFERENCE = "payment_method_preference"
private const val FIELD_UNACTIVATED_PAYMENT_METHOD_TYPES = "unactivated_payment_method_types"
diff --git a/paymentsheet/build.gradle b/paymentsheet/build.gradle
index 465848cdf7b..9187e2bd137 100644
--- a/paymentsheet/build.gradle
+++ b/paymentsheet/build.gradle
@@ -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
diff --git a/paymentsheet/res/values/themes.xml b/paymentsheet/res/values/themes.xml
index b922dae5a5c..054956a56c8 100644
--- a/paymentsheet/res/values/themes.xml
+++ b/paymentsheet/res/values/themes.xml
@@ -44,4 +44,10 @@
- @color/stripe_link_window_background
- true
+
+
diff --git a/paymentsheet/src/main/AndroidManifest.xml b/paymentsheet/src/main/AndroidManifest.xml
index c23e33c7ee1..9370a71e515 100644
--- a/paymentsheet/src/main/AndroidManifest.xml
+++ b/paymentsheet/src/main/AndroidManifest.xml
@@ -1,65 +1,70 @@
-
+
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePaymentSheetDefaultTheme" />
+ android:exported="false"
+ android:theme="@style/StripePayLauncherDefaultTheme" />
-
+
-
+ android:configChanges="orientation|keyboard|keyboardHidden|screenLayout|screenSize|smallestScreenSize"
+ android:exported="false"
+ android:label="@string/stripe_link"
+ android:theme="@style/StripeLinkTranslucentTheme"
+ android:windowSoftInputMode="adjustResize" />
-
+ android:theme="@style/StripeTransparentTheme" >
@@ -67,10 +72,11 @@
+ android:path="/${applicationId}"
+ android:scheme="link-popup" />
-
+
+
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/EagerPath.kt b/paymentsheet/src/main/java/com/stripe/android/link/EagerPath.kt
new file mode 100644
index 00000000000..cf083c6a943
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/EagerPath.kt
@@ -0,0 +1,49 @@
+package com.stripe.android.link
+
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.Card
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.window.Dialog
+import com.stripe.android.link.model.LinkAccount
+import com.stripe.android.link.theme.DefaultLinkTheme
+import com.stripe.android.link.ui.verification.VerificationScreen
+import com.stripe.android.link.ui.verification.VerificationViewModel
+
+@Composable
+internal fun EagerPath(
+ linkAccount: LinkAccount?,
+ viewModel: LinkActivityViewModel,
+) {
+ VerificationDialog(
+ linkAccount = linkAccount,
+ navigateAndClearStack = { screen ->
+ viewModel.navigate(screen, clearStack = true)
+ },
+ goBack = {
+ viewModel.goBack()
+ }
+ )
+}
+
+@Composable
+private fun VerificationDialog(
+ linkAccount: LinkAccount?,
+ navigateAndClearStack: (LinkScreen) -> Unit,
+ goBack: () -> Unit
+) {
+ val viewModel: VerificationViewModel = linkViewModel { parentComponent ->
+ VerificationViewModel.factory(
+ parentComponent = parentComponent,
+ goBack = goBack,
+ navigateAndClearStack = navigateAndClearStack,
+ linkAccount = linkAccount
+ )
+ }
+ Dialog (onDismissRequest = {}) {
+ DefaultLinkTheme {
+ Card {
+ VerificationScreen(viewModel)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt
index d4e0d630d99..4b666fe558c 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt
@@ -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
@@ -40,6 +41,7 @@ internal class LinkActivity : ComponentActivity() {
@VisibleForTesting
internal lateinit var navController: NavHostController
+ private var webLauncher: ActivityResultLauncher? = null
@OptIn(ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -59,6 +61,10 @@ internal class LinkActivity : ComponentActivity() {
lifecycleOwner = this,
)
+ webLauncher = registerForActivityResult(vm.activityRetainedComponent.webLinkActivityContract) { result ->
+ dismissWithResult(result)
+ }
+
setContent {
var bottomSheetContent by remember { mutableStateOf(null) }
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
@@ -79,10 +85,16 @@ internal class LinkActivity : ComponentActivity() {
viewModel?.let {
it.navController = navController
it.dismissWithResult = ::dismissWithResult
+ it.launchWebFlow = ::launchWebFlow
lifecycle.addObserver(it)
}
}
+// EagerPath(
+// linkAccount = vm.linkAccount,
+// viewModel = vm
+// )
+
EventReporterProvider(
eventReporter = vm.eventReporter
) {
@@ -95,7 +107,10 @@ internal class LinkActivity : ComponentActivity() {
onUpdateSheetContent = {
bottomSheetContent = it
},
- onBackPressed = onBackPressedDispatcher::onBackPressed
+ onBackPressed = onBackPressedDispatcher::onBackPressed,
+ moveToWebFlow = {
+ vm.moveToWeb()
+ }
)
}
}
@@ -106,8 +121,8 @@ internal class LinkActivity : ComponentActivity() {
LinkActivityContract.EXTRA_RESULT to result
)
this@LinkActivity.setResult(
- RESULT_COMPLETE,
- Intent().putExtras(bundle)
+ NativeConstants.RESULT_COMPLETE,
+ intent.putExtras(bundle)
)
this@LinkActivity.finish()
}
@@ -131,20 +146,22 @@ 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
internal fun createIntent(
context: Context,
args: NativeLinkArgs
): Intent {
return Intent(context, LinkActivity::class.java)
- .putExtra(EXTRA_ARGS, args)
+ .putExtra(NativeConstants.EXTRA_ARGS, args)
}
internal fun getArgs(savedStateHandle: SavedStateHandle): NativeLinkArgs? {
- return savedStateHandle.get(EXTRA_ARGS)
+ return savedStateHandle.get(NativeConstants.EXTRA_ARGS)
}
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityContract.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityContract.kt
index 7c32a8ee3ff..ce07bd6d772 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityContract.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityContract.kt
@@ -3,54 +3,44 @@ package com.stripe.android.link
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
-import com.stripe.android.PaymentConfiguration
-import com.stripe.android.core.utils.FeatureFlags
-import com.stripe.android.link.serialization.PopupPayload
-import com.stripe.android.networking.StripeRepository
+import com.stripe.android.link.model.LinkAccount
import javax.inject.Inject
internal class LinkActivityContract @Inject internal constructor(
- private val stripeRepository: StripeRepository,
+ private val nativeLinkActivityContract: NativeLinkActivityContract,
+ private val webLinkActivityContract: WebLinkActivityContract
) : ActivityResultContract() {
override fun createIntent(context: Context, input: Args): Intent {
- return if (FeatureFlags.nativeLinkEnabled.isEnabled) {
- nativeIntent(context, input)
+ return if (useNativeLink(input)) {
+ nativeLinkActivityContract.createIntent(context, input).apply {
+ putExtra(EXTRA_USED_NATIVE_CONTRACT, true)
+ }
} else {
- webIntent(context, input)
+ webLinkActivityContract.createIntent(context, input).apply {
+ putExtra(EXTRA_USED_NATIVE_CONTRACT, false)
+ }
}
}
override fun parseResult(resultCode: Int, intent: Intent?): LinkActivityResult {
- return createLinkActivityResult(resultCode, intent)
- }
-
- private fun webIntent(context: Context, input: Args): Intent {
- val paymentConfiguration = PaymentConfiguration.getInstance(context)
- val payload = PopupPayload.create(
- configuration = input.configuration,
- context = context,
- publishableKey = paymentConfiguration.publishableKey,
- stripeAccount = paymentConfiguration.stripeAccountId,
- paymentUserAgent = stripeRepository.buildPaymentUserAgent(),
- )
- return LinkForegroundActivity.createIntent(context, payload.toUrl())
+ val usedNativeContract = intent?.getBooleanExtra(EXTRA_USED_NATIVE_CONTRACT, false) ?: false
+ return if (usedNativeContract) {
+ nativeLinkActivityContract.parseResult(resultCode, intent)
+ } else {
+ webLinkActivityContract.parseResult(resultCode, intent)
+ }
}
- private fun nativeIntent(context: Context, input: Args): Intent {
- val paymentConfiguration = PaymentConfiguration.getInstance(context)
- return LinkActivity.createIntent(
- context = context,
- args = NativeLinkArgs(
- configuration = input.configuration,
- stripeAccountId = paymentConfiguration.stripeAccountId,
- publishableKey = paymentConfiguration.publishableKey
- )
- )
+ private fun useNativeLink(input: Args): Boolean {
+// if (FeatureFlags.nativeLinkEnabled.isEnabled) return true
+// return input.configuration.useAttestationEndpointsForLink
+ return true
}
data class Args internal constructor(
- internal val configuration: LinkConfiguration
+ internal val configuration: LinkConfiguration,
+ internal val linkAccount: LinkAccount? = null
)
data class Result(
@@ -60,5 +50,6 @@ internal class LinkActivityContract @Inject internal constructor(
companion object {
internal const val EXTRA_RESULT =
"com.stripe.android.link.LinkActivityContract.extra_result"
+ internal const val EXTRA_USED_NATIVE_CONTRACT = "used_native_contract"
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityResult.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityResult.kt
index d960b7e3504..dcf4e7d2cab 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityResult.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityResult.kt
@@ -1,14 +1,8 @@
package com.stripe.android.link
-import android.app.Activity
-import android.content.Intent
import android.os.Parcelable
-import android.util.Base64
-import androidx.core.os.BundleCompat
import com.stripe.android.model.PaymentMethod
-import com.stripe.android.model.parsers.PaymentMethodJsonParser
import kotlinx.parcelize.Parcelize
-import org.json.JSONObject
internal sealed class LinkActivityResult : Parcelable {
/**
@@ -47,73 +41,3 @@ internal sealed class LinkActivityResult : Parcelable {
val error: Throwable
) : LinkActivityResult()
}
-
-internal fun createLinkActivityResult(resultCode: Int, intent: Intent?): LinkActivityResult {
- return when (resultCode) {
- Activity.RESULT_CANCELED -> {
- LinkActivityResult.Canceled()
- }
-
- LinkForegroundActivity.RESULT_FAILURE -> {
- val exception = intent?.extras?.let {
- BundleCompat.getSerializable(
- it,
- LinkForegroundActivity.EXTRA_FAILURE,
- Exception::class.java
- )
- }
- if (exception != null) {
- LinkActivityResult.Failed(exception)
- } else {
- LinkActivityResult.Canceled()
- }
- }
-
- LinkForegroundActivity.RESULT_COMPLETE -> {
- val redirectUri = intent?.data ?: return LinkActivityResult.Canceled()
- when (redirectUri.getQueryParameter("link_status")) {
- "complete" -> {
- val paymentMethod = redirectUri.getQueryParameter("pm")
- ?.parsePaymentMethod()
- if (paymentMethod == null) {
- LinkActivityResult.Canceled()
- } else {
- LinkActivityResult.PaymentMethodObtained(paymentMethod)
- }
- }
-
- "logout" -> {
- LinkActivityResult.Canceled(LinkActivityResult.Canceled.Reason.LoggedOut)
- }
-
- else -> {
- LinkActivityResult.Canceled()
- }
- }
- }
-
- LinkActivity.RESULT_COMPLETE -> {
- handleNativeLinkResult(intent)
- }
-
- else -> {
- LinkActivityResult.Canceled()
- }
- }
-}
-
-private fun handleNativeLinkResult(intent: Intent?): LinkActivityResult {
- val result = intent?.extras?.let {
- BundleCompat.getParcelable(it, LinkActivityContract.EXTRA_RESULT, LinkActivityResult::class.java)
- }
- return result ?: LinkActivityResult.Canceled()
-}
-
-private fun String.parsePaymentMethod(): PaymentMethod? = try {
- val decodedPaymentMethod = String(Base64.decode(this, 0), Charsets.UTF_8)
- val paymentMethod = PaymentMethodJsonParser()
- .parse(JSONObject(decodedPaymentMethod))
- paymentMethod
-} catch (e: Exception) {
- null
-}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt
index c2a53b2f89c..34a2037dd45 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt
@@ -13,8 +13,12 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.navigation.NavHostController
+import com.stripe.android.core.Logger
+import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.link.LinkActivity.Companion.getArgs
import com.stripe.android.link.account.LinkAccountManager
+import com.stripe.android.link.account.LinkAuth
+import com.stripe.android.link.injection.APPLICATION_ID
import com.stripe.android.link.injection.DaggerNativeLinkComponent
import com.stripe.android.link.injection.NativeLinkComponent
import com.stripe.android.link.model.AccountStatus
@@ -23,17 +27,23 @@ import com.stripe.android.link.ui.LinkAppBarState
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
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
import kotlinx.coroutines.launch
import javax.inject.Inject
+import javax.inject.Named
internal class LinkActivityViewModel @Inject constructor(
val activityRetainedComponent: NativeLinkComponent,
confirmationHandlerFactory: ConfirmationHandler.Factory,
private val linkAccountManager: LinkAccountManager,
+ private val linkAuth: LinkAuth,
val eventReporter: EventReporter,
+ private val integrityRequestManager: IntegrityRequestManager,
+ private val logger: Logger,
+ @Named(APPLICATION_ID) val applicationId: String
) : ViewModel(), DefaultLifecycleObserver {
val confirmationHandler = confirmationHandlerFactory.create(viewModelScope)
private val _linkState = MutableStateFlow(
@@ -51,6 +61,7 @@ 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) {
@@ -58,6 +69,13 @@ internal class LinkActivityViewModel @Inject constructor(
}
}
+ fun moveToWeb() {
+ launchWebFlow?.let { launcher ->
+ navigate(LinkScreen.Loading, clearStack = true)
+ launcher.invoke(activityRetainedComponent.configuration)
+ }
+ }
+
private fun handleBackPressed() {
navController?.let { navController ->
if (!navController.popBackStack()) {
@@ -103,6 +121,8 @@ internal class LinkActivityViewModel @Inject constructor(
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
@@ -113,6 +133,17 @@ internal class LinkActivityViewModel @Inject constructor(
}
}
+ private suspend fun warmUpIntegrityManager(): Boolean {
+ val result = integrityRequestManager.prepare()
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ FeatureFlags.nativeLinkEnabled.setEnabled(false)
+ launchWebFlow?.invoke(activityRetainedComponent.configuration)
+ return false
+ }
+ return true
+ }
+
companion object {
fun factory(savedStateHandle: SavedStateHandle? = null): ViewModelProvider.Factory = viewModelFactory {
initializer {
@@ -123,10 +154,12 @@ internal class LinkActivityViewModel @Inject constructor(
DaggerNativeLinkComponent
.builder()
.configuration(args.configuration)
+ .linkAccount(args.linkAccount)
.publishableKeyProvider { args.publishableKey }
.stripeAccountIdProvider { args.stripeAccountId }
.savedStateHandle(handle)
.context(app)
+ .application(app)
.build()
.viewModel
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkConfiguration.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkConfiguration.kt
index b28a67c81b8..858038c37e6 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkConfiguration.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkConfiguration.kt
@@ -3,6 +3,7 @@ package com.stripe.android.link
import android.os.Parcelable
import com.stripe.android.model.StripeIntent
import com.stripe.android.paymentsheet.addresselement.AddressDetails
+import com.stripe.android.paymentsheet.state.PaymentElementLoader
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -15,6 +16,9 @@ internal data class LinkConfiguration(
val passthroughModeEnabled: Boolean,
val flags: Map,
val cardBrandChoice: CardBrandChoice?,
+ val useAttestationEndpointsForLink: Boolean,
+ val elementSessionId: String?,
+ val initializationMode: PaymentElementLoader.InitializationMode
) : Parcelable {
@Parcelize
data class CustomerInfo(
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkConfigurationCoordinator.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkConfigurationCoordinator.kt
index 978958ca5fb..1d0684352bc 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkConfigurationCoordinator.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkConfigurationCoordinator.kt
@@ -2,6 +2,7 @@ package com.stripe.android.link
import com.stripe.android.link.injection.LinkComponent
import com.stripe.android.link.model.AccountStatus
+import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.PaymentMethodCreateParams
@@ -22,6 +23,8 @@ internal interface LinkConfigurationCoordinator {
fun getAccountStatusFlow(configuration: LinkConfiguration): Flow
+ fun getAccountFlow(configuration: LinkConfiguration): Flow
+
suspend fun signInWithUserInput(
configuration: LinkConfiguration,
userInput: UserInput
@@ -62,6 +65,10 @@ internal class RealLinkConfigurationCoordinator @Inject internal constructor(
override fun getAccountStatusFlow(configuration: LinkConfiguration): Flow =
getLinkPaymentLauncherComponent(configuration).linkAccountManager.accountStatus
+ override fun getAccountFlow(configuration: LinkConfiguration): Flow {
+ return getLinkPaymentLauncherComponent(configuration).linkAccountManager.linkAccount
+ }
+
/**
* Trigger Link sign in with the input collected from the user inline in PaymentSheet, whether
* it's a new or existing account.
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressArgs.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressArgs.kt
new file mode 100644
index 00000000000..78e02afda08
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressArgs.kt
@@ -0,0 +1,13 @@
+package com.stripe.android.link
+
+import android.os.Parcelable
+import com.stripe.android.link.model.LinkAccount
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+internal data class LinkExpressArgs(
+ val configuration: LinkConfiguration,
+ val publishableKey: String,
+ val stripeAccountId: String?,
+ val linkAccount: LinkAccount?
+) : Parcelable
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressContract.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressContract.kt
new file mode 100644
index 00000000000..c1d7486ac05
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressContract.kt
@@ -0,0 +1,53 @@
+package com.stripe.android.link
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.os.BundleCompat
+import com.stripe.android.PaymentConfiguration
+import com.stripe.android.link.model.LinkAccount
+import com.stripe.android.link.ui.verification.VerificationActivity
+import javax.inject.Inject
+
+internal class LinkExpressContract @Inject constructor() :
+ ActivityResultContract() {
+
+ override fun createIntent(context: Context, input: Args): Intent {
+ val paymentConfiguration = PaymentConfiguration.getInstance(context)
+ return VerificationActivity.createIntent(
+ context = context,
+ args = LinkExpressArgs(
+ configuration = input.configuration,
+ stripeAccountId = paymentConfiguration.stripeAccountId,
+ publishableKey = paymentConfiguration.publishableKey,
+ linkAccount = input.linkAccount
+ )
+ )
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): LinkExpressResult {
+ return when (resultCode) {
+ VerificationActivity.RESULT_COMPLETE -> {
+ val result = intent?.extras?.let {
+ BundleCompat.getParcelable(it, EXTRA_RESULT, LinkExpressResult::class.java)
+ }
+ return result ?: LinkExpressResult.Canceled
+ }
+ Activity.RESULT_CANCELED -> {
+ LinkExpressResult.Canceled
+ }
+ else -> LinkExpressResult.Canceled
+ }
+ }
+
+ data class Args(
+ val configuration: LinkConfiguration,
+ val linkAccount: LinkAccount
+ )
+
+ companion object {
+ internal const val EXTRA_RESULT =
+ "com.stripe.android.link.LinkExpressContract.extra_result"
+ }
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressLauncher.kt
new file mode 100644
index 00000000000..b975fd886c3
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressLauncher.kt
@@ -0,0 +1,67 @@
+package com.stripe.android.link
+
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.ActivityResultRegistry
+import com.stripe.android.link.model.LinkAccount
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class LinkExpressLauncher @Inject constructor(
+ private val linkExpressContract: LinkExpressContract
+) {
+ private var linkActivityResultLauncher:
+ ActivityResultLauncher? = null
+
+ fun register(
+ activityResultRegistry: ActivityResultRegistry,
+ callback: (LinkExpressResult) -> Unit,
+ ) {
+ linkActivityResultLauncher = activityResultRegistry.register(
+ "LinkPaymentLauncher",
+ linkExpressContract,
+ ) { linkExpressResult ->
+ handleActivityResult(linkExpressResult, callback)
+ }
+ }
+
+ fun register(
+ activityResultCaller: ActivityResultCaller,
+ callback: (LinkExpressResult) -> Unit,
+ ) {
+ linkActivityResultLauncher = activityResultCaller.registerForActivityResult(
+ linkExpressContract
+ ) { linkExpressContract ->
+ handleActivityResult(linkExpressContract, callback)
+ }
+ }
+
+ private fun handleActivityResult(
+ linkExpressResult: LinkExpressResult,
+ nextStep: (LinkExpressResult) -> Unit
+ ) {
+ nextStep(linkExpressResult)
+ }
+
+ fun unregister() {
+ linkActivityResultLauncher?.unregister()
+ linkActivityResultLauncher = null
+ }
+
+ /**
+ * Launch the Link UI to process a payment.
+ *
+ * @param configuration The payment and customer settings
+ */
+ fun present(
+ configuration: LinkConfiguration,
+ linkAccount: LinkAccount
+ ) {
+ val args = LinkExpressContract.Args(
+ configuration = configuration,
+ linkAccount = linkAccount
+ )
+ linkActivityResultLauncher?.launch(args)
+ }
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressResult.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressResult.kt
new file mode 100644
index 00000000000..f4244ef2cce
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkExpressResult.kt
@@ -0,0 +1,17 @@
+package com.stripe.android.link
+
+import android.os.Parcelable
+import com.stripe.android.link.model.LinkAccount
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+internal sealed interface LinkExpressResult: Parcelable {
+ @Parcelize
+ data class Authenticated(val linkAccount: LinkAccount): LinkExpressResult
+
+ @Parcelize
+ data object Canceled : LinkExpressResult
+
+ @Parcelize
+ data class Failed(val error: Throwable) : LinkExpressResult
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt
index 63f8840547a..ebea3278835 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt
@@ -4,8 +4,10 @@ import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import com.stripe.android.link.LinkActivityResult.PaymentMethodObtained
+import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.account.LinkStore
import com.stripe.android.link.injection.LinkAnalyticsComponent
+import com.stripe.android.link.model.LinkAccount
import javax.inject.Inject
import javax.inject.Singleton
@@ -72,9 +74,11 @@ internal class LinkPaymentLauncher @Inject internal constructor(
*/
fun present(
configuration: LinkConfiguration,
+ linkAccount: LinkAccount?
) {
val args = LinkActivityContract.Args(
- configuration,
+ configuration = configuration,
+ linkAccount = linkAccount
)
linkActivityResultLauncher?.launch(args)
analyticsHelper.onLinkLaunched()
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/NativeConstants.kt b/paymentsheet/src/main/java/com/stripe/android/link/NativeConstants.kt
new file mode 100644
index 00000000000..98cdc3e4555
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/NativeConstants.kt
@@ -0,0 +1,6 @@
+package com.stripe.android.link
+
+internal object NativeConstants {
+ internal const val EXTRA_ARGS = "native_link_args"
+ internal const val RESULT_COMPLETE = 73563
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt
new file mode 100644
index 00000000000..16404a6cef3
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt
@@ -0,0 +1,44 @@
+package com.stripe.android.link
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.os.BundleCompat
+import com.stripe.android.PaymentConfiguration
+import javax.inject.Inject
+
+internal class NativeLinkActivityContract @Inject constructor() :
+ ActivityResultContract() {
+ override fun createIntent(context: Context, input: LinkActivityContract.Args): Intent {
+ val paymentConfiguration = PaymentConfiguration.getInstance(context)
+ return LinkActivity.createIntent(
+ context = context,
+ args = NativeLinkArgs(
+ configuration = input.configuration,
+ stripeAccountId = paymentConfiguration.stripeAccountId,
+ publishableKey = paymentConfiguration.publishableKey,
+ linkAccount = input.linkAccount
+ )
+ )
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): LinkActivityResult {
+ return when (resultCode) {
+ Activity.RESULT_CANCELED -> {
+ LinkActivityResult.Canceled()
+ }
+
+ NativeConstants.RESULT_COMPLETE -> {
+ val result = intent?.extras?.let {
+ BundleCompat.getParcelable(it, LinkActivityContract.EXTRA_RESULT, LinkActivityResult::class.java)
+ }
+ return result ?: LinkActivityResult.Canceled()
+ }
+
+ else -> {
+ LinkActivityResult.Canceled()
+ }
+ }
+ }
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt
index c4416cbf717..d7bcf8426b3 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt
@@ -1,11 +1,13 @@
package com.stripe.android.link
import android.os.Parcelable
+import com.stripe.android.link.model.LinkAccount
import kotlinx.parcelize.Parcelize
@Parcelize
internal data class NativeLinkArgs(
val configuration: LinkConfiguration,
val publishableKey: String,
- val stripeAccountId: String?
+ val stripeAccountId: String?,
+ val linkAccount: LinkAccount?
) : Parcelable
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/WebLinkActivityContract.kt b/paymentsheet/src/main/java/com/stripe/android/link/WebLinkActivityContract.kt
new file mode 100644
index 00000000000..8c3bc7e7bf5
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/WebLinkActivityContract.kt
@@ -0,0 +1,91 @@
+package com.stripe.android.link
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.util.Base64
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.os.BundleCompat
+import com.stripe.android.PaymentConfiguration
+import com.stripe.android.link.serialization.PopupPayload
+import com.stripe.android.model.PaymentMethod
+import com.stripe.android.model.parsers.PaymentMethodJsonParser
+import com.stripe.android.networking.StripeRepository
+import org.json.JSONObject
+import javax.inject.Inject
+
+internal class WebLinkActivityContract @Inject internal constructor(
+ private val stripeRepository: StripeRepository,
+) : ActivityResultContract() {
+
+ override fun createIntent(context: Context, input: LinkActivityContract.Args): Intent {
+ val paymentConfiguration = PaymentConfiguration.getInstance(context)
+ val payload = PopupPayload.create(
+ configuration = input.configuration,
+ context = context,
+ publishableKey = paymentConfiguration.publishableKey,
+ stripeAccount = paymentConfiguration.stripeAccountId,
+ paymentUserAgent = stripeRepository.buildPaymentUserAgent(),
+ )
+ return LinkForegroundActivity.createIntent(context, payload.toUrl())
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): LinkActivityResult {
+ return when (resultCode) {
+ LinkForegroundActivity.RESULT_FAILURE -> {
+ val exception = intent?.extras?.let {
+ BundleCompat.getSerializable(
+ it,
+ LinkForegroundActivity.EXTRA_FAILURE,
+ Exception::class.java
+ )
+ }
+ if (exception != null) {
+ LinkActivityResult.Failed(exception)
+ } else {
+ LinkActivityResult.Canceled()
+ }
+ }
+
+ LinkForegroundActivity.RESULT_COMPLETE -> {
+ val redirectUri = intent?.data ?: return LinkActivityResult.Canceled()
+ when (redirectUri.getQueryParameter("link_status")) {
+ "complete" -> {
+ val paymentMethod = redirectUri.getQueryParameter("pm")
+ ?.parsePaymentMethod()
+ if (paymentMethod == null) {
+ LinkActivityResult.Canceled()
+ } else {
+ LinkActivityResult.PaymentMethodObtained(paymentMethod)
+ }
+ }
+
+ "logout" -> {
+ LinkActivityResult.Canceled(LinkActivityResult.Canceled.Reason.LoggedOut)
+ }
+
+ else -> {
+ LinkActivityResult.Canceled()
+ }
+ }
+ }
+
+ Activity.RESULT_CANCELED -> {
+ LinkActivityResult.Canceled()
+ }
+ else -> {
+ LinkActivityResult.Canceled()
+ }
+ }
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught", "SwallowedException")
+ private fun String.parsePaymentMethod(): PaymentMethod? = try {
+ val decodedPaymentMethod = String(Base64.decode(this, 0), Charsets.UTF_8)
+ val paymentMethod = PaymentMethodJsonParser()
+ .parse(JSONObject(decodedPaymentMethod))
+ paymentMethod
+ } catch (e: Exception) {
+ null
+ }
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/AttestLinkAccountManager.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/AttestLinkAccountManager.kt
new file mode 100644
index 00000000000..2a3e838c21c
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/account/AttestLinkAccountManager.kt
@@ -0,0 +1,76 @@
+package com.stripe.android.link.account
+
+import com.stripe.android.link.LinkConfiguration
+import com.stripe.android.link.account.DefaultLinkAuth.Companion.NO_ELEMENT_SESSION_ID
+import com.stripe.android.link.injection.APPLICATION_ID
+import com.stripe.android.link.model.LinkAccount
+import com.stripe.android.link.repositories.LinkRepository
+import com.stripe.android.link.ui.inline.SignUpConsentAction
+import com.stripe.attestation.IntegrityRequestManager
+import javax.inject.Inject
+import javax.inject.Named
+
+internal class AttestLinkAccountManager @Inject constructor(
+ private val config: LinkConfiguration,
+ private val defaultLinkAccountManager: DefaultLinkAccountManager,
+ private val integrityRequestManager: IntegrityRequestManager,
+ private val linkRepository: LinkRepository,
+ @Named(APPLICATION_ID) private val applicationId: String
+): LinkAccountManager by defaultLinkAccountManager {
+
+ init {
+
+ }
+
+ override suspend fun lookupConsumer(
+ email: String,
+ startSession: Boolean
+ ): Result {
+ return runCatching {
+ val tokenResult = integrityRequestManager.requestToken().getOrThrow()
+ val lookupResult = linkRepository.mobileLookupConsumer(
+ verificationToken = tokenResult,
+ appId = applicationId,
+ email = email,
+ sessionId = config.elementSessionId?.takeIf { it.isNotBlank() } ?: throw NO_ELEMENT_SESSION_ID
+ )
+ val lookup = lookupResult.getOrThrow()
+ val linkAccount = lookup.consumerSession?.let { LinkAccount(it) }
+
+ if (startSession) {
+ defaultLinkAccountManager.setAccountNullable(
+ consumerSession = lookup.consumerSession,
+ publishableKey = lookup.publishableKey
+ )
+ }
+ linkAccount
+ }
+ }
+
+ override suspend fun signUp(
+ email: String,
+ phone: String,
+ country: String,
+ name: String?,
+ consentAction: SignUpConsentAction
+ ): Result {
+ return runCatching {
+ val tokenResult = integrityRequestManager.requestToken().getOrThrow()
+ val consumerSessionSignUpResult = linkRepository.mobileSignUp(
+ name = name,
+ email = email,
+ phoneNumber = phone,
+ country = country,
+ consentAction = consentAction.consumerAction,
+ verificationToken = tokenResult,
+ appId = applicationId
+ )
+ val consumerSessionSignUp = consumerSessionSignUpResult.getOrThrow()
+ defaultLinkAccountManager.setAccount(
+ consumerSession = consumerSessionSignUp.consumerSession,
+ publishableKey = consumerSessionSignUp.publishableKey
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/ConsumerAction.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/ConsumerAction.kt
new file mode 100644
index 00000000000..96a6837c89e
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/account/ConsumerAction.kt
@@ -0,0 +1,18 @@
+package com.stripe.android.link.account
+
+import com.stripe.android.link.ui.inline.SignUpConsentAction
+import com.stripe.android.model.ConsumerSignUpConsentAction
+
+internal val SignUpConsentAction.consumerAction: ConsumerSignUpConsentAction
+ get() = when (this) {
+ SignUpConsentAction.Checkbox ->
+ ConsumerSignUpConsentAction.Checkbox
+ SignUpConsentAction.CheckboxWithPrefilledEmail ->
+ ConsumerSignUpConsentAction.CheckboxWithPrefilledEmail
+ SignUpConsentAction.CheckboxWithPrefilledEmailAndPhone ->
+ ConsumerSignUpConsentAction.CheckboxWithPrefilledEmailAndPhone
+ SignUpConsentAction.Implied ->
+ ConsumerSignUpConsentAction.Implied
+ SignUpConsentAction.ImpliedWithPrefilledEmail ->
+ ConsumerSignUpConsentAction.ImpliedWithPrefilledEmail
+ }
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt
index a8779fe887a..08f78edc4b3 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt
@@ -16,10 +16,10 @@ import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
-import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.paymentsheet.BuildConfig
+import com.stripe.android.repository.ConsumersApiService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
@@ -197,7 +197,7 @@ internal class DefaultLinkAccountManager @Inject constructor(
}
}
- private fun setAccount(
+ internal fun setAccount(
consumerSession: ConsumerSession,
publishableKey: String?,
): LinkAccount {
@@ -207,6 +207,10 @@ internal class DefaultLinkAccountManager @Inject constructor(
return newAccount
}
+ internal fun setAccount(linkAccount: LinkAccount?) {
+ _linkAccount.value = linkAccount
+ }
+
override fun setLinkAccountFromLookupResult(
lookup: ConsumerSessionLookup,
startSession: Boolean,
@@ -275,7 +279,6 @@ internal class DefaultLinkAccountManager @Inject constructor(
)
}
- @VisibleForTesting
internal fun setAccountNullable(
consumerSession: ConsumerSession?,
publishableKey: String?,
@@ -323,18 +326,4 @@ internal class DefaultLinkAccountManager @Inject constructor(
AccountStatus.Error
}
} ?: AccountStatus.SignedOut
-
- private val SignUpConsentAction.consumerAction: ConsumerSignUpConsentAction
- get() = when (this) {
- SignUpConsentAction.Checkbox ->
- ConsumerSignUpConsentAction.Checkbox
- SignUpConsentAction.CheckboxWithPrefilledEmail ->
- ConsumerSignUpConsentAction.CheckboxWithPrefilledEmail
- SignUpConsentAction.CheckboxWithPrefilledEmailAndPhone ->
- ConsumerSignUpConsentAction.CheckboxWithPrefilledEmailAndPhone
- SignUpConsentAction.Implied ->
- ConsumerSignUpConsentAction.Implied
- SignUpConsentAction.ImpliedWithPrefilledEmail ->
- ConsumerSignUpConsentAction.ImpliedWithPrefilledEmail
- }
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAuth.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAuth.kt
new file mode 100644
index 00000000000..93048d95c96
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAuth.kt
@@ -0,0 +1,88 @@
+package com.stripe.android.link.account
+
+import com.stripe.android.core.exception.APIException
+import com.stripe.android.link.LinkConfiguration
+import com.stripe.android.link.model.LinkAccount
+import com.stripe.android.link.ui.inline.SignUpConsentAction
+import com.stripe.attestation.AttestationError
+import javax.inject.Inject
+
+internal class DefaultLinkAuth @Inject constructor(
+ private val linkConfiguration: LinkConfiguration,
+ private val defaultLinkAccountManager: DefaultLinkAccountManager,
+ private val attestLinkAccountManager: AttestLinkAccountManager,
+) : LinkAuth {
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ override suspend fun signUp(
+ email: String,
+ phoneNumber: String,
+ country: String,
+ name: String?,
+ consentAction: SignUpConsentAction
+ ): LinkAuthResult {
+ val consumerSessionSignUpResult = if (linkConfiguration.useAttestationEndpointsForLink) {
+ attestLinkAccountManager.signUp(
+ name = name,
+ email = email,
+ phone = phoneNumber,
+ country = country,
+ consentAction = consentAction,
+ )
+ } else {
+ defaultLinkAccountManager.signUp(
+ name = name,
+ email = email,
+ phone = phoneNumber,
+ country = country,
+ consentAction = consentAction,
+ )
+ }
+ return consumerSessionSignUpResult.toLinkAuthResult()
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ override suspend fun lookUp(email: String): LinkAuthResult {
+ val lookupResult = if (linkConfiguration.useAttestationEndpointsForLink) {
+ attestLinkAccountManager.lookupConsumer(email)
+ } else {
+ defaultLinkAccountManager.lookupConsumer(email)
+ }
+
+ return lookupResult.toLinkAuthResult()
+ }
+
+ private fun Result.toLinkAuthResult(): LinkAuthResult {
+ return try {
+ val linkAccount = getOrThrow()
+ return if (linkAccount != null) {
+ LinkAuthResult.Success(linkAccount)
+ } else {
+ LinkAuthResult.Error(Throwable("No link account found"))
+ }
+ } catch (e: Throwable) {
+ e.toLinkAuthResult()
+ }
+ }
+
+ private fun Throwable.toLinkAuthResult(): LinkAuthResult {
+ return if (isAttestationError) {
+ LinkAuthResult.AttestationFailed(this)
+ } else {
+ LinkAuthResult.Error(this)
+ }
+ }
+
+ private val Throwable.isAttestationError: Boolean
+ get() = when (this) {
+ // Stripe backend could not verify the intregrity of the request
+ is APIException -> stripeError?.code == "link_failed_to_attest_request"
+ // Interaction with Integrity API to generate tokens resulted in a failure
+ is AttestationError -> true
+ else -> false
+ }
+
+ companion object {
+ internal val NO_ELEMENT_SESSION_ID = IllegalStateException("No element session id found.")
+ }
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/LinkAuth.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/LinkAuth.kt
new file mode 100644
index 00000000000..88aa3ceddf7
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/account/LinkAuth.kt
@@ -0,0 +1,17 @@
+package com.stripe.android.link.account
+
+import com.stripe.android.link.ui.inline.SignUpConsentAction
+
+internal interface LinkAuth {
+ suspend fun signUp(
+ email: String,
+ phoneNumber: String,
+ country: String,
+ name: String?,
+ consentAction: SignUpConsentAction
+ ): LinkAuthResult
+
+ suspend fun lookUp(
+ email: String
+ ): LinkAuthResult
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/LinkAuthResult.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/LinkAuthResult.kt
new file mode 100644
index 00000000000..406f357026f
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/account/LinkAuthResult.kt
@@ -0,0 +1,9 @@
+package com.stripe.android.link.account
+
+import com.stripe.android.link.model.LinkAccount
+
+internal sealed interface LinkAuthResult {
+ data class Success(val account: LinkAccount) : LinkAuthResult
+ data class AttestationFailed(val throwable: Throwable) : LinkAuthResult
+ data class Error(val throwable: Throwable) : LinkAuthResult
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt
index fd324e77c50..4747cec6f9f 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt
@@ -5,14 +5,11 @@ import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.ConsumerPaymentDetails
-import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentMethodCreateParams
-import com.stripe.android.model.SetupIntent
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.R
-import com.stripe.android.paymentsheet.state.PaymentElementLoader
import javax.inject.Inject
internal class DefaultLinkConfirmationHandler @Inject constructor(
@@ -74,23 +71,11 @@ internal class DefaultLinkConfirmationHandler @Inject constructor(
shouldSave = false
),
appearance = PaymentSheet.Appearance(),
- initializationMode = initializationMode(),
+ initializationMode = configuration.initializationMode,
shippingDetails = configuration.shippingDetails
)
}
- private fun initializationMode(): PaymentElementLoader.InitializationMode {
- val clientSecret = configuration.stripeIntent.clientSecret ?: throw NO_CLIENT_SECRET_FOUND
- return when (configuration.stripeIntent) {
- is PaymentIntent -> {
- PaymentElementLoader.InitializationMode.PaymentIntent(clientSecret)
- }
- is SetupIntent -> {
- PaymentElementLoader.InitializationMode.SetupIntent(clientSecret)
- }
- }
- }
-
private fun createPaymentMethodCreateParams(
selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails,
linkAccount: LinkAccount,
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt
index 579f59119c5..88cf52e76d6 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt
@@ -1,9 +1,13 @@
package com.stripe.android.link.injection
+import android.app.Application
+import com.stripe.android.core.Logger
import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.account.LinkAccountManager
+import com.stripe.android.link.account.LinkAuth
import com.stripe.android.paymentelement.confirmation.DefaultConfirmationHandler
import com.stripe.android.paymentsheet.analytics.EventReporter
+import com.stripe.attestation.IntegrityRequestManager
import dagger.Module
import dagger.Provides
@@ -15,13 +19,21 @@ internal object LinkViewModelModule {
component: NativeLinkComponent,
defaultConfirmationHandlerFactory: DefaultConfirmationHandler.Factory,
linkAccountManager: LinkAccountManager,
- eventReporter: EventReporter
+ eventReporter: EventReporter,
+ logger: Logger,
+ integrityRequestManager: IntegrityRequestManager,
+ application: Application,
+ linkAuth: LinkAuth
): LinkActivityViewModel {
return LinkActivityViewModel(
activityRetainedComponent = component,
confirmationHandlerFactory = defaultConfirmationHandlerFactory,
linkAccountManager = linkAccountManager,
- eventReporter = eventReporter
+ eventReporter = eventReporter,
+ integrityRequestManager = integrityRequestManager,
+ logger = logger,
+ applicationId = application.packageName,
+ linkAuth = linkAuth,
)
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt
index 56e4eb200d2..3dc5eedccfa 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt
@@ -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
@@ -8,11 +9,16 @@ 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.account.LinkAuth
import com.stripe.android.link.analytics.LinkEventsReporter
import com.stripe.android.link.confirmation.LinkConfirmationHandler
+import com.stripe.android.link.model.LinkAccount
+import com.stripe.android.link.ui.verification.VerificationViewModel
import com.stripe.android.paymentelement.confirmation.injection.DefaultConfirmationModule
import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR
+import com.stripe.attestation.IntegrityRequestManager
import dagger.BindsInstance
import dagger.Component
import javax.inject.Named
@@ -36,7 +42,10 @@ internal interface NativeLinkComponent {
val linkEventsReporter: LinkEventsReporter
val logger: Logger
val linkConfirmationHandlerFactory: LinkConfirmationHandler.Factory
+ val webLinkActivityContract: WebLinkActivityContract
val cardAccountRangeRepositoryFactory: CardAccountRangeRepository.Factory
+ val integrityRequestManager: IntegrityRequestManager
+ val linkAuth: LinkAuth
val viewModel: LinkActivityViewModel
@Component.Builder
@@ -44,6 +53,9 @@ internal interface NativeLinkComponent {
@BindsInstance
fun configuration(configuration: LinkConfiguration): Builder
+ @BindsInstance
+ fun linkAccount(linkAccount: LinkAccount?): Builder
+
@BindsInstance
fun publishableKeyProvider(@Named(PUBLISHABLE_KEY) publishableKeyProvider: () -> String): Builder
@@ -59,6 +71,9 @@ internal interface NativeLinkComponent {
@BindsInstance
fun statusBarColor(@Named(STATUS_BAR_COLOR) statusBarColor: Int?): Builder
+ @BindsInstance
+ fun application(application: Application): Builder
+
fun build(): NativeLinkComponent
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt
index d658eee3dda..1b7850f480d 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt
@@ -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
@@ -20,12 +21,17 @@ import com.stripe.android.core.utils.ContextUtils.packageInfo
import com.stripe.android.core.utils.DefaultDurationProvider
import com.stripe.android.core.utils.DurationProvider
import com.stripe.android.core.version.StripeSdkVersion
+import com.stripe.android.link.LinkConfiguration
+import com.stripe.android.link.account.AttestLinkAccountManager
import com.stripe.android.link.account.DefaultLinkAccountManager
+import com.stripe.android.link.account.DefaultLinkAuth
import com.stripe.android.link.account.LinkAccountManager
+import com.stripe.android.link.account.LinkAuth
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.model.LinkAccount
import com.stripe.android.link.repositories.LinkApiRepository
import com.stripe.android.link.repositories.LinkRepository
import com.stripe.android.networking.StripeApiRepository
@@ -38,6 +44,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
@@ -55,9 +64,9 @@ internal interface NativeLinkModule {
@NativeLinkScope
fun bindLinkEventsReporter(linkEventsReporter: DefaultLinkEventsReporter): LinkEventsReporter
- @Binds
- @NativeLinkScope
- fun bindLinkAccountManager(linkAccountManager: DefaultLinkAccountManager): LinkAccountManager
+// @Binds
+// @NativeLinkScope
+// fun bindLinkAccountManager(linkAccountManager: DefaultLinkAccountManager): LinkAccountManager
@Binds
@NativeLinkScope
@@ -161,8 +170,63 @@ 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(com.stripe.attestation.BuildConfig.DEBUG).error(message, error)
+ },
+ factory = RealStandardIntegrityManagerFactory(context)
+ )
+
@Provides
@NativeLinkScope
fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom
+
+ @Provides
+ @Named(APPLICATION_ID)
+ @NativeLinkScope
+ fun provideApplicationId(
+ application: Application
+ ): String = application.packageName
+
+ @Provides
+ @NativeLinkScope
+ fun provideLinkAuth(
+ configuration: LinkConfiguration,
+ defaultLinkAccountManager: DefaultLinkAccountManager,
+ attestLinkAccountManager: AttestLinkAccountManager,
+ ): LinkAuth {
+ return DefaultLinkAuth(
+ linkConfiguration = configuration,
+ defaultLinkAccountManager = defaultLinkAccountManager,
+ attestLinkAccountManager = attestLinkAccountManager,
+ )
+ }
+
+ @Provides
+ @NativeLinkScope
+ fun provideLinkAccountManager(
+ configuration: LinkConfiguration,
+ linkRepository: LinkRepository,
+ linkEventsReporter: LinkEventsReporter,
+ errorReporter: ErrorReporter,
+ linkAccount: LinkAccount?
+ ): LinkAccountManager {
+ return DefaultLinkAccountManager(
+ config = configuration,
+ linkRepository = linkRepository,
+ linkEventsReporter = linkEventsReporter,
+ errorReporter = errorReporter
+ ).apply {
+ setAccount(linkAccount)
+ }
+ }
}
}
+
+internal const val APPLICATION_ID = "application_id"
+internal const val ATTEST_LINK_ACCOUNT_MANAGER = "attest_link_account_manager"
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt b/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt
index 17f6b22c9a2..8a5eb066284 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt
@@ -1,21 +1,30 @@
package com.stripe.android.link.model
+import android.os.Parcelable
import com.stripe.android.model.ConsumerSession
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
/**
* Immutable object representing a Link account.
*/
-internal class LinkAccount(private val consumerSession: ConsumerSession) {
+@Parcelize
+internal class LinkAccount(private val consumerSession: ConsumerSession): Parcelable {
+ @IgnoredOnParcel
val redactedPhoneNumber = consumerSession.redactedPhoneNumber
+ @IgnoredOnParcel
val clientSecret = consumerSession.clientSecret
+ @IgnoredOnParcel
val email = consumerSession.emailAddress
+ @IgnoredOnParcel
val isVerified: Boolean = consumerSession.containsVerifiedSMSSession() ||
consumerSession.isVerifiedForSignup()
+ @IgnoredOnParcel
val accountStatus = when {
isVerified -> {
AccountStatus.Verified
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt b/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt
index 481c6aea344..8364ab2aeac 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt
@@ -14,6 +14,7 @@ import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.ConsumerSignUpConsentAction
+import com.stripe.android.model.EmailSource
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SignUpParams
import com.stripe.android.model.StripeIntent
@@ -54,6 +55,25 @@ internal class LinkApiRepository @Inject constructor(
}
}
+ override suspend fun mobileLookupConsumer(
+ email: String,
+ verificationToken: String,
+ appId: String,
+ sessionId: String
+ ): Result = withContext(workContext) {
+ runCatching {
+ consumersApiService.mobileLookupConsumerSession(
+ email = email,
+ emailSource = EmailSource.USER_ACTION,
+ requestSurface = REQUEST_SURFACE,
+ verificationToken = verificationToken,
+ appId = appId,
+ requestOptions = buildRequestOptions(),
+ sessionId = sessionId
+ )
+ }
+ }
+
override suspend fun consumerSignUp(
email: String,
phone: String,
@@ -78,6 +98,34 @@ internal class LinkApiRepository @Inject constructor(
)
}
+ override suspend fun mobileSignUp(
+ name: String?,
+ email: String,
+ phoneNumber: String,
+ country: String,
+ consentAction: ConsumerSignUpConsentAction,
+ verificationToken: String,
+ appId: String
+ ): Result = withContext(workContext) {
+ consumersApiService.mobileSignUp(
+ SignUpParams(
+ email = email,
+ phoneNumber = phoneNumber,
+ country = country,
+ name = name,
+ locale = locale,
+ amount = null,
+ currency = null,
+ incentiveEligibilitySession = null,
+ consentAction = consentAction,
+ requestSurface = REQUEST_SURFACE,
+ verificationToken = verificationToken,
+ appId = appId
+ ),
+ requestOptions = buildRequestOptions(),
+ )
+ }
+
override suspend fun createCardPaymentDetails(
paymentMethodCreateParams: PaymentMethodCreateParams,
userEmail: String,
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt b/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt
index c9e01345878..9f49617a481 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt
@@ -13,6 +13,7 @@ import com.stripe.android.model.StripeIntent
/**
* Interface for a repository that interacts with Link services.
*/
+@SuppressWarnings("TooManyFunctions")
internal interface LinkRepository {
/**
@@ -22,6 +23,13 @@ internal interface LinkRepository {
email: String,
): Result
+ suspend fun mobileLookupConsumer(
+ email: String,
+ verificationToken: String,
+ appId: String,
+ sessionId: String
+ ): Result
+
/**
* Sign up for a new Link account.
*/
@@ -33,6 +41,16 @@ internal interface LinkRepository {
consentAction: ConsumerSignUpConsentAction
): Result
+ suspend fun mobileSignUp(
+ name: String?,
+ email: String,
+ phoneNumber: String,
+ country: String,
+ consentAction: ConsumerSignUpConsentAction,
+ verificationToken: String,
+ appId: String
+ ): Result
+
/**
* Create a new card payment method in the consumer account.
*/
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt b/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt
index 46191f6a89c..11bab1ce233 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt
@@ -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
@@ -27,8 +28,13 @@ internal fun DefaultLinkTheme(
colors = colors.materialColors,
typography = Typography,
shapes = MaterialTheme.shapes,
- content = content
- )
+ ) {
+ Surface(
+ color = MaterialTheme.colors.background
+ ) {
+ content()
+ }
+ }
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt
index 6cd2e2a5abb..c5cd9ae4971 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt
@@ -52,7 +52,8 @@ internal fun LinkContent(
sheetState: ModalBottomSheetState,
bottomSheetContent: BottomSheetContent?,
onUpdateSheetContent: (BottomSheetContent?) -> Unit,
- onBackPressed: () -> Unit
+ onBackPressed: () -> Unit,
+ moveToWebFlow: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
DefaultLinkTheme {
@@ -111,7 +112,8 @@ internal fun LinkContent(
showBottomSheetContent = onUpdateSheetContent,
hideBottomSheetContent = {
onUpdateSheetContent(null)
- }
+ },
+ moveToWebFlow = moveToWebFlow
)
}
}
@@ -127,7 +129,8 @@ private fun Screens(
navigateAndClearStack: (route: LinkScreen) -> Unit,
dismissWithResult: (LinkActivityResult) -> Unit,
showBottomSheetContent: (BottomSheetContent?) -> Unit,
- hideBottomSheetContent: () -> Unit
+ hideBottomSheetContent: () -> Unit,
+ moveToWebFlow: () -> Unit
) {
NavHost(
navController = navController,
@@ -139,7 +142,10 @@ private fun Screens(
composable(LinkScreen.SignUp.route) {
val viewModel: SignUpViewModel = linkViewModel {
- SignUpViewModel.factory(it)
+ SignUpViewModel.factory(
+ parentComponent = it,
+ moveToWebFlow = moveToWebFlow
+ )
}
SignUpScreen(
viewModel = viewModel,
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/signup/SignUpViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/signup/SignUpViewModel.kt
index 8b2ea684637..ab1614e7b8f 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/ui/signup/SignUpViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/signup/SignUpViewModel.kt
@@ -11,7 +11,8 @@ import com.stripe.android.core.model.CountryCode
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkScreen
-import com.stripe.android.link.account.LinkAccountManager
+import com.stripe.android.link.account.LinkAuth
+import com.stripe.android.link.account.LinkAuthResult
import com.stripe.android.link.analytics.LinkEventsReporter
import com.stripe.android.link.injection.NativeLinkComponent
import com.stripe.android.link.model.LinkAccount
@@ -37,9 +38,10 @@ import kotlin.time.Duration.Companion.seconds
internal class SignUpViewModel @Inject constructor(
private val configuration: LinkConfiguration,
- private val linkAccountManager: LinkAccountManager,
private val linkEventsReporter: LinkEventsReporter,
- private val logger: Logger
+ private val logger: Logger,
+ private val linkAuth: LinkAuth,
+ private val moveToWebFlow: () -> Unit
) : ViewModel() {
internal var navController: NavHostController? = null
val emailController = EmailConfig.createController(
@@ -112,22 +114,31 @@ internal class SignUpViewModel @Inject constructor(
fun onSignUpClick() {
clearError()
viewModelScope.launch {
- linkAccountManager.signUp(
- email = emailController.fieldValue.value,
- phone = phoneNumberController.getE164PhoneNumber(phoneNumberController.fieldValue.value),
- country = phoneNumberController.getCountryCode(),
- name = nameController.fieldValue.value,
- consentAction = SignUpConsentAction.Implied
- ).fold(
- onSuccess = {
- onAccountFetched(it)
- linkEventsReporter.onSignupCompleted()
- },
- onFailure = {
- onError(it)
- linkEventsReporter.onSignupFailure(error = it)
- }
- )
+ signUp()
+ }
+ }
+
+ private suspend fun signUp() {
+ val result = linkAuth.signUp(
+ email = emailController.fieldValue.value,
+ phoneNumber = phoneNumberController.getE164PhoneNumber(phoneNumberController.fieldValue.value),
+ country = phoneNumberController.getCountryCode(),
+ name = nameController.fieldValue.value,
+ consentAction = SignUpConsentAction.Implied
+ )
+
+ when (result) {
+ is LinkAuthResult.AttestationFailed -> {
+ moveToWebFlow()
+ }
+ is LinkAuthResult.Error -> {
+ onError(result.throwable)
+ linkEventsReporter.onSignupFailure(error = result.throwable)
+ }
+ is LinkAuthResult.Success -> {
+ onAccountFetched(result.account)
+ linkEventsReporter.onSignupCompleted()
+ }
}
}
@@ -177,15 +188,17 @@ internal class SignUpViewModel @Inject constructor(
internal val LOOKUP_DEBOUNCE = 1.seconds
fun factory(
- parentComponent: NativeLinkComponent
+ parentComponent: NativeLinkComponent,
+ moveToWebFlow: () -> Unit
): ViewModelProvider.Factory {
return viewModelFactory {
initializer {
SignUpViewModel(
configuration = parentComponent.configuration,
linkEventsReporter = parentComponent.linkEventsReporter,
- linkAccountManager = parentComponent.linkAccountManager,
- logger = parentComponent.logger
+ logger = parentComponent.logger,
+ linkAuth = parentComponent.linkAuth,
+ moveToWebFlow = moveToWebFlow
)
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationActivity.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationActivity.kt
new file mode 100644
index 00000000000..e46b659552b
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationActivity.kt
@@ -0,0 +1,100 @@
+package com.stripe.android.link.ui.verification
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.VisibleForTesting
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.ui.window.Dialog
+import androidx.core.os.bundleOf
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.NavHostController
+import com.stripe.android.core.Logger
+import com.stripe.android.link.LinkExpressArgs
+import com.stripe.android.link.LinkExpressContract
+import com.stripe.android.link.LinkExpressResult
+import com.stripe.android.link.NativeLinkArgs
+import com.stripe.android.link.NoArgsException
+import com.stripe.android.link.theme.DefaultLinkTheme
+import com.stripe.android.paymentsheet.BuildConfig
+
+class VerificationActivity: ComponentActivity() {
+ internal var viewModel: VerificationViewModel? = null
+
+ @VisibleForTesting
+ internal lateinit var navController: NavHostController
+
+ @OptIn(ExperimentalMaterialApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ try {
+ viewModel = ViewModelProvider(
+ owner = this,
+ factory = VerificationViewModel.factory(
+ goBack = {
+ dismissWithResult(LinkExpressResult.Canceled)
+ },
+ onVerified = { linkAccount ->
+ dismissWithResult(LinkExpressResult.Authenticated(linkAccount))
+ },
+ onError = { error ->
+ dismissWithResult(LinkExpressResult.Failed(error))
+ }
+ )
+ )[VerificationViewModel::class.java]
+ } catch (e: NoArgsException) {
+ Logger.getInstance(BuildConfig.DEBUG).error("Failed to create VerificationViewModel", e)
+ setResult(Activity.RESULT_CANCELED)
+ finish()
+ }
+
+ val vm = viewModel ?: return
+
+ setContent {
+ Dialog(
+ onDismissRequest = {}
+ ) {
+ DefaultLinkTheme {
+ VerificationScreen(vm)
+ }
+ }
+ }
+ }
+
+ private fun dismissWithResult(result: LinkExpressResult) {
+ val bundle = bundleOf(
+ LinkExpressContract.EXTRA_RESULT to result
+ )
+ this@VerificationActivity.setResult(
+ RESULT_COMPLETE,
+ intent.putExtras(bundle)
+ )
+ this@VerificationActivity.finish()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ }
+
+ companion object {
+ internal const val EXTRA_ARGS = "link_express_args"
+ internal const val RESULT_COMPLETE = 57576
+
+ internal fun createIntent(
+ context: Context,
+ args: LinkExpressArgs
+ ): Intent {
+ return Intent(context, VerificationActivity::class.java)
+ .putExtra(EXTRA_ARGS, args)
+ }
+
+ internal fun getArgs(savedStateHandle: SavedStateHandle): LinkExpressArgs? {
+ return savedStateHandle.get(EXTRA_ARGS)
+ }
+ }
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt
index 6108ef39b96..c9fe32e95f0 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt
@@ -1,21 +1,29 @@
package com.stripe.android.link.ui.verification
+import android.app.Application
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.core.Logger
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.resolvableString
+import com.stripe.android.link.LinkExpressArgs
import com.stripe.android.link.LinkScreen
+import com.stripe.android.link.NoArgsException
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.analytics.LinkEventsReporter
+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.ErrorMessage
import com.stripe.android.link.ui.getErrorMessage
+import com.stripe.android.link.ui.verification.VerificationActivity.Companion.getArgs
import com.stripe.android.ui.core.elements.OTPSpec
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -29,18 +37,20 @@ import javax.inject.Inject
* ViewModel that handles user verification confirmation logic.
*/
internal class VerificationViewModel @Inject constructor(
- private val linkAccount: LinkAccount,
+ val linkAccount: LinkAccount?,
private val linkAccountManager: LinkAccountManager,
private val linkEventsReporter: LinkEventsReporter,
private val logger: Logger,
private val goBack: () -> Unit,
private val navigateAndClearStack: (route: LinkScreen) -> Unit,
+ private val onVerified: (LinkAccount) -> Unit = {},
+ private val onError: (Throwable) -> Unit = {}
) : ViewModel() {
private val _viewState = MutableStateFlow(
value = VerificationViewState(
- redactedPhoneNumber = linkAccount.redactedPhoneNumber,
- email = linkAccount.email,
+ redactedPhoneNumber = linkAccount?.redactedPhoneNumber ?: "",
+ email = linkAccount?.email ?: "",
isProcessing = false,
requestFocus = true,
errorMessage = null,
@@ -60,6 +70,7 @@ internal class VerificationViewModel @Inject constructor(
}
private fun setUp() {
+ if (linkAccount == null) return goBack()
if (linkAccount.accountStatus != AccountStatus.VerificationStarted) {
startVerification()
}
@@ -80,11 +91,12 @@ internal class VerificationViewModel @Inject constructor(
}
linkAccountManager.confirmVerification(code).fold(
- onSuccess = {
+ onSuccess = { linkAccount ->
updateViewState {
it.copy(isProcessing = false)
}
navigateAndClearStack(LinkScreen.Wallet)
+ onVerified(linkAccount)
},
onFailure = {
otpElement.controller.reset()
@@ -178,7 +190,7 @@ internal class VerificationViewModel @Inject constructor(
companion object {
fun factory(
parentComponent: NativeLinkComponent,
- linkAccount: LinkAccount,
+ linkAccount: LinkAccount?,
goBack: () -> Unit,
navigateAndClearStack: (route: LinkScreen) -> Unit,
): ViewModelProvider.Factory {
@@ -195,5 +207,42 @@ internal class VerificationViewModel @Inject constructor(
}
}
}
+
+ fun factory(
+ savedStateHandle: SavedStateHandle? = null,
+ goBack: () -> Unit,
+ onVerified: (LinkAccount) -> Unit,
+ onError: (Throwable) -> Unit
+ ): ViewModelProvider.Factory {
+ return viewModelFactory {
+ initializer {
+ val handle: SavedStateHandle = savedStateHandle ?: createSavedStateHandle()
+ val app = this[APPLICATION_KEY] as Application
+ val args: LinkExpressArgs = getArgs(handle) ?: throw NoArgsException()
+
+ val component = DaggerNativeLinkComponent
+ .builder()
+ .configuration(args.configuration)
+ .linkAccount(args.linkAccount)
+ .publishableKeyProvider { args.publishableKey }
+ .stripeAccountIdProvider { args.stripeAccountId }
+ .savedStateHandle(handle)
+ .context(app)
+ .application(app)
+ .build()
+
+ VerificationViewModel(
+ linkAccount = args.linkAccount,
+ linkEventsReporter = component.linkEventsReporter,
+ linkAccountManager = component.linkAccountManager,
+ logger = component.logger,
+ goBack = goBack,
+ navigateAndClearStack = {},
+ onError = onError,
+ onVerified = onVerified
+ )
+ }
+ }
+ }
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt
index 0353aeecba7..1a641a81a14 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt
@@ -8,6 +8,7 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationOptio
import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationOption
import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationOption
import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationOption
+import com.stripe.android.paymentelement.confirmation.linkexpress.LinkExpressConfirmationOption
import com.stripe.android.paymentsheet.model.PaymentSelection
internal fun PaymentSelection.toConfirmationOption(
@@ -70,5 +71,13 @@ internal fun PaymentSelection.toConfirmationOption(
is PaymentSelection.Link -> linkConfiguration?.let {
LinkConfirmationOption(configuration = linkConfiguration)
}
+ is PaymentSelection.LinkExpress -> {
+ linkConfiguration?.let {
+ LinkExpressConfirmationOption(
+ configuration = linkConfiguration,
+ linkAccount = linkAccount
+ )
+ }
+ }
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt
index a4ef1ebff45..e796b39372f 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt
@@ -4,6 +4,7 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationModul
import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationModule
import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationModule
import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationModule
+import com.stripe.android.paymentelement.confirmation.linkexpress.LinkExpressConfirmationModule
import dagger.Module
@Module(
@@ -13,6 +14,7 @@ import dagger.Module
ExternalPaymentMethodConfirmationModule::class,
GooglePayConfirmationModule::class,
LinkConfirmationModule::class,
+ LinkExpressConfirmationModule::class
]
)
internal interface PaymentElementConfirmationModule
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt
index 7e47161ee93..62a14f9310f 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt
@@ -50,7 +50,10 @@ internal class LinkConfirmationDefinition(
confirmationOption: LinkConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters,
) {
- launcher.present(confirmationOption.configuration)
+ launcher.present(
+ configuration = confirmationOption.configuration,
+ linkAccount = confirmationOption.linkAccount
+ )
}
override fun toResult(
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt
index 6f0db93e443..473f1520f94 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt
@@ -1,10 +1,12 @@
package com.stripe.android.paymentelement.confirmation.link
import com.stripe.android.link.LinkConfiguration
+import com.stripe.android.link.model.LinkAccount
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import kotlinx.parcelize.Parcelize
@Parcelize
internal data class LinkConfirmationOption(
val configuration: LinkConfiguration,
+ val linkAccount: LinkAccount? = null
) : ConfirmationHandler.Option
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationDefinition.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationDefinition.kt
new file mode 100644
index 00000000000..38b24f06c20
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationDefinition.kt
@@ -0,0 +1,87 @@
+package com.stripe.android.paymentelement.confirmation.linkexpress
+
+import androidx.activity.result.ActivityResultCaller
+import com.stripe.android.common.exception.stripeErrorMessage
+import com.stripe.android.link.LinkExpressLauncher
+import com.stripe.android.link.LinkExpressResult
+import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
+import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
+import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfirmationType
+import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationOption
+
+internal class LinkExpressConfirmationDefinition(
+ private val linkExpressLauncher: LinkExpressLauncher,
+) : ConfirmationDefinition {
+ override val key: String = "LinkExpress"
+
+ override fun option(confirmationOption: ConfirmationHandler.Option): LinkExpressConfirmationOption? {
+ return confirmationOption as? LinkExpressConfirmationOption
+ }
+
+ override fun createLauncher(
+ activityResultCaller: ActivityResultCaller,
+ onResult: (LinkExpressResult) -> Unit
+ ): LinkExpressLauncher {
+ return linkExpressLauncher.apply {
+ register(activityResultCaller, onResult)
+ }
+ }
+
+ override fun unregister(launcher: LinkExpressLauncher) {
+ launcher.unregister()
+ }
+
+ override suspend fun action(
+ confirmationOption: LinkExpressConfirmationOption,
+ confirmationParameters: ConfirmationDefinition.Parameters,
+ ): ConfirmationDefinition.Action {
+ return ConfirmationDefinition.Action.Launch(
+ launcherArguments = Unit,
+ receivesResultInProcess = false,
+ deferredIntentConfirmationType = null,
+ )
+ }
+
+ override fun launch(
+ launcher: LinkExpressLauncher,
+ arguments: Unit,
+ confirmationOption: LinkExpressConfirmationOption,
+ confirmationParameters: ConfirmationDefinition.Parameters,
+ ) {
+ launcher.present(
+ configuration = confirmationOption.configuration,
+ linkAccount = confirmationOption.linkAccount
+ )
+ }
+
+ override fun toResult(
+ confirmationOption: LinkExpressConfirmationOption,
+ confirmationParameters: ConfirmationDefinition.Parameters,
+ deferredIntentConfirmationType: DeferredIntentConfirmationType?,
+ result: LinkExpressResult
+ ): ConfirmationDefinition.Result {
+ return when (result) {
+ is LinkExpressResult.Authenticated -> {
+ ConfirmationDefinition.Result.NextStep(
+ parameters = confirmationParameters,
+ confirmationOption = LinkConfirmationOption(
+ configuration = confirmationOption.configuration,
+ linkAccount = result.linkAccount
+ )
+ )
+ }
+ LinkExpressResult.Canceled -> {
+ ConfirmationDefinition.Result.Canceled(
+ action = ConfirmationHandler.Result.Canceled.Action.InformCancellation,
+ )
+ }
+ is LinkExpressResult.Failed -> {
+ ConfirmationDefinition.Result.Failed(
+ cause = result.error,
+ message = result.error.stripeErrorMessage(),
+ type = ConfirmationHandler.Result.Failed.ErrorType.Payment,
+ )
+ }
+ }
+ }
+}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationModule.kt
new file mode 100644
index 00000000000..e33bfe2da71
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationModule.kt
@@ -0,0 +1,29 @@
+package com.stripe.android.paymentelement.confirmation.linkexpress
+
+import com.stripe.android.link.LinkExpressLauncher
+import com.stripe.android.link.LinkPaymentLauncher
+import com.stripe.android.link.account.LinkStore
+import com.stripe.android.link.injection.LinkAnalyticsComponent
+import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
+import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationDefinition
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.IntoSet
+
+@Module(
+ subcomponents = [
+ LinkAnalyticsComponent::class,
+ ]
+)
+internal object LinkExpressConfirmationModule {
+ @JvmSuppressWildcards
+ @Provides
+ @IntoSet
+ fun providesLinkConfirmationDefinition(
+ linkExpressLauncher: LinkExpressLauncher,
+ ): ConfirmationDefinition<*, *, *, *> {
+ return LinkExpressConfirmationDefinition(
+ linkExpressLauncher,
+ )
+ }
+}
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationOption.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationOption.kt
new file mode 100644
index 00000000000..0e3603f7f7e
--- /dev/null
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationOption.kt
@@ -0,0 +1,12 @@
+package com.stripe.android.paymentelement.confirmation.linkexpress
+
+import com.stripe.android.link.LinkConfiguration
+import com.stripe.android.link.model.LinkAccount
+import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+internal data class LinkExpressConfirmationOption(
+ val configuration: LinkConfiguration,
+ val linkAccount: LinkAccount
+) : ConfirmationHandler.Option
\ No newline at end of file
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/PaymentOptionDisplayDataFactory.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/PaymentOptionDisplayDataFactory.kt
index be29706a0e3..59f2c56d5b5 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/PaymentOptionDisplayDataFactory.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/PaymentOptionDisplayDataFactory.kt
@@ -43,7 +43,7 @@ internal class PaymentOptionDisplayDataFactory @Inject constructor(
}
is PaymentSelection.ExternalPaymentMethod -> null
PaymentSelection.GooglePay -> null
- PaymentSelection.Link -> null
+ PaymentSelection.Link, is PaymentSelection.LinkExpress -> null
}
return EmbeddedPaymentElement.PaymentOptionDisplayData(
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt
index 72f872768a1..d1e10c08948 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt
@@ -8,6 +8,7 @@ import com.stripe.android.link.account.LinkStore
import com.stripe.android.link.analytics.LinkAnalyticsHelper
import com.stripe.android.link.injection.LinkAnalyticsComponent
import com.stripe.android.link.model.AccountStatus
+import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.PaymentMethod
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -60,12 +62,24 @@ internal class LinkHandler @Inject constructor(
linkAnalyticsComponentBuilder.build().linkAnalyticsHelper
}
- fun setupLink(state: LinkState?) {
+ @OptIn(DelicateCoroutinesApi::class)
+ fun setupLink(state: LinkState?, launchEagerly: (LinkAccount) -> Unit = {}) {
_isLinkEnabled.value = state != null
if (state == null) return
_linkConfiguration.value = state.configuration
+
+ if (state.configuration.useAttestationEndpointsForLink) {
+ GlobalScope.launch {
+ linkConfigurationCoordinator.getAccountFlow(state.configuration)
+ .collectLatest { account ->
+ if (account != null) {
+ launchEagerly(account)
+ }
+ }
+ }
+ }
}
suspend fun payWithLinkInline(
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsStateFactory.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsStateFactory.kt
index 1b3c7b1d0c8..a32da4bfe3a 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsStateFactory.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsStateFactory.kt
@@ -79,7 +79,7 @@ private fun List.findSelectedItem(paymentSelection: PaymentS
return firstOrNull { item ->
when (paymentSelection) {
is PaymentSelection.GooglePay -> item is PaymentOptionsItem.GooglePay
- is PaymentSelection.Link -> item is PaymentOptionsItem.Link
+ is PaymentSelection.Link, is PaymentSelection.LinkExpress-> item is PaymentOptionsItem.Link
is PaymentSelection.Saved -> {
when (item) {
is PaymentOptionsItem.SavedPaymentMethod -> {
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt
index 74a653e6e8d..755831e51df 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt
@@ -226,7 +226,8 @@ internal class PaymentOptionsViewModel @Inject constructor(
when (paymentSelection) {
is PaymentSelection.Saved,
is PaymentSelection.GooglePay,
- is PaymentSelection.Link -> processExistingPaymentMethod(paymentSelection)
+ is PaymentSelection.Link,
+ is PaymentSelection.LinkExpress -> processExistingPaymentMethod(paymentSelection)
is PaymentSelection.New -> processNewOrExternalPaymentMethod(paymentSelection)
is PaymentSelection.ExternalPaymentMethod -> processNewOrExternalPaymentMethod(paymentSelection)
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt
index 9ee91e32af4..8b3fe7d65b5 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt
@@ -20,6 +20,7 @@ import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.utils.requireApplication
import com.stripe.android.googlepaylauncher.GooglePayEnvironment
import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher
+import com.stripe.android.link.model.LinkAccount
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.SetupIntent
@@ -306,7 +307,9 @@ internal class PaymentSheetViewModel @Inject internal constructor(
setPaymentMethodMetadata(state.paymentMethodMetadata)
- linkHandler.setupLink(state.paymentMethodMetadata.linkState)
+ linkHandler.setupLink(state.paymentMethodMetadata.linkState) { account ->
+ checkoutWithLinkExpress(account)
+ }
val pendingFailedPaymentResult = confirmationHandler.awaitResult()
as? ConfirmationHandler.Result.Failed
@@ -376,6 +379,10 @@ internal class PaymentSheetViewModel @Inject internal constructor(
checkout(PaymentSelection.Link, CheckoutIdentifier.SheetTopWallet)
}
+ fun checkoutWithLinkExpress(account: LinkAccount) {
+ checkout(PaymentSelection.LinkExpress(account), CheckoutIdentifier.SheetTopWallet)
+ }
+
private fun checkout(
paymentSelection: PaymentSelection?,
identifier: CheckoutIdentifier,
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt
index 9952626bd52..8d496d0e9f6 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt
@@ -475,6 +475,7 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent {
is PaymentSelection.GooglePay -> "googlepay"
is PaymentSelection.Saved -> "savedpm"
is PaymentSelection.Link,
+ is PaymentSelection.LinkExpress,
is PaymentSelection.New.LinkInline -> "link"
is PaymentSelection.ExternalPaymentMethod,
is PaymentSelection.New -> "newpm"
@@ -536,7 +537,7 @@ private val Duration.asSeconds: Float
internal fun PaymentSelection?.code(): String? {
return when (this) {
is PaymentSelection.GooglePay -> "google_pay"
- is PaymentSelection.Link -> "link"
+ is PaymentSelection.Link, is PaymentSelection.LinkExpress -> "link"
is PaymentSelection.New -> paymentMethodCreateParams.typeCode
is PaymentSelection.Saved -> paymentMethod.type?.code
is PaymentSelection.ExternalPaymentMethod -> type
@@ -553,7 +554,7 @@ private fun PaymentSelection?.paymentMethodInfo(): Map {
internal fun PaymentSelection?.linkContext(): String? {
return when (this) {
- is PaymentSelection.Link -> "wallet"
+ is PaymentSelection.Link, is PaymentSelection.LinkExpress -> "wallet"
is PaymentSelection.New.USBankAccount -> {
instantDebits?.let {
if (it.linkMode == LinkMode.LinkCardBrand) {
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt
index 7838ec5b9f8..417d0186eeb 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt
@@ -272,6 +272,7 @@ internal class DefaultFlowController @Inject internal constructor(
when (val paymentSelection = viewModel.paymentSelection) {
is PaymentSelection.Link,
+ is PaymentSelection.LinkExpress,
is PaymentSelection.New.LinkInline,
is PaymentSelection.GooglePay,
is PaymentSelection.ExternalPaymentMethod,
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/PaymentSelectionUpdater.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/PaymentSelectionUpdater.kt
index 0c0352908f4..4173bea08a0 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/PaymentSelectionUpdater.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/PaymentSelectionUpdater.kt
@@ -56,7 +56,7 @@ internal class DefaultPaymentSelectionUpdater @Inject constructor() : PaymentSel
is PaymentSelection.GooglePay -> {
state.paymentMethodMetadata.isGooglePayReady
}
- is PaymentSelection.Link -> {
+ is PaymentSelection.Link, is PaymentSelection.LinkExpress -> {
state.paymentMethodMetadata.linkState != null
}
is PaymentSelection.ExternalPaymentMethod -> {
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt
index c03bddc6edd..d0b5ad4bc06 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt
@@ -13,6 +13,7 @@ import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.orEmpty
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkPaymentDetails
+import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.Address
import com.stripe.android.model.CardBrand
import com.stripe.android.model.ConfirmPaymentIntentParams
@@ -76,6 +77,20 @@ internal sealed class PaymentSelection : Parcelable {
}
}
+ @Parcelize
+ data class LinkExpress(val linkAccount: LinkAccount) : PaymentSelection() {
+
+ override val requiresConfirmation: Boolean
+ get() = false
+
+ override fun mandateText(
+ merchantName: String,
+ isSetupFlow: Boolean,
+ ): ResolvableString? {
+ return null
+ }
+ }
+
@Parcelize
data class ExternalPaymentMethod(
val type: String,
@@ -306,6 +321,7 @@ internal val PaymentSelection.isLink: Boolean
is PaymentSelection.New -> false
is PaymentSelection.Saved -> walletType == PaymentSelection.Saved.WalletType.Link
is PaymentSelection.ExternalPaymentMethod -> false
+ is PaymentSelection.LinkExpress -> true
}
internal val PaymentSelection.isSaved: Boolean
@@ -324,6 +340,7 @@ internal val PaymentSelection.drawableResourceId: Int
is PaymentSelection.New.LinkInline -> R.drawable.stripe_ic_paymentsheet_link
is PaymentSelection.New.USBankAccount -> iconResource
is PaymentSelection.Saved -> getSavedIcon(this)
+ is PaymentSelection.LinkExpress -> R.drawable.stripe_ic_paymentsheet_link
}
private fun getSavedIcon(selection: PaymentSelection.Saved): Int {
@@ -349,6 +366,7 @@ internal val PaymentSelection.lightThemeIconUrl: String?
is PaymentSelection.New.LinkInline -> null
is PaymentSelection.New.USBankAccount -> null
is PaymentSelection.Saved -> null
+ is PaymentSelection.LinkExpress -> null
}
internal val PaymentSelection.darkThemeIconUrl: String?
@@ -361,6 +379,7 @@ internal val PaymentSelection.darkThemeIconUrl: String?
is PaymentSelection.New.LinkInline -> null
is PaymentSelection.New.USBankAccount -> null
is PaymentSelection.Saved -> null
+ is PaymentSelection.LinkExpress -> null
}
internal val PaymentSelection.label: ResolvableString
@@ -373,6 +392,7 @@ internal val PaymentSelection.label: ResolvableString
is PaymentSelection.New.LinkInline -> label.resolvableString
is PaymentSelection.New.USBankAccount -> label.resolvableString
is PaymentSelection.Saved -> getSavedLabel(this).orEmpty()
+ is PaymentSelection.LinkExpress -> StripeR.string.stripe_link.resolvableString
}
private fun getSavedLabel(selection: PaymentSelection.Saved): ResolvableString? {
@@ -389,7 +409,7 @@ internal val PaymentSelection.paymentMethodType: String
get() = when (this) {
is PaymentSelection.ExternalPaymentMethod -> type
PaymentSelection.GooglePay -> "google_pay"
- PaymentSelection.Link -> "link"
+ PaymentSelection.Link, is PaymentSelection.LinkExpress -> "link"
is PaymentSelection.New.Card -> paymentMethodCreateParams.typeCode
is PaymentSelection.New.GenericPaymentMethod -> paymentMethodCreateParams.typeCode
is PaymentSelection.New.LinkInline -> linkPaymentDetails.paymentMethodCreateParams.typeCode
@@ -401,7 +421,7 @@ internal val PaymentSelection.billingDetails: PaymentMethod.BillingDetails?
get() = when (this) {
is PaymentSelection.ExternalPaymentMethod -> billingDetails
PaymentSelection.GooglePay -> null
- PaymentSelection.Link -> null
+ PaymentSelection.Link, is PaymentSelection.LinkExpress -> null
is PaymentSelection.New -> paymentMethodCreateParams.billingDetails
is PaymentSelection.Saved -> paymentMethod.billingDetails
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt
index c9dcf8f2dd5..f9f7c810867 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt
@@ -170,6 +170,7 @@ internal class DefaultPaymentElementLoader @Inject constructor(
configuration = configuration,
elementsSession = elementsSession,
customer = customerInfo,
+ initializationMode = initializationMode
)
}
@@ -384,6 +385,7 @@ internal class DefaultPaymentElementLoader @Inject constructor(
elementsSession: ElementsSession,
configuration: CommonConfiguration,
customer: CustomerInfo?,
+ initializationMode: PaymentElementLoader.InitializationMode
): LinkState? {
return if (elementsSession.isLinkEnabled &&
!configuration.billingDetailsCollectionConfiguration.collectsAnything
@@ -396,6 +398,8 @@ internal class DefaultPaymentElementLoader @Inject constructor(
passthroughModeEnabled = elementsSession.linkPassthroughModeEnabled,
linkSignUpDisabled = elementsSession.disableLinkSignup,
flags = elementsSession.linkFlags,
+ useAttestationEndpointsForLink = elementsSession.useAttestationEndpointsForLink,
+ initializationMode = initializationMode
)
} else {
null
@@ -410,6 +414,8 @@ internal class DefaultPaymentElementLoader @Inject constructor(
passthroughModeEnabled: Boolean,
linkSignUpDisabled: Boolean,
flags: Map,
+ useAttestationEndpointsForLink: Boolean,
+ initializationMode: PaymentElementLoader.InitializationMode
): LinkState {
val linkConfig = createLinkConfiguration(
configuration = configuration,
@@ -418,6 +424,8 @@ internal class DefaultPaymentElementLoader @Inject constructor(
merchantCountry = merchantCountry,
passthroughModeEnabled = passthroughModeEnabled,
flags = flags,
+ useAttestationEndpointsForLink = useAttestationEndpointsForLink,
+ initializationMode = initializationMode
)
val accountStatus = accountStatusProvider(linkConfig)
@@ -467,6 +475,8 @@ internal class DefaultPaymentElementLoader @Inject constructor(
merchantCountry: String?,
passthroughModeEnabled: Boolean,
flags: Map,
+ useAttestationEndpointsForLink: Boolean,
+ initializationMode: PaymentElementLoader.InitializationMode
): LinkConfiguration {
val shippingDetails: AddressDetails? = configuration.shippingDetails
@@ -511,6 +521,9 @@ internal class DefaultPaymentElementLoader @Inject constructor(
passthroughModeEnabled = passthroughModeEnabled,
cardBrandChoice = cardBrandChoice,
flags = flags,
+ useAttestationEndpointsForLink = useAttestationEndpointsForLink,
+ elementSessionId = elementsSession.sessionId,
+ initializationMode = initializationMode
)
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt
index 9dfd22e359f..b3e2ca4ce7c 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt
@@ -170,7 +170,10 @@ internal class DefaultSelectSavedPaymentMethodsInteractor(
paymentOptionsItems: List,
): PaymentOptionsItem? {
val paymentSelection = when (selection) {
- is PaymentSelection.Saved, PaymentSelection.Link, PaymentSelection.GooglePay -> selection
+ is PaymentSelection.Saved,
+ PaymentSelection.Link,
+ is PaymentSelection.LinkExpress,
+ PaymentSelection.GooglePay -> selection
is PaymentSelection.New, is PaymentSelection.ExternalPaymentMethod, null -> savedSelection?.let {
PaymentSelection.Saved(it)
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/ManageScreenInteractor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/ManageScreenInteractor.kt
index 3994fd18022..616b4d110f1 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/ManageScreenInteractor.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/ManageScreenInteractor.kt
@@ -177,6 +177,7 @@ internal class DefaultManageScreenInteractor(
is PaymentSelection.ExternalPaymentMethod,
PaymentSelection.GooglePay,
PaymentSelection.Link,
+ is PaymentSelection.LinkExpress,
is PaymentSelection.New -> return null
is PaymentSelection.Saved -> selection.paymentMethod.id
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt
index c26b8a80869..7608efd5077 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt
@@ -1,114 +1,105 @@
package com.stripe.android.link
-import androidx.core.os.BundleCompat
+import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
-import com.stripe.android.PaymentConfiguration
import com.stripe.android.core.utils.FeatureFlags
-import com.stripe.android.model.PaymentIntentFixtures
-import com.stripe.android.networking.StripeRepository
+import com.stripe.android.link.LinkActivityContract.Companion.EXTRA_USED_NATIVE_CONTRACT
import com.stripe.android.testing.FeatureFlagTestRule
-import org.junit.After
-import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
-import org.mockito.kotlin.any
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class LinkActivityContractTest {
+ private val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION)
+
@get:Rule
val featureFlagTestRule = FeatureFlagTestRule(
featureFlag = FeatureFlags.nativeLinkEnabled,
isEnabled = false
)
- @Before
- fun before() {
- PaymentConfiguration.init(
- context = ApplicationProvider.getApplicationContext(),
- publishableKey = "pk_test_abcdefg",
- )
- }
+ @Test
+ fun `LinkActivityContract creates intent with URL with native link disabled`() {
+ featureFlagTestRule.setEnabled(false)
+
+ val expectedIntent = Intent()
+ val webLinkActivityContract = mock()
+ whenever(webLinkActivityContract.createIntent(ApplicationProvider.getApplicationContext(), args))
+ .thenReturn(expectedIntent)
+
+ val contract = linkActivityContract(webLinkActivityContract = webLinkActivityContract)
- @After
- fun after() {
- PaymentConfiguration.clearInstance()
+ val actualIntent = contract.createIntent(ApplicationProvider.getApplicationContext(), args)
+
+ assertThat(expectedIntent).isEqualTo(actualIntent)
+ assertThat(actualIntent.extras?.getBoolean(EXTRA_USED_NATIVE_CONTRACT)).isFalse()
}
@Test
- fun `LinkActivityContract creates intent with URL with native link disabled`() {
+ fun `LinkActivityContract parses result with webLinkActivityContract`() {
featureFlagTestRule.setEnabled(false)
- val config = LinkConfiguration(
- stripeIntent = PaymentIntentFixtures.PI_SUCCEEDED,
- merchantName = "Merchant, Inc",
- merchantCountryCode = "US",
- customerInfo = LinkConfiguration.CustomerInfo(
- name = "Name",
- email = "customer@email.com",
- phone = "1234567890",
- billingCountryCode = "US",
- ),
- shippingDetails = null,
- passthroughModeEnabled = false,
- flags = emptyMap(),
- cardBrandChoice = null,
- )
- val args = LinkActivityContract.Args(
- config,
- )
- val stripeRepository = mock()
- whenever(stripeRepository.buildPaymentUserAgent(any())).thenReturn("test")
- val contract = LinkActivityContract(stripeRepository)
+ val expectedIntent = Intent()
+ val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION)
+ val webLinkActivityContract = mock()
+ whenever(webLinkActivityContract.createIntent(ApplicationProvider.getApplicationContext(), args))
+ .thenReturn(expectedIntent)
+
+ val contract = linkActivityContract(webLinkActivityContract = webLinkActivityContract)
+
val intent = contract.createIntent(ApplicationProvider.getApplicationContext(), args)
- assertThat(intent.component?.className).isEqualTo(LinkForegroundActivity::class.java.name)
- assertThat(intent.extras?.getString(LinkForegroundActivity.EXTRA_POPUP_URL)).startsWith(
- "https://checkout.link.com/#"
- )
+ contract.parseResult(0, intent)
+
+ verify(webLinkActivityContract).parseResult(0, intent)
}
@Test
fun `LinkActivityContract creates intent with with NativeLinkArgs when native link is enabled`() {
featureFlagTestRule.setEnabled(true)
- val config = LinkConfiguration(
- stripeIntent = PaymentIntentFixtures.PI_SUCCEEDED,
- merchantName = "Merchant, Inc",
- merchantCountryCode = "US",
- customerInfo = LinkConfiguration.CustomerInfo(
- name = "Name",
- email = "customer@email.com",
- phone = "1234567890",
- billingCountryCode = "US",
- ),
- shippingDetails = null,
- passthroughModeEnabled = false,
- flags = emptyMap(),
- cardBrandChoice = null,
- )
- val args = LinkActivityContract.Args(
- config,
- )
- val stripeRepository = mock()
- whenever(stripeRepository.buildPaymentUserAgent(any())).thenReturn("test")
- val contract = LinkActivityContract(stripeRepository)
+ val expectedIntent = Intent()
+ val nativeLinkActivityContract = mock()
+ whenever(nativeLinkActivityContract.createIntent(ApplicationProvider.getApplicationContext(), args))
+ .thenReturn(expectedIntent)
+
+ val contract = linkActivityContract(nativeLinkActivityContract = nativeLinkActivityContract)
+
+ val actualIntent = contract.createIntent(ApplicationProvider.getApplicationContext(), args)
+
+ assertThat(expectedIntent).isEqualTo(actualIntent)
+ assertThat(actualIntent.extras?.getBoolean(EXTRA_USED_NATIVE_CONTRACT)).isTrue()
+ }
+
+ @Test
+ fun `LinkActivityContract parses result with nativeLinkActivityContract`() {
+ featureFlagTestRule.setEnabled(true)
+
+ val nativeLinkActivityContract = mock()
+ whenever(nativeLinkActivityContract.createIntent(ApplicationProvider.getApplicationContext(), args))
+ .thenReturn(Intent())
+
+ val contract = linkActivityContract(nativeLinkActivityContract = nativeLinkActivityContract)
+
val intent = contract.createIntent(ApplicationProvider.getApplicationContext(), args)
- assertThat(intent.component?.className).isEqualTo(LinkActivity::class.java.name)
-
- val actualArg = intent.extras?.let {
- BundleCompat.getParcelable(it, LinkActivity.EXTRA_ARGS, NativeLinkArgs::class.java)
- }
- assertThat(actualArg).isEqualTo(
- NativeLinkArgs(
- configuration = config,
- publishableKey = "pk_test_abcdefg",
- stripeAccountId = null
- )
+ contract.parseResult(0, intent)
+
+ verify(nativeLinkActivityContract).parseResult(0, intent)
+ }
+
+ private fun linkActivityContract(
+ webLinkActivityContract: WebLinkActivityContract = mock(),
+ nativeLinkActivityContract: NativeLinkActivityContract = mock()
+ ): LinkActivityContract {
+ return LinkActivityContract(
+ nativeLinkActivityContract = nativeLinkActivityContract,
+ webLinkActivityContract = webLinkActivityContract
)
}
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt
index 0cbf72e2b78..7288e11cb4c 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt
@@ -348,7 +348,8 @@ internal class LinkActivityViewModelTest {
linkAccountManager = linkAccountManager,
activityRetainedComponent = mock(),
eventReporter = eventReporter,
- confirmationHandlerFactory = { confirmationHandler }
+ confirmationHandlerFactory = { confirmationHandler },
+ integrityRequestManager = mock()
).apply {
this.navController = navController
this.dismissWithResult = dismissWithResult
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkConfigurationCoordinatorTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkConfigurationCoordinatorTest.kt
index 69fe4f8cfcf..e011ba2940a 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/LinkConfigurationCoordinatorTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkConfigurationCoordinatorTest.kt
@@ -31,6 +31,7 @@ class LinkConfigurationCoordinatorTest {
passthroughModeEnabled = false,
flags = emptyMap(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
)
private val linkComponentBuilder: LinkComponent.Builder = mock()
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt
new file mode 100644
index 00000000000..e43c3fb3d03
--- /dev/null
+++ b/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt
@@ -0,0 +1,108 @@
+package com.stripe.android.link
+
+import android.app.Activity
+import android.content.Intent
+import androidx.core.os.BundleCompat
+import androidx.core.os.bundleOf
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import com.stripe.android.PaymentConfiguration
+import com.stripe.android.model.PaymentMethodFixtures
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class NativeLinkActivityContractTest {
+
+ @Before
+ fun before() {
+ PaymentConfiguration.init(
+ context = ApplicationProvider.getApplicationContext(),
+ publishableKey = "pk_test_abcdefg",
+ )
+ }
+
+ @After
+ fun after() {
+ PaymentConfiguration.clearInstance()
+ }
+
+ @Test
+ fun `intent is created correctly`() {
+ val contract = NativeLinkActivityContract()
+ val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION)
+
+ val intent = contract.createIntent(ApplicationProvider.getApplicationContext(), args)
+
+ assertThat(intent.component?.className).isEqualTo(LinkActivity::class.java.name)
+
+ val actualArg = intent.extras?.let {
+ BundleCompat.getParcelable(it, LinkActivity.EXTRA_ARGS, NativeLinkArgs::class.java)
+ }
+ assertThat(actualArg).isEqualTo(
+ NativeLinkArgs(
+ configuration = TestFactory.LINK_CONFIGURATION,
+ publishableKey = "pk_test_abcdefg",
+ stripeAccountId = null
+ )
+ )
+ }
+
+ @Test
+ fun `complete with result from native link`() {
+ val expectedResult = LinkActivityResult.PaymentMethodObtained(
+ paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD
+ )
+
+ val contract = NativeLinkActivityContract()
+
+ val result = contract.parseResult(
+ resultCode = LinkActivity.RESULT_COMPLETE,
+ intent = intent(expectedResult)
+ )
+
+ assertThat(result).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun `complete with canceled result when result not found`() {
+ val contract = NativeLinkActivityContract()
+
+ val result = contract.parseResult(
+ resultCode = LinkActivity.RESULT_COMPLETE,
+ intent = Intent()
+ )
+
+ assertThat(result).isEqualTo(LinkActivityResult.Canceled())
+ }
+
+ @Test
+ fun `unknown result code results in canceled`() {
+ val contract = NativeLinkActivityContract()
+
+ val result = contract.parseResult(42, Intent())
+
+ assertThat(result).isEqualTo(LinkActivityResult.Canceled())
+ }
+
+ @Test
+ fun `canceled result code is handled correctly`() {
+ val contract = NativeLinkActivityContract()
+
+ val result = contract.parseResult(Activity.RESULT_CANCELED, Intent())
+
+ assertThat(result).isEqualTo(LinkActivityResult.Canceled())
+ }
+
+ private fun intent(result: LinkActivityResult): Intent {
+ val bundle = bundleOf(
+ LinkActivityContract.EXTRA_RESULT to result
+ )
+ return Intent().apply {
+ putExtras(bundle)
+ }
+ }
+}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt b/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt
index c99d2678412..38b8a10c14b 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt
@@ -132,7 +132,8 @@ internal object TestFactory {
shippingDetails = null,
flags = emptyMap(),
cardBrandChoice = null,
- passthroughModeEnabled = false
+ passthroughModeEnabled = false,
+ useAttestationEndpointsForLink = false
)
val LINK_WALLET_PRIMARY_BUTTON_LABEL = Amount(
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt
similarity index 62%
rename from paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt
rename to paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt
index db342cd5852..42efb48f09c 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt
@@ -3,23 +3,61 @@ package com.stripe.android.link
import android.app.Activity
import android.content.Intent
import androidx.core.net.toUri
-import androidx.core.os.bundleOf
+import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
-import com.stripe.android.model.PaymentMethodFixtures
+import com.stripe.android.PaymentConfiguration
+import com.stripe.android.customersheet.FakeStripeRepository
+import com.stripe.android.networking.StripeRepository
+import com.stripe.android.testing.AbsFakeStripeRepository
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
-class LinkActivityResultTest {
+class WebLinkActivityContractTest {
+
+ @Before
+ fun before() {
+ PaymentConfiguration.init(
+ context = ApplicationProvider.getApplicationContext(),
+ publishableKey = "pk_test_abcdefg",
+ )
+ }
+
+ @After
+ fun after() {
+ PaymentConfiguration.clearInstance()
+ }
+
+ @Test
+ fun `intent is created correctly`() {
+ val stripeRepository = object : AbsFakeStripeRepository() {
+ override fun buildPaymentUserAgent(attribution: Set) = "test"
+ }
+ val contract = WebLinkActivityContract(stripeRepository)
+ val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION)
+
+ val intent = contract.createIntent(ApplicationProvider.getApplicationContext(), args)
+
+ assertThat(intent.component?.className).isEqualTo(LinkForegroundActivity::class.java.name)
+ assertThat(intent.extras?.getString(LinkForegroundActivity.EXTRA_POPUP_URL)).startsWith(
+ "https://checkout.link.com/#"
+ )
+ }
@Test
- fun `complete with payment method`() {
+ fun `parse paymentMethodObtained result correctly`() {
val redirectUrl =
"link-popup://complete?link_status=complete&pm=eyJpZCI6InBtXzFOSmVFckx1NW8zUDE4WnBtWHBDdElyUiIsIm9iamVjdCI6InBheW1lbnRfbWV0aG9kIiwiYmlsbGluZ19kZXRhaWxzIjp7ImFkZHJlc3MiOnsiY2l0eSI6bnVsbCwiY291bnRyeSI6bnVsbCwibGluZTEiOm51bGwsImxpbmUyIjpudWxsLCJwb3N0YWxfY29kZSI6bnVsbCwic3RhdGUiOm51bGx9LCJlbWFpbCI6bnVsbCwibmFtZSI6bnVsbCwicGhvbmUiOm51bGx9LCJjYXJkIjp7ImJyYW5kIjoidmlzYSIsImNoZWNrcyI6eyJhZGRyZXNzX2xpbmUxX2NoZWNrIjpudWxsLCJhZGRyZXNzX3Bvc3RhbF9jb2RlX2NoZWNrIjpudWxsLCJjdmNfY2hlY2siOm51bGx9LCJjb3VudHJ5IjpudWxsLCJleHBfbW9udGgiOjEyLCJleHBfeWVhciI6MjAzNCwiZnVuZGluZyI6ImNyZWRpdCIsImdlbmVyYXRlZF9mcm9tIjpudWxsLCJsYXN0NCI6IjAwMDAiLCJuZXR3b3JrcyI6eyJhdmFpbGFibGUiOlsidmlzYSJdLCJwcmVmZXJyZWQiOm51bGx9LCJ0aHJlZV9kX3NlY3VyZV91c2FnZSI6eyJzdXBwb3J0ZWQiOnRydWV9LCJ3YWxsZXQiOnsiZHluYW1pY19sYXN0NCI6bnVsbCwibGluayI6e30sInR5cGUiOiJsaW5rIn19LCJjcmVhdGVkIjoxNjg2OTI4MDIxLCJjdXN0b21lciI6bnVsbCwibGl2ZW1vZGUiOmZhbHNlLCJ0eXBlIjoiY2FyZCJ9ICAg"
val intent = Intent()
intent.data = redirectUrl.toUri()
- val result = createLinkActivityResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
+ val contract = contract()
+
+ val result = contract.parseResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
assertThat(result).isInstanceOf(LinkActivityResult.PaymentMethodObtained::class.java)
val paymentMethodObtained = result as LinkActivityResult.PaymentMethodObtained
assertThat(paymentMethodObtained.paymentMethod.type?.code).isEqualTo("card")
@@ -28,22 +66,30 @@ class LinkActivityResultTest {
}
@Test
- fun `complete with logout`() {
+ fun `parse logout result correctly`() {
val redirectUrl = "link-popup://complete?link_status=logout"
val intent = Intent()
intent.data = redirectUrl.toUri()
- val result = createLinkActivityResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
+ val contract = contract()
+
+ val result = contract.parseResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
assertThat(result).isInstanceOf(LinkActivityResult.Canceled::class.java)
val canceled = result as LinkActivityResult.Canceled
assertThat(canceled.reason).isEqualTo(LinkActivityResult.Canceled.Reason.LoggedOut)
}
@Test
- fun `complete with unknown link_status results in canceled`() {
+ fun `parse unknown status result correctly`() {
val redirectUrl = "link-popup://complete?link_status=shrug"
val intent = Intent()
intent.data = redirectUrl.toUri()
- val result = createLinkActivityResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
+ val contract = contract()
+
+ val result = contract.parseResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
assertThat(result).isInstanceOf(LinkActivityResult.Canceled::class.java)
val canceled = result as LinkActivityResult.Canceled
assertThat(canceled.reason).isEqualTo(LinkActivityResult.Canceled.Reason.BackPressed)
@@ -51,7 +97,10 @@ class LinkActivityResultTest {
@Test
fun `canceled result code`() {
- val result = createLinkActivityResult(Activity.RESULT_CANCELED, null)
+ val contract = contract()
+
+ val result = contract.parseResult(Activity.RESULT_CANCELED, null)
+
assertThat(result).isInstanceOf(LinkActivityResult.Canceled::class.java)
val canceled = result as LinkActivityResult.Canceled
assertThat(canceled.reason).isEqualTo(LinkActivityResult.Canceled.Reason.BackPressed)
@@ -61,7 +110,11 @@ class LinkActivityResultTest {
fun `failure with data`() {
val intent = Intent()
intent.putExtra(LinkForegroundActivity.EXTRA_FAILURE, IllegalStateException("Foobar!"))
- val result = createLinkActivityResult(LinkForegroundActivity.RESULT_FAILURE, intent)
+
+ val contract = contract()
+
+ val result = contract.parseResult(LinkForegroundActivity.RESULT_FAILURE, intent)
+
assertThat(result).isInstanceOf(LinkActivityResult.Failed::class.java)
val failed = result as LinkActivityResult.Failed
assertThat(failed.error).hasMessageThat().isEqualTo("Foobar!")
@@ -69,7 +122,10 @@ class LinkActivityResultTest {
@Test
fun `failure without data results in canceled`() {
- val result = createLinkActivityResult(LinkForegroundActivity.RESULT_FAILURE, null)
+ val contract = contract()
+
+ val result = contract.parseResult(LinkForegroundActivity.RESULT_FAILURE, null)
+
assertThat(result).isInstanceOf(LinkActivityResult.Canceled::class.java)
val canceled = result as LinkActivityResult.Canceled
assertThat(canceled.reason).isEqualTo(LinkActivityResult.Canceled.Reason.BackPressed)
@@ -77,7 +133,10 @@ class LinkActivityResultTest {
@Test
fun `unknown result code results in canceled`() {
- val result = createLinkActivityResult(42, null)
+ val contract = contract()
+
+ val result = contract.parseResult(42, null)
+
assertThat(result).isInstanceOf(LinkActivityResult.Canceled::class.java)
val canceled = result as LinkActivityResult.Canceled
assertThat(canceled.reason).isEqualTo(LinkActivityResult.Canceled.Reason.BackPressed)
@@ -89,30 +148,19 @@ class LinkActivityResultTest {
"link-popup://complete?link_status=complete&pm=🤷"
val intent = Intent()
intent.data = redirectUrl.toUri()
- val result = createLinkActivityResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
+ val contract = contract()
+
+ val result = contract.parseResult(LinkForegroundActivity.RESULT_COMPLETE, intent)
+
assertThat(result).isInstanceOf(LinkActivityResult.Canceled::class.java)
val canceled = result as LinkActivityResult.Canceled
assertThat(canceled.reason).isEqualTo(LinkActivityResult.Canceled.Reason.BackPressed)
}
- @Test
- fun `complete with result from native link`() {
- val bundle = bundleOf(
- LinkActivityContract.EXTRA_RESULT to LinkActivityResult.PaymentMethodObtained(
- paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD
- )
- )
- val intent = Intent()
- intent.putExtras(bundle)
- val result = createLinkActivityResult(LinkActivity.RESULT_COMPLETE, intent)
- assertThat(result)
- .isEqualTo(LinkActivityResult.PaymentMethodObtained(PaymentMethodFixtures.CARD_PAYMENT_METHOD))
- }
-
- @Test
- fun `complete with canceled result when native link result not found`() {
- val intent = Intent()
- val result = createLinkActivityResult(LinkActivity.RESULT_COMPLETE, intent)
- assertThat(result).isEqualTo(LinkActivityResult.Canceled())
+ private fun contract(
+ stripeRepository: StripeRepository = FakeStripeRepository()
+ ): WebLinkActivityContract {
+ return WebLinkActivityContract(stripeRepository)
}
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/account/DefaultLinkAccountManagerTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/account/DefaultLinkAccountManagerTest.kt
index eba546aabb9..5a62100a7d1 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/account/DefaultLinkAccountManagerTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/account/DefaultLinkAccountManagerTest.kt
@@ -832,6 +832,7 @@ class DefaultLinkAccountManagerTest {
passthroughModeEnabled = passthroughModeEnabled,
flags = emptyMap(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
),
linkRepository,
linkEventsReporter,
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/serialization/PopupPayloadTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/serialization/PopupPayloadTest.kt
index 9cebfe3e197..03c1ab9a6bd 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/serialization/PopupPayloadTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/serialization/PopupPayloadTest.kt
@@ -223,7 +223,8 @@ internal class PopupPayloadTest {
passthroughModeEnabled = true,
shippingDetails = null,
cardBrandChoice = cardBrandChoice,
- stripeIntent = intent
+ stripeIntent = intent,
+ useAttestationEndpointsForLink = true
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewModelTest.kt
index dda6d853171..79263a926a7 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewModelTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewModelTest.kt
@@ -60,6 +60,7 @@ class InlineSignupViewModelTest {
passthroughModeEnabled = false,
flags = emptyMap(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
),
signupMode = LinkSignupMode.InsteadOfSaveForFutureUse,
linkAccountManager = linkAccountManager,
@@ -423,6 +424,7 @@ class InlineSignupViewModelTest {
passthroughModeEnabled = false,
flags = emptyMap(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
),
signupMode = signupMode,
linkAccountManager = linkAccountManager,
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewStateTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewStateTest.kt
index d2041b88cf5..ed324d0a64c 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewStateTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/inline/InlineSignupViewStateTest.kt
@@ -95,6 +95,7 @@ class InlineSignupViewStateTest {
passthroughModeEnabled = false,
flags = emptyMap(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
)
}
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/signup/SignUpScreenTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/signup/SignUpScreenTest.kt
index 2c13512db8f..01ae553017f 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/ui/signup/SignUpScreenTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/signup/SignUpScreenTest.kt
@@ -42,6 +42,7 @@ internal class SignUpScreenTest {
passthroughModeEnabled = false,
cardBrandChoice = null,
shippingDetails = null,
+ useAttestationEndpointsForLink = true,
)
private val linkAccountManager = FakeLinkAccountManager()
private val linkEventsReporter = object : FakeLinkEventsReporter() {
diff --git a/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt b/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt
index eaea85726b7..29a1063e36b 100644
--- a/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt
@@ -1314,6 +1314,7 @@ internal class PaymentMethodMetadataTest {
preferredNetworks = listOf("cartes_bancaires")
),
passthroughModeEnabled = false,
+ useAttestationEndpointsForLink = true,
),
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/definitions/CardDefinitionTest.kt b/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/definitions/CardDefinitionTest.kt
index 7eedf7e7591..b6c6bd9cffe 100644
--- a/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/definitions/CardDefinitionTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/definitions/CardDefinitionTest.kt
@@ -171,7 +171,8 @@ class CardDefinitionTest {
flags = mapOf(),
passthroughModeEnabled = false,
cardBrandChoice = null,
- shippingDetails = null
+ shippingDetails = null,
+ useAttestationEndpointsForLink = true,
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt
index 8315bedee53..3390393f038 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt
@@ -363,7 +363,8 @@ class ConfirmationHandlerOptionKtxTest {
shippingDetails = null,
passthroughModeEnabled = false,
cardBrandChoice = null,
- flags = mapOf()
+ flags = mapOf(),
+ useAttestationEndpointsForLink = true,
)
}
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt
index f0fcf506935..7f064b87a9c 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt
@@ -226,6 +226,7 @@ internal class LinkConfirmationActivityTest(private val nativeLinkEnabled: Boole
passthroughModeEnabled = false,
flags = mapOf(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
),
)
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt
index cdf789e2367..89d14de638b 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt
@@ -282,6 +282,7 @@ internal class LinkConfirmationDefinitionTest {
passthroughModeEnabled = false,
flags = mapOf(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
),
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt
index 3688b4cadd8..cfc959bfac9 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt
@@ -160,6 +160,7 @@ class LinkConfirmationFlowTest {
passthroughModeEnabled = false,
flags = mapOf(),
cardBrandChoice = null,
+ useAttestationEndpointsForLink = true,
),
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt
index 8b4b60c1cf6..f8b1eb726af 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt
@@ -566,6 +566,7 @@ private fun defaultLinkConfiguration(
passthroughModeEnabled = false,
cardBrandChoice = null,
flags = emptyMap(),
+ useAttestationEndpointsForLink = true,
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt
index 1bc120c9c4e..0205f9ed256 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt
@@ -950,6 +950,7 @@ internal class PaymentSheetViewModelTest {
passthroughModeEnabled = false,
cardBrandChoice = null,
shippingDetails = null,
+ useAttestationEndpointsForLink = true,
)
val viewModel = createViewModel(
@@ -3344,7 +3345,8 @@ internal class PaymentSheetViewModelTest {
shippingDetails = null,
passthroughModeEnabled = false,
cardBrandChoice = null,
- flags = emptyMap()
+ flags = emptyMap(),
+ useAttestationEndpointsForLink = true,
)
}
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt
index 6d85a0a7c04..0d1649e2e17 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt
@@ -658,6 +658,7 @@ internal class DefaultPaymentElementLoaderTest {
passthroughModeEnabled = false,
cardBrandChoice = null,
flags = emptyMap(),
+ useAttestationEndpointsForLink = true,
)
assertThat(result.paymentMethodMetadata.linkState?.configuration).isEqualTo(expectedLinkConfig)
@@ -695,6 +696,7 @@ internal class DefaultPaymentElementLoaderTest {
linkFlags = emptyMap(),
disableLinkSignup = false,
linkConsumerIncentive = null,
+ useAttestationEndpoints = true,
)
)
@@ -727,6 +729,7 @@ internal class DefaultPaymentElementLoaderTest {
),
disableLinkSignup = false,
linkConsumerIncentive = null,
+ useAttestationEndpoints = true,
)
)
@@ -803,6 +806,7 @@ internal class DefaultPaymentElementLoaderTest {
linkFlags = mapOf(),
disableLinkSignup = false,
linkConsumerIncentive = null,
+ useAttestationEndpoints = true,
),
linkStore = mock {
on { hasUsedLink() } doReturn true
@@ -830,6 +834,7 @@ internal class DefaultPaymentElementLoaderTest {
linkFlags = mapOf(),
disableLinkSignup = true,
linkConsumerIncentive = null,
+ useAttestationEndpoints = true,
)
)
@@ -2350,6 +2355,7 @@ internal class DefaultPaymentElementLoaderTest {
linkFlags = mapOf(),
disableLinkSignup = false,
linkConsumerIncentive = null,
+ useAttestationEndpoints = true,
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt
index 856a6f908b0..67ed993b647 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt
@@ -60,6 +60,7 @@ internal object LinkTestUtils {
passthroughModeEnabled = false,
cardBrandChoice = null,
shippingDetails = null,
+ useAttestationEndpointsForLink = true,
)
}
}
diff --git a/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt b/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt
index e8acdccc9bb..291ec8e13c6 100644
--- a/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt
+++ b/stripe-attestation/src/main/java/com/stripe/attestation/IntegrityStandardRequestManager.kt
@@ -43,7 +43,7 @@ class IntegrityStandardRequestManager(
if (integrityTokenProvider != null) {
return Result.success(Unit)
}
-
+// throw Exception()
val finishedTask: Task = standardIntegrityManager
.prepareIntegrityToken(
PrepareIntegrityTokenRequest.builder()
@@ -68,6 +68,7 @@ class IntegrityStandardRequestManager(
private suspend fun request(
requestHash: String?,
): Result = runCatching {
+// throw Exception()
val finishedTask = requireNotNull(
value = integrityTokenProvider,
lazyMessage = { "Integrity token provider is not initialized. Call prepare() first." }