From f8ff40e2f4a02665bde6b774365853adf89038df Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Fri, 19 Aug 2022 10:44:22 -0700 Subject: [PATCH 01/22] WIP. --- .../FinancialConnectionsSheetViewModel.kt | 49 +++++++++++--- .../FinancialConnectionsSheetActivityArgs.kt | 7 ++ ...FinancialConnectionsSheetActivityResult.kt | 6 +- ...inancialConnectionsSheetForDataContract.kt | 2 +- ...nancialConnectionsSheetForTokenContract.kt | 2 +- ...inancialConnectionsSheetForLinkContract.kt | 48 +++++++++++++ .../FinancialConnectionsSheetLinkResult.kt | 36 ++++++++++ .../FinancialConnectionsSheetViewModelTest.kt | 67 +++++++++++++++++-- .../DefaultConnectionsEventReportTest.kt | 4 +- 9 files changed, 204 insertions(+), 17 deletions(-) create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index bcc0774336d..dfc663bea71 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -1,9 +1,11 @@ package com.stripe.android.financialconnections import android.content.Intent +import android.net.Uri import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext +import com.stripe.android.core.Logger import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl import com.stripe.android.financialconnections.analytics.FinancialConnectionsEventReporter @@ -12,9 +14,12 @@ import com.stripe.android.financialconnections.di.DaggerFinancialConnectionsShee import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSession import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSessionForToken import com.stripe.android.financialconnections.domain.GenerateFinancialConnectionsSessionManifest -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForData +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForLink +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForToken import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Completed import com.stripe.android.financialconnections.model.FinancialConnectionsSession import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest import kotlinx.coroutines.launch @@ -27,6 +32,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private val generateFinancialConnectionsSessionManifest: GenerateFinancialConnectionsSessionManifest, private val fetchFinancialConnectionsSession: FetchFinancialConnectionsSession, private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken, + private val logger: Logger, private val eventReporter: FinancialConnectionsEventReporter, initialState: FinancialConnectionsSheetState ) : MavericksViewModel(initialState) { @@ -142,7 +148,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( kotlin.runCatching { fetchFinancialConnectionsSession(state.sessionSecret) }.onSuccess { - val result = FinancialConnectionsSheetActivityResult.Completed(it) + val result = Completed(financialConnectionsSession = it) eventReporter.onResult(state.initialArgs.configuration, result) setState { copy(viewEffect = FinishWithResult(result)) } }.onFailure { @@ -163,7 +169,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( kotlin.runCatching { fetchFinancialConnectionsSessionForToken(clientSecret = state.sessionSecret) }.onSuccess { (las, token) -> - val result = FinancialConnectionsSheetActivityResult.Completed(las, token) + val result = Completed(financialConnectionsSession = las, token = token) eventReporter.onResult(state.initialArgs.configuration, result) setState { copy(viewEffect = FinishWithResult(result)) } }.onFailure { @@ -206,12 +212,14 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( internal fun handleOnNewIntent(intent: Intent?) { setState { copy(authFlowActive = false) } withState { state -> - when (intent?.data.toString()) { + val receivedUrl: Uri? = intent?.data?.toString()?.toUriOrNull() + if (receivedUrl == null) { + onFatal(state, Exception("Intent url received from web flow is null")) + } else when (val urlWithoutPath = receivedUrl.buildUpon().clearQuery().toString()) { state.manifest?.successUrl -> when (state.initialArgs) { - is FinancialConnectionsSheetActivityArgs.ForData -> - fetchFinancialConnectionsSession(state) - is FinancialConnectionsSheetActivityArgs.ForToken -> - fetchFinancialConnectionsSessionForToken(state) + is ForData -> fetchFinancialConnectionsSession(state) + is ForToken -> fetchFinancialConnectionsSessionForToken(state) + is ForLink -> onSuccessFromLinkFlow(receivedUrl) } state.manifest?.cancelUrl -> onUserCancel(state) else -> @@ -220,10 +228,34 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( } } + /** + * Link flows do not need to fetch the FC session, since the linked account id is + * appended to the web success url. + */ + private fun onSuccessFromLinkFlow(url: Uri) { + kotlin.runCatching { + requireNotNull(url.getQueryParameter(QUERY_PARAM_LINKED_ACCOUNT)) + }.onSuccess { + setState { copy(viewEffect = FinishWithResult(Completed(linkedAccountId = it))) } + }.onFailure { error -> + logger.error("Could not retrieve linked account from success url", error) + withState { state -> onFatal(state, error) } + } + } + fun onViewEffectLaunched() { setState { copy(viewEffect = null) } } + private fun String.toUriOrNull(): Uri? { + Uri.parse(this).buildUpon().clearQuery() + return kotlin.runCatching { + return Uri.parse(this) + }.onFailure { + logger.error("Could not parse web flow url", it) + }.getOrNull() + } + companion object : MavericksViewModelFactory { @@ -240,5 +272,6 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( } internal const val MAX_ACCOUNTS = 100 + internal const val QUERY_PARAM_LINKED_ACCOUNT = "linked_account" } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt index 9fe4ed27ddb..06a143d4d3d 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt @@ -2,6 +2,7 @@ package com.stripe.android.financialconnections.launcher import android.content.Intent import android.os.Parcelable +import androidx.annotation.RestrictTo import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.FinancialConnectionsSheet import kotlinx.parcelize.Parcelize @@ -26,6 +27,12 @@ internal sealed class FinancialConnectionsSheetActivityArgs constructor( override val configuration: FinancialConnectionsSheet.Configuration ) : FinancialConnectionsSheetActivityArgs(configuration) + @Parcelize + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + data class ForLink( + override val configuration: FinancialConnectionsSheet.Configuration + ) : FinancialConnectionsSheetActivityArgs(configuration) + fun validate() { if (configuration.financialConnectionsSessionClientSecret.isBlank()) { throw InvalidParameterException( diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityResult.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityResult.kt index 9f9a95c8aca..6fef5746d58 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityResult.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityResult.kt @@ -19,7 +19,11 @@ internal sealed class FinancialConnectionsSheetActivityResult : Parcelable { */ @Parcelize data class Completed( - val financialConnectionsSession: FinancialConnectionsSession, + // Link sessions: just return linkedAccountId. + val linkedAccountId: String? = null, + // non-Link sessions: return full LinkedAccountSession + val financialConnectionsSession: FinancialConnectionsSession? = null, + // Bank account Token sessions: session + token. val token: Token? = null ) : FinancialConnectionsSheetActivityResult() diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt index ab61b5cda2d..f929db18e7a 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt @@ -36,7 +36,7 @@ internal class FinancialConnectionsSheetForDataContract : is FinancialConnectionsSheetActivityResult.Canceled -> FinancialConnectionsSheetResult.Canceled is FinancialConnectionsSheetActivityResult.Failed -> FinancialConnectionsSheetResult.Failed(error) is FinancialConnectionsSheetActivityResult.Completed -> FinancialConnectionsSheetResult.Completed( - financialConnectionsSession = financialConnectionsSession + financialConnectionsSession = requireNotNull(financialConnectionsSession) ) } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt index f9afdf5a39e..c5cc370787e 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt @@ -38,7 +38,7 @@ internal class FinancialConnectionsSheetForTokenContract : error ) is FinancialConnectionsSheetActivityResult.Completed -> FinancialConnectionsSheetForTokenResult.Completed( - financialConnectionsSession = financialConnectionsSession, + financialConnectionsSession = requireNotNull(financialConnectionsSession), token = requireNotNull(token) ) } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt new file mode 100644 index 00000000000..407814ae13a --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt @@ -0,0 +1,48 @@ +package com.stripe.android.financialconnections.link + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.RestrictTo +import com.airbnb.mvrx.Mavericks +import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity +import com.stripe.android.financialconnections.FinancialConnectionsSheetResult +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Companion.EXTRA_RESULT + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +internal class FinancialConnectionsSheetForLinkContract : + ActivityResultContract() { + + override fun createIntent( + context: Context, + input: FinancialConnectionsSheetActivityArgs.ForData + ): Intent { + return Intent(context, FinancialConnectionsSheetActivity::class.java) + .putExtra(Mavericks.KEY_ARG, input) + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): FinancialConnectionsSheetLinkResult { + return intent + ?.getParcelableExtra(EXTRA_RESULT) + ?.toExposedResult() + ?: FinancialConnectionsSheetLinkResult.Failed( + IllegalArgumentException("Failed to retrieve a ConnectionsSheetResult.") + ) + } + + private fun FinancialConnectionsSheetActivityResult.toExposedResult(): FinancialConnectionsSheetLinkResult = + when (this) { + is FinancialConnectionsSheetActivityResult.Canceled -> FinancialConnectionsSheetLinkResult.Canceled + is FinancialConnectionsSheetActivityResult.Failed -> FinancialConnectionsSheetLinkResult.Failed( + error + ) + is FinancialConnectionsSheetActivityResult.Completed -> FinancialConnectionsSheetLinkResult.Completed( + linkedAccountId = requireNotNull(linkedAccountId) + ) + } +} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt new file mode 100644 index 00000000000..65b76696a60 --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt @@ -0,0 +1,36 @@ +package com.stripe.android.financialconnections.link + +import android.os.Parcelable +import androidx.annotation.RestrictTo +import com.stripe.android.financialconnections.model.FinancialConnectionsSession +import kotlinx.parcelize.Parcelize + +/** + * The result of an attempt to complete a connections session + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +sealed class FinancialConnectionsSheetLinkResult : Parcelable { + /** + * The customer completed the connections session. + * @param financialConnectionsSession The financial connections session connected + */ + @Parcelize + data class Completed( + val linkedAccountId: String + ) : FinancialConnectionsSheetLinkResult() + + /** + * The customer canceled the connections session attempt. + */ + @Parcelize + object Canceled : FinancialConnectionsSheetLinkResult() + + /** + * The connections session attempt failed. + * @param error The error encountered by the customer. + */ + @Parcelize + data class Failed( + val error: Throwable + ) : FinancialConnectionsSheetLinkResult() +} diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index 0cff6ff823a..d9c3308c09b 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.withState import com.google.common.truth.Truth.assertThat +import com.stripe.android.core.Logger import com.stripe.android.core.exception.APIException import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.analytics.FinancialConnectionsEventReporter @@ -13,6 +14,7 @@ import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsS import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSessionForToken import com.stripe.android.financialconnections.domain.GenerateFinancialConnectionsSessionManifest import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForLink import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Completed import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Failed @@ -97,6 +99,58 @@ class FinancialConnectionsSheetViewModelTest { } } + @Test + fun `handleOnNewIntent - Link flows, linked account param is extracted`() { + runTest { + // Given + val linkedAccountId = "1234" + whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) + val viewModel = createViewModel( + defaultInitialState.copy(initialArgs = ForLink(configuration)) + ) + + // When + viewModel.handleOnNewIntent(successIntent( + "stripe-auth://link-accounts/success?linked_account=$linkedAccountId" + )) + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo( + Completed(linkedAccountId = linkedAccountId) + ) + } + } + } + + @Test + fun `handleOnNewIntent - on Link flows, linked account param is extracted`() { + runTest { + // Given + val linkedAccountId = "1234" + whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) + val viewModel = createViewModel( + defaultInitialState.copy(initialArgs = ForLink(configuration)) + ) + + // When + viewModel.handleOnNewIntent(successIntent( + "stripe-auth://link-accounts/success?linked_account=$linkedAccountId" + )) + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo( + Completed(linkedAccountId = linkedAccountId) + ) + } + } + } + @Test fun `handleOnNewIntent - intent with cancel url should fire analytics event and set cancel result`() { runTest { @@ -170,7 +224,9 @@ class FinancialConnectionsSheetViewModelTest { withState(viewModel) { assertThat(it.authFlowActive).isFalse() val viewEffect = it.viewEffect as FinishWithResult - assertThat(viewEffect.result).isEqualTo(Completed(expectedSession)) + assertThat(viewEffect.result).isEqualTo( + Completed(financialConnectionsSession = expectedSession) + ) } } @@ -275,9 +331,9 @@ class FinancialConnectionsSheetViewModelTest { } } - private fun successIntent(): Intent = Intent().apply { - data = Uri.parse(ApiKeyFixtures.SUCCESS_URL) - } + private fun successIntent( + url: String = ApiKeyFixtures.SUCCESS_URL + ): Intent = Intent().apply { data = Uri.parse(url) } private fun cancelIntent() = Intent().also { it.data = Uri.parse(ApiKeyFixtures.CANCEL_URL) @@ -308,7 +364,8 @@ class FinancialConnectionsSheetViewModelTest { generateFinancialConnectionsSessionManifest = generateFinancialConnectionsSessionManifest, fetchFinancialConnectionsSession = fetchFinancialConnectionsSession, fetchFinancialConnectionsSessionForToken = fetchFinancialConnectionsSessionForToken, - eventReporter = eventReporter + eventReporter = eventReporter, + logger = Logger.noop() ) } } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/analytics/DefaultConnectionsEventReportTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/analytics/DefaultConnectionsEventReportTest.kt index bf5b1a38c92..bb12f1a0ee2 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/analytics/DefaultConnectionsEventReportTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/analytics/DefaultConnectionsEventReportTest.kt @@ -70,7 +70,9 @@ class DefaultConnectionsEventReportTest { fun `onResult() should fire analytics request with expected event value for success`() { eventReporter.onResult( configuration, - FinancialConnectionsSheetActivityResult.Completed(financialConnectionsSession) + FinancialConnectionsSheetActivityResult.Completed( + financialConnectionsSession = financialConnectionsSession + ) ) verify(analyticsRequestExecutor).executeAsync( argWhere { req -> From e62b66f660da5a4b29c6da973d8244cb8900fe80 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Fri, 19 Aug 2022 12:20:36 -0700 Subject: [PATCH 02/22] Adds tests. --- .../FinancialConnectionsSheetViewModelTest.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index d9c3308c09b..db6d87a513d 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -100,7 +100,7 @@ class FinancialConnectionsSheetViewModelTest { } @Test - fun `handleOnNewIntent - Link flows, linked account param is extracted`() { + fun `handleOnNewIntent - on Link flows with valid account, id param is extracted`() { runTest { // Given val linkedAccountId = "1234" @@ -126,7 +126,7 @@ class FinancialConnectionsSheetViewModelTest { } @Test - fun `handleOnNewIntent - on Link flows, linked account param is extracted`() { + fun `handleOnNewIntent - on Link flows with invalid account, error is thrown`() { runTest { // Given val linkedAccountId = "1234" @@ -137,16 +137,13 @@ class FinancialConnectionsSheetViewModelTest { // When viewModel.handleOnNewIntent(successIntent( - "stripe-auth://link-accounts/success?linked_account=$linkedAccountId" + "stripe-auth://link-accounts/success" )) // Then withState(viewModel) { - assertThat(it.authFlowActive).isFalse() val viewEffect = it.viewEffect as FinishWithResult - assertThat(viewEffect.result).isEqualTo( - Completed(linkedAccountId = linkedAccountId) - ) + assertThat(viewEffect.result).isInstanceOf(Failed::class.java) } } } From d14e4d2ed8c14f82ed793128b5b12ca679847724 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Tue, 23 Aug 2022 09:36:57 -0700 Subject: [PATCH 03/22] Moves contract to launcher package. --- .../FinancialConnectionsSheetForLinkContract.kt | 5 +---- .../FinancialConnectionsSheetLinkResult.kt | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) rename financial-connections/src/main/java/com/stripe/android/financialconnections/{link => launcher}/FinancialConnectionsSheetForLinkContract.kt (85%) rename financial-connections/src/main/java/com/stripe/android/financialconnections/{link => launcher}/FinancialConnectionsSheetLinkResult.kt (79%) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt similarity index 85% rename from financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt rename to financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt index 407814ae13a..ebac8c311a7 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetForLinkContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt @@ -1,4 +1,4 @@ -package com.stripe.android.financialconnections.link +package com.stripe.android.financialconnections.launcher import android.content.Context import android.content.Intent @@ -6,9 +6,6 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.RestrictTo import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity -import com.stripe.android.financialconnections.FinancialConnectionsSheetResult -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Companion.EXTRA_RESULT @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult.kt similarity index 79% rename from financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt rename to financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult.kt index 65b76696a60..edcd374d928 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/link/FinancialConnectionsSheetLinkResult.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult.kt @@ -1,8 +1,7 @@ -package com.stripe.android.financialconnections.link +package com.stripe.android.financialconnections.launcher import android.os.Parcelable import androidx.annotation.RestrictTo -import com.stripe.android.financialconnections.model.FinancialConnectionsSession import kotlinx.parcelize.Parcelize /** @@ -12,7 +11,7 @@ import kotlinx.parcelize.Parcelize sealed class FinancialConnectionsSheetLinkResult : Parcelable { /** * The customer completed the connections session. - * @param financialConnectionsSession The financial connections session connected + * @param linkedAccountId The linked account id result of the AuthFlow. */ @Parcelize data class Completed( From 1665e02081f08d0c3483cc9247373efebdacff9f Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Tue, 23 Aug 2022 09:56:43 -0700 Subject: [PATCH 04/22] Ktlint fixes. --- .../FinancialConnectionsSheetViewModelTest.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index db6d87a513d..17ac4addb64 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -110,9 +110,11 @@ class FinancialConnectionsSheetViewModelTest { ) // When - viewModel.handleOnNewIntent(successIntent( - "stripe-auth://link-accounts/success?linked_account=$linkedAccountId" - )) + viewModel.handleOnNewIntent( + successIntent( + "stripe-auth://link-accounts/success?linked_account=$linkedAccountId" + ) + ) // Then withState(viewModel) { @@ -136,9 +138,11 @@ class FinancialConnectionsSheetViewModelTest { ) // When - viewModel.handleOnNewIntent(successIntent( - "stripe-auth://link-accounts/success" - )) + viewModel.handleOnNewIntent( + successIntent( + "stripe-auth://link-accounts/success" + ) + ) // Then withState(viewModel) { From 11dd135ae650470edad4c4089685f1b363ec540b Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Wed, 24 Aug 2022 15:56:14 -0700 Subject: [PATCH 05/22] Use ForLink as contract input. --- .../launcher/FinancialConnectionsSheetForLinkContract.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt index ebac8c311a7..2fb85f88d0c 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt @@ -10,11 +10,11 @@ import com.stripe.android.financialconnections.launcher.FinancialConnectionsShee @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) internal class FinancialConnectionsSheetForLinkContract : - ActivityResultContract() { + ActivityResultContract() { override fun createIntent( context: Context, - input: FinancialConnectionsSheetActivityArgs.ForData + input: FinancialConnectionsSheetActivityArgs.ForLink ): Intent { return Intent(context, FinancialConnectionsSheetActivity::class.java) .putExtra(Mavericks.KEY_ARG, input) From 830d0454666ed9766d4d1f005371e14b4ca1602f Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Wed, 24 Aug 2022 16:01:38 -0700 Subject: [PATCH 06/22] exposes internal objects for link. --- .../api/financial-connections.api | 79 ++++++++++++++++++- .../FinancialConnectionsSheetViewModel.kt | 2 +- .../FinancialConnectionsSheetActivityArgs.kt | 8 +- ...inancialConnectionsSheetForLinkContract.kt | 2 +- 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index 309aa142b92..6f5434a5201 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -199,11 +199,11 @@ public abstract interface class com/stripe/android/financialconnections/Financia } public final class com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel_Factory : dagger/internal/Factory { - public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel_Factory; public fun get ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; public synthetic fun get ()Ljava/lang/Object; - public static fun newInstance (Ljava/lang/String;Lcom/stripe/android/financialconnections/domain/GenerateFinancialConnectionsSessionManifest;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSession;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSessionForToken;Lcom/stripe/android/financialconnections/analytics/FinancialConnectionsEventReporter;Lcom/stripe/android/financialconnections/FinancialConnectionsSheetState;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; + public static fun newInstance (Ljava/lang/String;Lcom/stripe/android/financialconnections/domain/GenerateFinancialConnectionsSessionManifest;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSession;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSessionForToken;Lcom/stripe/android/core/Logger;Lcom/stripe/android/financialconnections/analytics/FinancialConnectionsEventReporter;Lcom/stripe/android/financialconnections/FinancialConnectionsSheetState;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; } public final class com/stripe/android/financialconnections/analytics/DefaultFinancialConnectionsEventReporter_Factory : dagger/internal/Factory { @@ -330,6 +330,9 @@ public final class com/stripe/android/financialconnections/domain/GenerateFinanc public static fun newInstance (Lcom/stripe/android/financialconnections/repository/FinancialConnectionsRepository;)Lcom/stripe/android/financialconnections/domain/GenerateFinancialConnectionsSessionManifest; } +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$Companion { +} + public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; @@ -338,6 +341,14 @@ public final class com/stripe/android/financialconnections/launcher/FinancialCon public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; @@ -370,6 +381,68 @@ public final class com/stripe/android/financialconnections/launcher/FinancialCon public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Canceled : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public static final field INSTANCE Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Canceled; + public fun describeContents ()I + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Canceled$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Canceled; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Canceled; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed; + public static synthetic fun copy$default (Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed; + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getLinkedAccountId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Completed; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/Throwable;)V + public final fun component1 ()Ljava/lang/Throwable; + public final fun copy (Ljava/lang/Throwable;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed; + public static synthetic fun copy$default (Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed; + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getError ()Ljava/lang/Throwable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetLinkResult$Failed; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/financialconnections/model/AccountHolder : android/os/Parcelable, com/stripe/android/core/model/StripeModel { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index dfc663bea71..845c3edff56 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -215,7 +215,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( val receivedUrl: Uri? = intent?.data?.toString()?.toUriOrNull() if (receivedUrl == null) { onFatal(state, Exception("Intent url received from web flow is null")) - } else when (val urlWithoutPath = receivedUrl.buildUpon().clearQuery().toString()) { + } else when (receivedUrl.buildUpon().clearQuery().toString()) { state.manifest?.successUrl -> when (state.initialArgs) { is ForData -> fetchFinancialConnectionsSession(state) is ForToken -> fetchFinancialConnectionsSessionForToken(state) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt index 06a143d4d3d..5abb49ea263 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt @@ -13,16 +13,20 @@ import java.security.InvalidParameterException * [com.stripe.android.financialconnections.FinancialConnectionsSheetActivity] and * instances of [com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetLauncher]. */ -internal sealed class FinancialConnectionsSheetActivityArgs constructor( + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +sealed class FinancialConnectionsSheetActivityArgs constructor( open val configuration: FinancialConnectionsSheet.Configuration ) : Parcelable { @Parcelize + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class ForData( override val configuration: FinancialConnectionsSheet.Configuration ) : FinancialConnectionsSheetActivityArgs(configuration) @Parcelize + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class ForToken( override val configuration: FinancialConnectionsSheet.Configuration ) : FinancialConnectionsSheetActivityArgs(configuration) @@ -33,7 +37,7 @@ internal sealed class FinancialConnectionsSheetActivityArgs constructor( override val configuration: FinancialConnectionsSheet.Configuration ) : FinancialConnectionsSheetActivityArgs(configuration) - fun validate() { + internal fun validate() { if (configuration.financialConnectionsSessionClientSecret.isBlank()) { throw InvalidParameterException( "The session client secret cannot be an empty string." diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt index 2fb85f88d0c..51a94720046 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForLinkContract.kt @@ -9,7 +9,7 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Companion.EXTRA_RESULT @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -internal class FinancialConnectionsSheetForLinkContract : +class FinancialConnectionsSheetForLinkContract : ActivityResultContract() { override fun createIntent( From cf0b4f37603813bcdb11dc844c721190af10ccd1 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Wed, 24 Aug 2022 16:04:31 -0700 Subject: [PATCH 07/22] Add bank account --- .../api/financial-connections.api | 41 ++++ link/api/link.api | 48 ++-- link/build.gradle | 1 + link/res/drawable/ic_link_add.xml | 19 +- link/res/drawable/ic_link_add_green.xml | 16 ++ link/res/drawable/ic_link_bank.xml | 8 +- link/res/drawable/ic_link_card.xml | 9 + link/res/values/strings.xml | 4 +- .../com/stripe/android/link/LinkActivity.kt | 2 +- .../android/link/LinkPaymentLauncher.kt | 4 +- .../link/account/LinkAccountManager.kt | 39 +++- .../stripe/android/link/model/Navigator.kt | 10 +- .../link/repositories/LinkApiRepository.kt | 13 +- .../link/repositories/LinkRepository.kt | 13 +- .../com/stripe/android/link/theme/Theme.kt | 1 + .../stripe/android/link/ui/PrimaryButton.kt | 80 ++++--- .../link/ui/cardedit/CardEditViewModel.kt | 28 +-- .../ui/paymentmethod/PaymentMethodBody.kt | 211 +++++++++++++++--- .../paymentmethod/PaymentMethodViewModel.kt | 130 ++++++++--- .../paymentmethod/SupportedPaymentMethod.kt | 78 ++++--- .../android/link/ui/signup/SignUpScreen.kt | 9 +- .../ui/verification/VerificationViewModel.kt | 2 +- .../link/ui/wallet/PaymentDetailsResult.kt | 24 ++ .../android/link/ui/wallet/WalletScreen.kt | 39 ++-- .../android/link/ui/wallet/WalletViewModel.kt | 39 ++-- .../link/account/LinkAccountManagerTest.kt | 137 +++++++++--- .../repositories/LinkApiRepositoryTest.kt | 12 +- .../link/ui/cardedit/CardEditViewModelTest.kt | 9 +- .../PaymentMethodViewModelTest.kt | 166 ++++++++++---- .../link/ui/wallet/WalletViewModelTest.kt | 15 +- payments-core/api/payments-core.api | 5 + .../ConsumerPaymentDetailsCreateParams.kt | 11 + payments-ui-core/res/values/totranslate.xml | 3 + 33 files changed, 873 insertions(+), 353 deletions(-) create mode 100644 link/res/drawable/ic_link_add_green.xml create mode 100644 link/res/drawable/ic_link_card.xml create mode 100644 link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index 6f5434a5201..8f673f1d8bd 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -333,6 +333,24 @@ public final class com/stripe/android/financialconnections/domain/GenerateFinanc public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$Companion { } +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$Companion { +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)V + public final fun component1 ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; + public final fun copy (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; + public static synthetic fun copy$default (Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData;Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;ILjava/lang/Object;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public fun getConfiguration ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; @@ -349,6 +367,29 @@ public final class com/stripe/android/financialconnections/launcher/FinancialCon public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)V + public final fun component1 ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; + public final fun copy (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; + public static synthetic fun copy$default (Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken;Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;ILjava/lang/Object;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public fun getConfiguration ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; diff --git a/link/api/link.api b/link/api/link.api index 927055c4870..f8031f2ac71 100644 --- a/link/api/link.api +++ b/link/api/link.api @@ -361,30 +361,6 @@ public final class com/stripe/android/link/ui/LinkTermsKt { public static final fun LinkTerms-5stqomU (Landroidx/compose/ui/Modifier;ILandroidx/compose/runtime/Composer;II)V } -public final class com/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Cancelled$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Cancelled; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Cancelled; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Failure$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Failure; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Failure; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Success$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Success; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Success; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/stripe/android/link/ui/cardedit/CardEditViewModel_Factory : dagger/internal/Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel_Factory; @@ -584,6 +560,30 @@ public final class com/stripe/android/link/ui/wallet/ComposableSingletons$Wallet public final fun getLambda-2$link_release ()Lkotlin/jvm/functions/Function2; } +public final class com/stripe/android/link/ui/wallet/PaymentDetailsResult$Cancelled$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Cancelled; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Cancelled; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/link/ui/wallet/PaymentDetailsResult$Failure$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Failure; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Failure; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/link/ui/wallet/PaymentDetailsResult$Success$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Success; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Success; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/link/ui/wallet/WalletViewModel_Factory : dagger/internal/Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/link/ui/wallet/WalletViewModel_Factory; diff --git a/link/build.gradle b/link/build.gradle index 680bab7ac20..702d4879fa6 100644 --- a/link/build.gradle +++ b/link/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation project(':payments-core') implementation project(':stripe-core') implementation project(':payments-ui-core') + implementation project(':financial-connections') implementation "androidx.appcompat:appcompat:$androidxAppcompatVersion" implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintlayoutVersion" diff --git a/link/res/drawable/ic_link_add.xml b/link/res/drawable/ic_link_add.xml index 57e7183ca39..8afe25acf9b 100644 --- a/link/res/drawable/ic_link_add.xml +++ b/link/res/drawable/ic_link_add.xml @@ -1,16 +1,9 @@ + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> - + android:pathData="M8.75,0.75C8.75,0.336 8.414,0 8,0C7.586,0 7.25,0.336 7.25,0.75V7.25H0.75C0.336,7.25 0,7.586 0,8C0,8.414 0.336,8.75 0.75,8.75H7.25V15.25C7.25,15.664 7.586,16 8,16C8.414,16 8.75,15.664 8.75,15.25V8.75H15.25C15.664,8.75 16,8.414 16,8C16,7.586 15.664,7.25 15.25,7.25H8.75V0.75Z" + android:fillColor="#1D3944"/> diff --git a/link/res/drawable/ic_link_add_green.xml b/link/res/drawable/ic_link_add_green.xml new file mode 100644 index 00000000000..57e7183ca39 --- /dev/null +++ b/link/res/drawable/ic_link_add_green.xml @@ -0,0 +1,16 @@ + + + + diff --git a/link/res/drawable/ic_link_bank.xml b/link/res/drawable/ic_link_bank.xml index 71593285b99..79ed3f998e8 100644 --- a/link/res/drawable/ic_link_bank.xml +++ b/link/res/drawable/ic_link_bank.xml @@ -1,9 +1,9 @@ + android:pathData="M1.328,5.047C1.328,5.43 1.625,5.797 2.133,5.797H16.852C17.359,5.797 17.656,5.43 17.656,5.047C17.656,4.766 17.5,4.539 17.164,4.328L10.508,0.492C10.18,0.305 9.828,0.203 9.492,0.203C9.156,0.203 8.805,0.305 8.477,0.492L1.82,4.328C1.484,4.539 1.328,4.766 1.328,5.047ZM2.359,14.055C2.359,14.43 2.578,14.648 2.961,14.648H4.961C5.344,14.648 5.563,14.43 5.563,14.055V13.844C5.563,13.477 5.344,13.258 4.961,13.258H4.664V7.789H4.961C5.344,7.789 5.563,7.57 5.563,7.195V6.984C5.563,6.609 5.344,6.391 4.961,6.391H2.961C2.578,6.391 2.359,6.609 2.359,6.984V7.195C2.359,7.57 2.578,7.789 2.961,7.789H3.273V13.258H2.961C2.578,13.258 2.359,13.477 2.359,13.844V14.055ZM6.078,14.055C6.078,14.43 6.305,14.648 6.68,14.648H8.688C9.063,14.648 9.281,14.43 9.281,14.055V13.844C9.281,13.477 9.063,13.258 8.688,13.258H8.383V7.789H8.688C9.063,7.789 9.281,7.57 9.281,7.195V6.984C9.281,6.609 9.063,6.391 8.688,6.391H6.68C6.305,6.391 6.078,6.609 6.078,6.984V7.195C6.078,7.57 6.305,7.789 6.68,7.789H6.992V13.258H6.68C6.305,13.258 6.078,13.477 6.078,13.844V14.055ZM9.813,14.055C9.813,14.43 10.031,14.648 10.406,14.648H12.414C12.789,14.648 13.016,14.43 13.016,14.055V13.844C13.016,13.477 12.789,13.258 12.414,13.258H12.117V7.789H12.414C12.789,7.789 13.016,7.57 13.016,7.195V6.984C13.016,6.609 12.789,6.391 12.414,6.391H10.406C10.031,6.391 9.813,6.609 9.813,6.984V7.195C9.813,7.57 10.031,7.789 10.406,7.789H10.719V13.258H10.406C10.031,13.258 9.813,13.477 9.813,13.844V14.055ZM13.531,14.055C13.531,14.43 13.75,14.648 14.133,14.648H16.133C16.516,14.648 16.734,14.43 16.734,14.055V13.844C16.734,13.477 16.516,13.258 16.133,13.258H15.836V7.789H16.133C16.516,7.789 16.734,7.57 16.734,7.195V6.984C16.734,6.609 16.516,6.391 16.133,6.391H14.133C13.75,6.391 13.531,6.609 13.531,6.984V7.195C13.531,7.57 13.75,7.789 14.133,7.789H14.438V13.258H14.133C13.75,13.258 13.531,13.477 13.531,13.844V14.055ZM0.906,16C0.906,16.406 1.242,16.742 1.656,16.742H17.344C17.75,16.742 18.086,16.406 18.086,16C18.086,15.586 17.75,15.25 17.344,15.25H1.656C1.242,15.25 0.906,15.586 0.906,16Z" + android:fillColor="#6A7383"/> diff --git a/link/res/drawable/ic_link_card.xml b/link/res/drawable/ic_link_card.xml new file mode 100644 index 00000000000..826b19e80ca --- /dev/null +++ b/link/res/drawable/ic_link_card.xml @@ -0,0 +1,9 @@ + + + diff --git a/link/res/values/strings.xml b/link/res/values/strings.xml index 4161d3c0c92..cc5aea9a6ae 100644 --- a/link/res/values/strings.xml +++ b/link/res/values/strings.xml @@ -31,10 +31,10 @@ Are you sure you want to remove this account? By continuing, you agree to authorize payments pursuant to <a href=\"https://stripe.com/legal/ach-payments/authorization\">these terms</a>. - Add a payment method + Add a payment method + Add bank account Pay another way - Add a new card This is your default Set as default payment diff --git a/link/src/main/java/com/stripe/android/link/LinkActivity.kt b/link/src/main/java/com/stripe/android/link/LinkActivity.kt index 945c3371827..83397253d95 100644 --- a/link/src/main/java/com/stripe/android/link/LinkActivity.kt +++ b/link/src/main/java/com/stripe/android/link/LinkActivity.kt @@ -119,7 +119,7 @@ internal class LinkActivity : ComponentActivity() { LinkAppBar( state = appBarState, - onButtonClick = { viewModel.navigator.onBack() } + onButtonClick = { viewModel.navigator.onBack(true) } ) NavHost(navController, LinkScreen.Loading.route) { diff --git a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt index 165532090c4..13b53d0bb4c 100644 --- a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt +++ b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt @@ -26,7 +26,6 @@ import com.stripe.android.link.ui.cardedit.CardEditViewModel import com.stripe.android.link.ui.inline.InlineSignupViewModel import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.link.ui.paymentmethod.PaymentMethodViewModel -import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.link.ui.signup.SignUpViewModel import com.stripe.android.link.ui.verification.VerificationViewModel import com.stripe.android.link.ui.wallet.WalletViewModel @@ -183,8 +182,7 @@ class LinkPaymentLauncher @AssistedInject internal constructor( suspend fun attachNewCardToAccount( paymentMethodCreateParams: PaymentMethodCreateParams ): Result = - linkAccountManager.createPaymentDetails( - SupportedPaymentMethod.Card, + linkAccountManager.createCardPaymentDetails( paymentMethodCreateParams ) diff --git a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt index 2f9935c7230..b28607e1650 100644 --- a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt +++ b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt @@ -10,7 +10,6 @@ import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.repositories.LinkRepository import com.stripe.android.link.ui.inline.UserInput -import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.PaymentMethodCreateParams @@ -41,7 +40,6 @@ internal class LinkAccountManager @Inject constructor( /** * The publishable key for the signed in Link account. */ - @VisibleForTesting var consumerPublishableKey: String? = null val accountStatus = linkAccount.transform { value -> @@ -201,18 +199,16 @@ internal class LinkAccountManager @Inject constructor( } /** - * Creates a new PaymentDetails attached to the current account. + * Creates a new PaymentDetails.Card attached to the current account. * * @return The parameters needed to confirm the current Stripe Intent using the newly created * Payment Details. */ - suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams ): Result = linkAccount.value?.let { account -> - createPaymentDetails( - paymentMethod, + createCardPaymentDetails( paymentMethodCreateParams, account.email, stripeIntent @@ -222,16 +218,37 @@ internal class LinkAccountManager @Inject constructor( ) /** - * Create a new payment method in the signed in consumer account. + * Create a session used to connect a bank account through Financial Connections. + */ + suspend fun createFinancialConnectionsSession() = retryingOnAuthError { clientSecret -> + linkRepository.createFinancialConnectionsSession( + clientSecret, + consumerPublishableKey + ) + } + + /** + * Create a new Bank Account payment method attached to the consumer account. */ suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + financialConnectionsAccountId: String + ) = retryingOnAuthError { clientSecret -> + linkRepository.createPaymentDetails( + financialConnectionsAccountId, + clientSecret, + consumerPublishableKey + ) + } + + /** + * Create a new Card payment method attached to the consumer account. + */ + suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams, userEmail: String, stripeIntent: StripeIntent ) = retryingOnAuthError { clientSecret -> - linkRepository.createPaymentDetails( - paymentMethod, + linkRepository.createCardPaymentDetails( paymentMethodCreateParams, userEmail, stripeIntent, diff --git a/link/src/main/java/com/stripe/android/link/model/Navigator.kt b/link/src/main/java/com/stripe/android/link/model/Navigator.kt index 06bf6e66025..66a8690d41b 100644 --- a/link/src/main/java/com/stripe/android/link/model/Navigator.kt +++ b/link/src/main/java/com/stripe/android/link/model/Navigator.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton */ @Singleton internal class Navigator @Inject constructor() { - var backNavigationEnabled = true + var userNavigationEnabled = true var navigationController: NavHostController? = null var onDismiss: ((LinkActivityResult) -> Unit)? = null @@ -41,10 +41,12 @@ internal class Navigator @Inject constructor() { /** * Behaves like a back button, popping the back stack and dismissing the Activity if this was * the last screen. - * Only performs any action if [backNavigationEnabled] is true. + * When [userInitiated] is true, only performs any action if [userNavigationEnabled] is true. + * + * @param userInitiated Whether the action was initiated by user interaction. */ - fun onBack() { - if (backNavigationEnabled) { + fun onBack(userInitiated: Boolean = false) { + if (!userInitiated || userNavigationEnabled) { navigationController?.let { navController -> if (!navController.popBackStack()) { dismiss() diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt index 26c685237a8..9b494bd14b7 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt @@ -8,6 +8,7 @@ import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.confirmation.ConfirmStripeIntentParamsFactory import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.ConsumerSessionLookup @@ -203,8 +204,7 @@ internal class LinkApiRepository @Inject constructor( } } - override suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + override suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams, userEmail: String, stripeIntent: StripeIntent, @@ -215,7 +215,10 @@ internal class LinkApiRepository @Inject constructor( requireNotNull( stripeRepository.createPaymentDetails( consumerSessionClientSecret, - paymentMethod.createParams(paymentMethodCreateParams, userEmail), + ConsumerPaymentDetailsCreateParams.Card( + paymentMethodCreateParams.toParamMap(), + userEmail + ), consumerPublishableKey?.let { ApiRequest.Options(it) } ?: ApiRequest.Options( @@ -229,7 +232,9 @@ internal class LinkApiRepository @Inject constructor( .createPaymentMethodCreateParams( consumerSessionClientSecret, it, - paymentMethod.extraConfirmationParams(paymentMethodCreateParams) + ConsumerPaymentDetailsCreateParams.Card.extraConfirmationParams( + paymentMethodCreateParams + ) ), paymentMethodCreateParams ) diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt index 89a2b129bb9..1bf95b4a041 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt @@ -1,7 +1,6 @@ package com.stripe.android.link.repositories import com.stripe.android.link.LinkPaymentDetails -import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession @@ -69,11 +68,18 @@ internal interface LinkRepository { consumerPublishableKey: String? ): Result + /** + * Create a new [FinancialConnectionsSession], used to link a bank account using the Financial + * Connections SDK. + */ suspend fun createFinancialConnectionsSession( consumerSessionClientSecret: String, consumerPublishableKey: String? ): Result + /** + * Create a new linked bank account payment method in the consumer account. + */ suspend fun createPaymentDetails( financialConnectionsAccountId: String, consumerSessionClientSecret: String, @@ -81,10 +87,9 @@ internal interface LinkRepository { ): Result /** - * Create a new payment method in the consumer account. + * Create a new card payment method in the consumer account. */ - suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams, userEmail: String, stripeIntent: StripeIntent, diff --git a/link/src/main/java/com/stripe/android/link/theme/Theme.kt b/link/src/main/java/com/stripe/android/link/theme/Theme.kt index 140a989c99c..e216b964aa5 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Theme.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Theme.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.unit.dp internal val MinimumTouchTargetSize = 48.dp internal val AppBarHeight = 56.dp +internal val PrimaryButtonHeight = 56.dp internal val HorizontalPadding = 20.dp private val LocalColors = staticCompositionLocalOf { LinkThemeConfig.colors(false) } diff --git a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt index 00aed43b323..4fce88836a0 100644 --- a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt +++ b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt @@ -3,6 +3,7 @@ package com.stripe.android.link.ui import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -26,13 +27,14 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.stripe.android.link.LinkActivityContract import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.HorizontalPadding +import com.stripe.android.link.theme.PrimaryButtonHeight import com.stripe.android.link.theme.linkColors import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent +import com.stripe.android.model.StripeIntent import com.stripe.android.ui.core.Amount /** @@ -56,13 +58,13 @@ internal enum class PrimaryButtonState(val isBlocking: Boolean) { internal const val progressIndicatorTestTag = "CircularProgressIndicator" internal const val completedIconTestTag = "CompletedIcon" -internal fun primaryButtonLabel( - args: LinkActivityContract.Args, +internal fun completePaymentButtonLabel( + stripeIntent: StripeIntent, resources: Resources -) = when (args.stripeIntent) { +) = when (stripeIntent) { is PaymentIntent -> Amount( - requireNotNull(args.stripeIntent.amount), - requireNotNull(args.stripeIntent.currency) + requireNotNull(stripeIntent.amount), + requireNotNull(stripeIntent.currency) ).buildPayButtonLabel(resources) is SetupIntent -> resources.getString(R.string.stripe_setup_button_label) } @@ -74,8 +76,8 @@ private fun PrimaryButton() { PrimaryButton( label = "Testing", state = PrimaryButtonState.Enabled, - icon = R.drawable.stripe_ic_lock, - onButtonClick = { } + onButtonClick = { }, + iconEnd = R.drawable.stripe_ic_lock ) } } @@ -84,8 +86,9 @@ private fun PrimaryButton() { internal fun PrimaryButton( label: String, state: PrimaryButtonState, - @DrawableRes icon: Int? = null, - onButtonClick: () -> Unit + onButtonClick: () -> Unit, + @DrawableRes iconStart: Int? = null, + @DrawableRes iconEnd: Int? = null ) { CompositionLocalProvider( LocalContentAlpha provides @@ -94,14 +97,12 @@ internal fun PrimaryButton( Box( modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp), - contentAlignment = Alignment.CenterEnd + .height(PrimaryButtonHeight + 32.dp) + .padding(vertical = 16.dp) ) { Button( onClick = onButtonClick, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), + modifier = Modifier.fillMaxSize(), enabled = state == PrimaryButtonState.Enabled, elevation = ButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp, 0.dp), shape = MaterialTheme.shapes.medium, @@ -137,25 +138,44 @@ internal fun PrimaryButton( ) } } - // Show icon only when button label is visible - if (icon != null && - state in setOf(PrimaryButtonState.Enabled, PrimaryButtonState.Disabled) - ) { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - modifier = Modifier - .height(16.dp) - // width should be 13dp and must include the horizontal padding - .width(13.dp + 40.dp) - .padding(horizontal = HorizontalPadding), - tint = MaterialTheme.linkColors.buttonLabel.copy(alpha = LocalContentAlpha.current) - ) + // Show icons only when button label is visible + if (state in setOf(PrimaryButtonState.Enabled, PrimaryButtonState.Disabled)) { + iconStart?.let { icon -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.CenterStart + ) { + PrimaryButtonIcon(icon) + } + } + iconEnd?.let { icon -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.CenterEnd + ) { + PrimaryButtonIcon(icon) + } + } } } } } +@Composable +private fun PrimaryButtonIcon( + @DrawableRes icon: Int +) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier + .height(16.dp) + .width(13.dp + 40.dp) + .padding(horizontal = HorizontalPadding), + tint = MaterialTheme.linkColors.buttonLabel.copy(alpha = LocalContentAlpha.current) + ) +} + @Composable internal fun SecondaryButton( enabled: Boolean, @@ -166,7 +186,7 @@ internal fun SecondaryButton( onClick = onClick, modifier = Modifier .fillMaxWidth() - .height(56.dp), + .height(PrimaryButtonHeight), enabled = enabled, shape = MaterialTheme.shapes.medium, colors = ButtonDefaults.buttonColors( diff --git a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt index 8b140b7cbd9..2c47cdda301 100644 --- a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt @@ -1,6 +1,5 @@ package com.stripe.android.link.ui.cardedit -import android.os.Parcelable import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -13,6 +12,7 @@ import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.getErrorMessage +import com.stripe.android.link.ui.wallet.PaymentDetailsResult import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.PaymentMethod @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize import javax.inject.Inject import javax.inject.Provider @@ -75,13 +74,13 @@ internal class CardEditViewModel @Inject constructor( .merchantName(args.merchantName) .build().formController } ?: dismiss( - Result.Failure( + PaymentDetailsResult.Failure( ErrorMessage.Raw("Payment details $paymentDetailsId not found.") ) ) }, onFailure = { - dismiss(Result.Failure(it.getErrorMessage())) + dismiss(PaymentDetailsResult.Failure(it.getErrorMessage())) } ) } @@ -112,15 +111,15 @@ internal class CardEditViewModel @Inject constructor( linkAccountManager.updatePaymentDetails(updateParams).fold( onSuccess = { _isProcessing.value = false - dismiss(Result.Success) + dismiss(PaymentDetailsResult.Success(paymentDetails.id)) }, onFailure = ::onError ) } } - fun dismiss(result: Result = Result.Cancelled) { - navigator.setResult(Result.KEY, result) + fun dismiss(result: PaymentDetailsResult = PaymentDetailsResult.Cancelled) { + navigator.setResult(PaymentDetailsResult.KEY, result) navigator.onBack() } @@ -161,19 +160,4 @@ internal class CardEditViewModel @Inject constructor( } as T } } - - sealed class Result : Parcelable { - @Parcelize - object Success : Result() - - @Parcelize - object Cancelled : Result() - - @Parcelize - class Failure(val error: ErrorMessage) : Result() - - companion object { - const val KEY = "CardEditScreenResult" - } - } } diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt index 44da62c8af1..8ad39836930 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt @@ -1,31 +1,49 @@ package com.stripe.android.link.ui.paymentmethod +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.stripe.android.financialconnections.FinancialConnectionsSheet +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetForLinkContract import com.stripe.android.link.R import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.PaymentsThemeForLink +import com.stripe.android.link.theme.linkColors import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.ErrorText import com.stripe.android.link.ui.PrimaryButton @@ -33,7 +51,6 @@ import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.ScrollableTopLevelColumn import com.stripe.android.link.ui.SecondaryButton import com.stripe.android.link.ui.forms.Form -import com.stripe.android.link.ui.primaryButtonLabel import com.stripe.android.ui.core.injection.NonFallbackInjector @Preview @@ -42,10 +59,13 @@ private fun PaymentMethodBodyPreview() { DefaultLinkTheme { Surface { PaymentMethodBody( + supportedPaymentMethods = SupportedPaymentMethod.allValues, + selectedPaymentMethod = SupportedPaymentMethod.Card, primaryButtonLabel = "Pay $10.99", primaryButtonState = PrimaryButtonState.Enabled, secondaryButtonLabel = "Cancel", errorMessage = null, + onPaymentMethodSelected = {}, onPrimaryButtonClick = {}, onSecondaryButtonClick = {} ) {} @@ -63,9 +83,60 @@ internal fun PaymentMethodBody( factory = PaymentMethodViewModel.Factory(linkAccount, injector, loadFromArgs) ) + val activityResultLauncher = rememberLauncherForActivityResult( + contract = FinancialConnectionsSheetForLinkContract(), + onResult = viewModel::onFinancialConnectionsAccountLinked + ) + + val clientSecret by viewModel.financialConnectionsSessionClientSecret.collectAsState() + + clientSecret?.let { secret -> + LaunchedEffect(secret) { + activityResultLauncher.launch( + FinancialConnectionsSheetActivityArgs.ForLink( + FinancialConnectionsSheet.Configuration( + financialConnectionsSessionClientSecret = secret, + publishableKey = viewModel.publishableKey + ) + ) + ) + } + } + val formController by viewModel.formController.collectAsState() - if (formController == null) { + formController?.let { + val formValues by it.completeFormValues.collectAsState(null) + val primaryButtonState by viewModel.primaryButtonState.collectAsState() + val errorMessage by viewModel.errorMessage.collectAsState() + val paymentMethod by viewModel.paymentMethod.collectAsState() + + PaymentMethodBody( + supportedPaymentMethods = viewModel.supportedTypes, + selectedPaymentMethod = paymentMethod, + primaryButtonLabel = paymentMethod.primaryButtonLabel( + viewModel.args.stripeIntent, + LocalContext.current.resources + ), + primaryButtonState = primaryButtonState.takeIf { formValues != null } + ?: PrimaryButtonState.Disabled, + secondaryButtonLabel = stringResource(id = viewModel.secondaryButtonLabel), + errorMessage = errorMessage, + onPaymentMethodSelected = viewModel::onPaymentMethodSelected, + onPrimaryButtonClick = { + formValues?.let { + viewModel.startPayment(it) + } + }, + onSecondaryButtonClick = viewModel::onSecondaryButtonClick, + formContent = { + Form( + it, + viewModel.isEnabled + ) + } + ) + } ?: run { Box( modifier = Modifier .fillMaxHeight() @@ -74,60 +145,60 @@ internal fun PaymentMethodBody( ) { CircularProgressIndicator() } - } else { - formController?.let { - val formValues by it.completeFormValues.collectAsState(null) - val primaryButtonState by viewModel.primaryButtonState.collectAsState() - val errorMessage by viewModel.errorMessage.collectAsState() - - PaymentMethodBody( - primaryButtonLabel = primaryButtonLabel( - viewModel.args, - LocalContext.current.resources - ), - primaryButtonState = primaryButtonState.takeIf { formValues != null } - ?: PrimaryButtonState.Disabled, - secondaryButtonLabel = stringResource(id = viewModel.secondaryButtonLabel), - errorMessage = errorMessage, - onPrimaryButtonClick = { - formValues?.let { - viewModel.startPayment(it) - } - }, - onSecondaryButtonClick = viewModel::onSecondaryButtonClick - ) { - Form( - it, - viewModel.isEnabled - ) - } - } } } @Composable internal fun PaymentMethodBody( + supportedPaymentMethods: List, + selectedPaymentMethod: SupportedPaymentMethod, primaryButtonLabel: String, primaryButtonState: PrimaryButtonState, secondaryButtonLabel: String, errorMessage: ErrorMessage?, + onPaymentMethodSelected: (SupportedPaymentMethod) -> Unit, onPrimaryButtonClick: () -> Unit, onSecondaryButtonClick: () -> Unit, formContent: @Composable ColumnScope.() -> Unit ) { ScrollableTopLevelColumn { Text( - text = stringResource(R.string.pm_add_new_card), + text = stringResource(R.string.add_payment_method), modifier = Modifier .padding(top = 4.dp, bottom = 32.dp), textAlign = TextAlign.Center, style = MaterialTheme.typography.h2, color = MaterialTheme.colors.onPrimary ) - PaymentsThemeForLink { - formContent() + if (supportedPaymentMethods.size > 1) { + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { + supportedPaymentMethods.forEachIndexed { index, paymentMethod -> + PaymentMethodTypeCell( + paymentMethod = paymentMethod, + selected = paymentMethod == selectedPaymentMethod, + enabled = !primaryButtonState.isBlocking, + onSelected = { + onPaymentMethodSelected(paymentMethod) + }, + modifier = Modifier.padding( + start = if (index > 0) 10.dp else 0.dp, + end = if (index < supportedPaymentMethods.lastIndex) 10.dp else 0.dp + ) + ) + } + } + } + if (selectedPaymentMethod.showsForm) { + Spacer(modifier = Modifier.height(4.dp)) + PaymentsThemeForLink { + formContent() + } + Spacer(modifier = Modifier.height(8.dp)) } - Spacer(modifier = Modifier.height(8.dp)) errorMessage?.let { ErrorText( text = it.getMessage(LocalContext.current.resources), @@ -137,8 +208,9 @@ internal fun PaymentMethodBody( PrimaryButton( label = primaryButtonLabel, state = primaryButtonState, - icon = R.drawable.stripe_ic_lock, - onButtonClick = onPrimaryButtonClick + onButtonClick = onPrimaryButtonClick, + iconStart = selectedPaymentMethod.primaryButtonStartIconResourceId, + iconEnd = selectedPaymentMethod.primaryButtonEndIconResourceId ) SecondaryButton( enabled = !primaryButtonState.isBlocking, @@ -147,3 +219,70 @@ internal fun PaymentMethodBody( ) } } + +@Composable +private fun RowScope.PaymentMethodTypeCell( + paymentMethod: SupportedPaymentMethod, + selected: Boolean, + enabled: Boolean, + onSelected: () -> Unit, + modifier: Modifier = Modifier +) { + CompositionLocalProvider(LocalContentAlpha provides if (enabled) 1f else 0.6f) { + Surface( + modifier = modifier + .height(56.dp) + .weight(1f), + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.linkColors.componentBackground, + border = BorderStroke( + width = if (selected) { + 2.dp + } else { + 1.dp + }, + color = if (selected) { + MaterialTheme.colors.primary + } else { + MaterialTheme.linkColors.componentBorder + } + ) + ) { + Row( + modifier = Modifier + .fillMaxSize() + .clickable( + enabled = enabled, + onClick = onSelected + ), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = paymentMethod.iconResourceId), + contentDescription = null, + modifier = Modifier + .width(50.dp) + .padding(horizontal = 16.dp), + alpha = LocalContentAlpha.current, + colorFilter = ColorFilter.tint( + color = if (selected) { + MaterialTheme.colors.primary + } else { + MaterialTheme.colors.onSecondary + } + ) + ) + Text( + text = stringResource(id = paymentMethod.nameResourceId), + modifier = Modifier.padding(end = 16.dp), + color = if (selected) { + MaterialTheme.colors.onPrimary + } else { + MaterialTheme.colors.onSecondary + }, + style = MaterialTheme.typography.h6 + ) + } + } + } +} diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index f4a998cee18..615f70953b3 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -4,9 +4,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.stripe.android.core.Logger +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetLinkResult import com.stripe.android.link.LinkActivityContract import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.LinkScreen import com.stripe.android.link.R import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.confirmation.ConfirmStripeIntentParamsFactory @@ -14,9 +16,12 @@ import com.stripe.android.link.confirmation.ConfirmationManager import com.stripe.android.link.injection.SignedInViewModelSubcomponent import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator +import com.stripe.android.link.model.supportedPaymentMethodTypes import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.getErrorMessage +import com.stripe.android.link.ui.wallet.PaymentDetailsResult +import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.ui.core.FieldValuesToParamsMapConverter import com.stripe.android.ui.core.FormController @@ -67,9 +72,23 @@ internal class PaymentMethodViewModel @Inject constructor( R.string.cancel } - val paymentMethod = SupportedPaymentMethod.Card + val supportedTypes = args.stripeIntent.supportedPaymentMethodTypes(linkAccount) + .let { supportedTypes -> + SupportedPaymentMethod.allValues.filter { supportedTypes.contains(it.type) } + } + + private val _paymentMethod = MutableStateFlow(supportedTypes.first()) + val paymentMethod: StateFlow = _paymentMethod val formController = MutableStateFlow(null) + private val formControllersCache = mutableMapOf() + + private val _financialConnectionsSessionClientSecret = MutableStateFlow(null) + val financialConnectionsSessionClientSecret: StateFlow = + _financialConnectionsSessionClientSecret + + // User must be signed in when Wallet Screen is loaded, so [consumerPublishableKey] is not null + val publishableKey = requireNotNull(linkAccountManager.consumerPublishableKey) fun init(loadFromArgs: Boolean) { val cardMap = args.prefilledCardParams?.toParamMap() @@ -78,39 +97,50 @@ internal class PaymentMethodViewModel @Inject constructor( ?: emptyMap() val initialValuesMap = args.initialFormValuesMap ?: emptyMap() - val combinedMap = cardMap + initialValuesMap - formController.value = - formControllerProvider.get() - .formSpec(LayoutSpec(paymentMethod.formSpec)) - .viewOnlyFields(emptySet()) - .viewModelScope(viewModelScope) - .initialValues(combinedMap) - .stripeIntent(args.stripeIntent) - .merchantName(args.merchantName) - .build().formController + updateFormController(cardMap + initialValuesMap) + } + + fun onPaymentMethodSelected(paymentMethod: SupportedPaymentMethod) { + _paymentMethod.value = paymentMethod + updateFormController() } fun startPayment(formValues: Map) { clearError() setState(PrimaryButtonState.Processing) - val paymentMethodCreateParams = - FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams( - formValues, - paymentMethod.type, - false - ) + when (paymentMethod.value) { + SupportedPaymentMethod.Card -> { + val paymentMethodCreateParams = + FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams( + formValues, + paymentMethod.value.type, + false + ) - viewModelScope.launch { - linkAccountManager.createPaymentDetails( - paymentMethod, - paymentMethodCreateParams, - linkAccount.email, - args.stripeIntent - ).fold( - onSuccess = ::completePayment, - onFailure = ::onError - ) + viewModelScope.launch { + linkAccountManager.createCardPaymentDetails( + paymentMethodCreateParams, + linkAccount.email, + args.stripeIntent + ).fold( + onSuccess = ::completePayment, + onFailure = ::onError + ) + } + } + SupportedPaymentMethod.BankAccount -> { + viewModelScope.launch { + linkAccountManager.createFinancialConnectionsSession() + .mapCatching { requireNotNull(it.clientSecret) } + .fold( + onSuccess = { + _financialConnectionsSessionClientSecret.value = it + }, + onFailure = ::onError + ) + } + } } } @@ -118,7 +148,51 @@ internal class PaymentMethodViewModel @Inject constructor( if (isRootScreen) { payAnotherWay() } else { + navigator.onBack(true) + } + } + + fun onFinancialConnectionsAccountLinked(result: FinancialConnectionsSheetLinkResult) { + when (result) { + is FinancialConnectionsSheetLinkResult.Canceled -> setState(PrimaryButtonState.Enabled) + is FinancialConnectionsSheetLinkResult.Failed -> onError(result.error) + is FinancialConnectionsSheetLinkResult.Completed -> { + viewModelScope.launch { + linkAccountManager.createPaymentDetails(result.linkedAccountId) + .fold( + onSuccess = ::navigateToWallet, + onFailure = ::onError + ) + } + } + } + } + + private fun updateFormController( + initialValues: Map = emptyMap() + ) { + formController.value = + formControllersCache[paymentMethod.value] ?: formControllerProvider.get() + .formSpec(LayoutSpec(paymentMethod.value.formSpec)) + .viewOnlyFields(emptySet()) + .viewModelScope(viewModelScope) + .initialValues(initialValues) + .stripeIntent(args.stripeIntent) + .merchantName(args.merchantName) + .build() + .formController + .also { formControllersCache[paymentMethod.value] = it } + } + + private fun navigateToWallet(selectedAccount: ConsumerPaymentDetails.BankAccount) { + if (navigator.isOnRootScreen() == false) { + navigator.setResult( + PaymentDetailsResult.KEY, + PaymentDetailsResult.Success(selectedAccount.id) + ) navigator.onBack() + } else { + navigator.navigateTo(LinkScreen.Wallet, clearBackStack = true) } } @@ -169,7 +243,7 @@ internal class PaymentMethodViewModel @Inject constructor( private fun setState(state: PrimaryButtonState) { _primaryButtonState.value = state - navigator.backNavigationEnabled = !state.isBlocking + navigator.userNavigationEnabled = !state.isBlocking } internal class Factory( diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt index 6fa975d743e..1a500b92435 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt @@ -1,8 +1,12 @@ package com.stripe.android.link.ui.paymentmethod +import android.content.res.Resources +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.stripe.android.link.R +import com.stripe.android.link.ui.completePaymentButtonLabel import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams -import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.StripeIntent import com.stripe.android.ui.core.elements.FormItemSpec import com.stripe.android.ui.core.forms.LinkCardForm @@ -11,58 +15,58 @@ import com.stripe.android.ui.core.forms.LinkCardForm * * @param type The Payment Method type. Matches the [ConsumerPaymentDetails] types. * @param formSpec Specification of how the payment method data collection UI should look. + * @param nameResourceId String resource id for the name of this payment method. + * @param iconResourceId Drawable resource id for the icon representing this payment method. + * @param primaryButtonStartIconResourceId Drawable resource id for the icon to be displayed at the + * start of the primary button when this payment method is being created. + * @param primaryButtonEndIconResourceId Drawable resource id for the icon to be displayed at the + * end of the primary button when this payment method is being created. */ internal enum class SupportedPaymentMethod( val type: String, - val formSpec: List + val formSpec: List, + @StringRes val nameResourceId: Int, + @DrawableRes val iconResourceId: Int, + @DrawableRes val primaryButtonStartIconResourceId: Int? = null, + @DrawableRes val primaryButtonEndIconResourceId: Int? = null ) { Card( ConsumerPaymentDetails.Card.type, - LinkCardForm.items + LinkCardForm.items, + R.string.stripe_paymentsheet_payment_method_card, + R.drawable.ic_link_card, + primaryButtonEndIconResourceId = R.drawable.stripe_ic_lock ) { - override fun createParams( - paymentMethodCreateParams: PaymentMethodCreateParams, - email: String - ) = ConsumerPaymentDetailsCreateParams.Card( - paymentMethodCreateParams.toParamMap(), - email - ) - - /** - * CVC is not passed during creation, and must be included when confirming the payment. - */ - override fun extraConfirmationParams(paymentMethodCreateParams: PaymentMethodCreateParams) = - (paymentMethodCreateParams.toParamMap()["card"] as? Map<*, *>)?.let { card -> - mapOf("card" to mapOf("cvc" to card["cvc"])) - } + override fun primaryButtonLabel( + stripeIntent: StripeIntent, + resources: Resources + ) = completePaymentButtonLabel(stripeIntent, resources) }, BankAccount( ConsumerPaymentDetails.BankAccount.type, - emptyList() + emptyList(), + R.string.stripe_payment_method_bank, + R.drawable.ic_link_bank, + primaryButtonStartIconResourceId = R.drawable.ic_link_add ) { - override fun createParams( - paymentMethodCreateParams: PaymentMethodCreateParams, - email: String - ): ConsumerPaymentDetailsCreateParams { - TODO("Not yet implemented") - } + override fun primaryButtonLabel( + stripeIntent: StripeIntent, + resources: Resources + ) = resources.getString(R.string.add_bank_account) }; - /** - * Build the [ConsumerPaymentDetailsCreateParams] that will to create this payment method. - */ - abstract fun createParams( - paymentMethodCreateParams: PaymentMethodCreateParams, - email: String - ): ConsumerPaymentDetailsCreateParams + val showsForm = formSpec.isNotEmpty() /** - * A map containing additional parameters that must be sent during payment confirmation. + * The label for the primary button when this payment method is being created. */ - open fun extraConfirmationParams(paymentMethodCreateParams: PaymentMethodCreateParams): - Map? = null + abstract fun primaryButtonLabel( + stripeIntent: StripeIntent, + resources: Resources + ): String internal companion object { - val allTypes = setOf(Card.type, BankAccount.type) + val allValues = listOf(Card, BankAccount) + val allTypes = allValues.map { it.type }.toSet() } } diff --git a/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt b/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt index b589e672970..93be4ef2141 100644 --- a/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt @@ -156,11 +156,12 @@ internal fun SignUpBody( PrimaryButtonState.Enabled } else { PrimaryButtonState.Disabled + }, + onButtonClick = { + onSignUpClick() + keyboardController?.hide() } - ) { - onSignUpClick() - keyboardController?.hide() - } + ) } } } diff --git a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt index 79c9a77ca59..14e5952357d 100644 --- a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt @@ -108,7 +108,7 @@ internal class VerificationViewModel @Inject constructor( fun onBack() { clearError() - navigator.onBack() + navigator.onBack(true) linkEventsReporter.on2FACancel() linkAccountManager.logout() } diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt new file mode 100644 index 00000000000..61fa6cade00 --- /dev/null +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt @@ -0,0 +1,24 @@ +package com.stripe.android.link.ui.wallet + +import android.os.Parcelable +import com.stripe.android.link.ui.ErrorMessage +import kotlinx.parcelize.Parcelize + +/** + * The result of an operation to add or edit a PaymentDetails. + */ +internal sealed class PaymentDetailsResult : Parcelable { + + @Parcelize + class Success(val itemId: String) : PaymentDetailsResult() + + @Parcelize + object Cancelled : PaymentDetailsResult() + + @Parcelize + class Failure(val error: ErrorMessage) : PaymentDetailsResult() + + companion object { + const val KEY = "PaymentDetailsResult" + } +} diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt index f9041c59663..1af3f6e1540 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,13 +48,12 @@ import com.stripe.android.link.ui.PrimaryButton import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.ScrollableTopLevelColumn import com.stripe.android.link.ui.SecondaryButton +import com.stripe.android.link.ui.completePaymentButtonLabel import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod -import com.stripe.android.link.ui.primaryButtonLabel import com.stripe.android.model.CardBrand import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.ui.core.elements.Html import com.stripe.android.ui.core.injection.NonFallbackInjector -import com.stripe.android.ui.core.paymentsColors @Preview @Composable @@ -83,9 +81,11 @@ private fun WalletBodyPreview() { ), supportedTypes = SupportedPaymentMethod.allTypes, selectedItem = null, + isExpanded = true, primaryButtonLabel = "Pay $10.99", primaryButtonState = PrimaryButtonState.Enabled, errorMessage = null, + setExpanded = {}, onItemSelected = {}, onAddNewPaymentMethodClick = {}, onEditPaymentMethod = {}, @@ -115,6 +115,7 @@ internal fun WalletBody( val primaryButtonState by viewModel.primaryButtonState.collectAsState() val selectedItem by viewModel.selectedItem.collectAsState() val errorMessage by viewModel.errorMessage.collectAsState() + val isExpanded by viewModel.isExpanded.collectAsState() if (paymentDetailsList.isEmpty()) { Box( @@ -130,9 +131,14 @@ internal fun WalletBody( paymentDetailsList = paymentDetailsList, supportedTypes = viewModel.supportedTypes, selectedItem = selectedItem, - primaryButtonLabel = primaryButtonLabel(viewModel.args, LocalContext.current.resources), + isExpanded = isExpanded, + primaryButtonLabel = completePaymentButtonLabel( + viewModel.args.stripeIntent, + LocalContext.current.resources + ), primaryButtonState = primaryButtonState, errorMessage = errorMessage, + setExpanded = viewModel::setExpanded, onItemSelected = viewModel::onItemSelected, onAddNewPaymentMethodClick = viewModel::addNewPaymentMethod, onEditPaymentMethod = viewModel::editPaymentMethod, @@ -149,9 +155,11 @@ internal fun WalletBody( paymentDetailsList: List, supportedTypes: Set, selectedItem: ConsumerPaymentDetails.PaymentDetails?, + isExpanded: Boolean, primaryButtonLabel: String, primaryButtonState: PrimaryButtonState, errorMessage: ErrorMessage?, + setExpanded: (Boolean) -> Unit, onItemSelected: (ConsumerPaymentDetails.PaymentDetails) -> Unit, onAddNewPaymentMethodClick: () -> Unit, onEditPaymentMethod: (ConsumerPaymentDetails.PaymentDetails) -> Unit, @@ -161,7 +169,6 @@ internal fun WalletBody( showBottomSheetContent: (BottomSheetContent?) -> Unit ) { val selectedItemIsValid = selectedItem?.let { supportedTypes.contains(it.type) } ?: false - var isWalletExpanded by rememberSaveable { mutableStateOf(!selectedItemIsValid) } var itemBeingRemoved by remember { mutableStateOf(null) } @@ -189,8 +196,8 @@ internal fun WalletBody( ScrollableTopLevelColumn { Spacer(modifier = Modifier.height(12.dp)) - if (isWalletExpanded || !selectedItemIsValid) { - isWalletExpanded = true + if (isExpanded || !selectedItemIsValid) { + setExpanded(true) ExpandedPaymentDetails( paymentDetailsList = paymentDetailsList, supportedTypes = supportedTypes, @@ -198,7 +205,7 @@ internal fun WalletBody( enabled = !primaryButtonState.isBlocking, onItemSelected = { onItemSelected(it) - isWalletExpanded = false + setExpanded(false) }, onMenuButtonClick = { showBottomSheetContent { @@ -220,7 +227,7 @@ internal fun WalletBody( }, onAddNewPaymentMethodClick = onAddNewPaymentMethodClick, onCollapse = { - isWalletExpanded = false + setExpanded(false) } ) } else { @@ -228,7 +235,7 @@ internal fun WalletBody( selectedPaymentMethod = selectedItem!!, enabled = !primaryButtonState.isBlocking, onClick = { - isWalletExpanded = true + setExpanded(true) } ) } @@ -236,7 +243,7 @@ internal fun WalletBody( Html( html = stringResource(R.string.wallet_bank_account_terms), imageGetter = emptyMap(), - color = MaterialTheme.paymentsColors.placeholderText, + color = MaterialTheme.colors.onSecondary, style = MaterialTheme.typography.caption, modifier = Modifier .fillMaxWidth() @@ -260,8 +267,8 @@ internal fun WalletBody( } else { PrimaryButtonState.Disabled }, - icon = R.drawable.stripe_ic_lock, - onButtonClick = onPrimaryButtonClick + onButtonClick = onPrimaryButtonClick, + iconEnd = R.drawable.stripe_ic_lock ) SecondaryButton( enabled = !primaryButtonState.isBlocking, @@ -372,7 +379,7 @@ private fun ExpandedPaymentDetails( // TODO(brnunes-stripe): Use LazyColumn, will need to write custom shape for the border // https://juliensalvi.medium.com/custom-shape-with-jetpack-compose-1cb48a991d42 - paymentDetailsList.forEachIndexed { index, item -> + paymentDetailsList.forEach { item -> PaymentDetailsListItem( paymentDetails = item, enabled = enabled, @@ -395,13 +402,13 @@ private fun ExpandedPaymentDetails( verticalAlignment = Alignment.CenterVertically ) { Icon( - painter = painterResource(id = R.drawable.ic_link_add), + painter = painterResource(id = R.drawable.ic_link_add_green), contentDescription = null, modifier = Modifier.padding(start = HorizontalPadding, end = 12.dp), tint = Color.Unspecified ) Text( - text = stringResource(id = R.string.wallet_add_payment_method), + text = stringResource(id = R.string.add_payment_method), modifier = Modifier.padding(end = HorizontalPadding, bottom = 4.dp), color = MaterialTheme.linkColors.actionLabel, style = MaterialTheme.typography.button diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt index 7ad1b56c045..9a939ed2496 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt @@ -16,7 +16,6 @@ import com.stripe.android.link.model.Navigator import com.stripe.android.link.model.supportedPaymentMethodTypes import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.PrimaryButtonState -import com.stripe.android.link.ui.cardedit.CardEditViewModel import com.stripe.android.link.ui.getErrorMessage import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.payments.paymentlauncher.PaymentResult @@ -47,6 +46,9 @@ internal class WalletViewModel @Inject constructor( requireNotNull(linkAccountManager.linkAccount.value) ) + private val _isExpanded = MutableStateFlow(false) + val isExpanded: StateFlow = _isExpanded + private val _selectedItem = MutableStateFlow(null) val selectedItem: StateFlow = _selectedItem @@ -60,14 +62,14 @@ internal class WalletViewModel @Inject constructor( loadPaymentDetails(true) viewModelScope.launch { - navigator.getResultFlow(CardEditViewModel.Result.KEY) - ?.collect { - when (it) { - CardEditViewModel.Result.Success -> loadPaymentDetails() - CardEditViewModel.Result.Cancelled -> {} - is CardEditViewModel.Result.Failure -> onError(it.error) - } + navigator.getResultFlow(PaymentDetailsResult.KEY)?.collect { + when (it) { + is PaymentDetailsResult.Success -> + loadPaymentDetails(selectedItem = it.itemId) + PaymentDetailsResult.Cancelled -> {} + is PaymentDetailsResult.Failure -> onError(it.error) } + } } } @@ -114,6 +116,10 @@ internal class WalletViewModel @Inject constructor( ) } + fun setExpanded(expanded: Boolean) { + _isExpanded.value = expanded + } + fun payAnotherWay() { navigator.dismiss() linkAccountManager.logout() @@ -148,7 +154,10 @@ internal class WalletViewModel @Inject constructor( _selectedItem.value = item } - private fun loadPaymentDetails(initialSetup: Boolean = false) { + private fun loadPaymentDetails( + initialSetup: Boolean = false, + selectedItem: String? = null + ) { setState(PrimaryButtonState.Processing) viewModelScope.launch { linkAccountManager.listPaymentDetails().fold( @@ -156,11 +165,15 @@ internal class WalletViewModel @Inject constructor( setState(PrimaryButtonState.Enabled) _paymentDetailsList.value = response.paymentDetails - _selectedItem.value = _selectedItem.value?.let { previouslySelectedItem -> - // If currently selected item is still available, keep it selected - response.paymentDetails.firstOrNull { it.id == previouslySelectedItem.id } + // Select selectedItem if provided, otherwise the previously selected item + _selectedItem.value = (selectedItem ?: _selectedItem.value?.id)?.let { itemId -> + response.paymentDetails.firstOrNull { it.id == itemId } } ?: getDefaultItemSelection(response.paymentDetails) + if (_selectedItem.value?.id == selectedItem) { + _isExpanded.value = false + } + if (initialSetup && args.prefilledCardParams != null) { // User has already pre-filled the payment details navigator.navigateTo( @@ -198,7 +211,7 @@ internal class WalletViewModel @Inject constructor( private fun setState(state: PrimaryButtonState) { _primaryButtonState.value = state - navigator.backNavigationEnabled = !state.isBlocking + navigator.userNavigationEnabled = !state.isBlocking } /** diff --git a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt index 4c3bea0803e..ce5de59043c 100644 --- a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt +++ b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt @@ -385,49 +385,92 @@ class LinkAccountManagerTest { } @Test - fun `createPaymentDetails retries on auth error`() = runSuspendTest { + fun `createFinancialConnectionsSession retries on auth error`() = runSuspendTest { val accountManager = accountManager() accountManager.setAccountNullable(mockConsumerSession) - whenever( - linkRepository.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() + whenever(linkRepository.createFinancialConnectionsSession(anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) ) - ).thenReturn( - Result.failure(AuthenticationException(StripeError())), - Result.success(mock()) - ) - accountManager.createPaymentDetails(mock(), mock(), "", mock()) + accountManager.createFinancialConnectionsSession() verify(linkRepository, times(2)) - .createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() + .createFinancialConnectionsSession(anyOrNull(), anyOrNull()) + verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) + + assertThat(accountManager.linkAccount.value).isNotNull() + } + + @Test + fun `createFinancialConnectionsSession does not retry on auth error if no cookie exists`() = + runSuspendTest { + whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever(linkRepository.createFinancialConnectionsSession(anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) + ) + + accountManager.createFinancialConnectionsSession() + + verify(linkRepository) + .createFinancialConnectionsSession(anyOrNull(), anyOrNull()) + verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + } + + @Test + fun `createPaymentDetails for bank account retries on auth error`() = runSuspendTest { + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever(linkRepository.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) ) + + accountManager.createPaymentDetails("") + + verify(linkRepository, times(2)) + .createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) assertThat(accountManager.linkAccount.value).isNotNull() } @Test - fun `createPaymentDetails does not retry on auth error if no cookie exists`() = runSuspendTest { - whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + fun `createPaymentDetails for bank account does not retry on auth error if no cookie exists`() = + runSuspendTest { + whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever(linkRepository.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) + ) + + accountManager.createPaymentDetails("") + + verify(linkRepository) + .createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + } + + @Test + fun `createPaymentDetails for card retries on auth error`() = runSuspendTest { val accountManager = accountManager() accountManager.setAccountNullable(mockConsumerSession) whenever( - linkRepository.createPaymentDetails( - anyOrNull(), + linkRepository.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull(), @@ -439,20 +482,54 @@ class LinkAccountManagerTest { Result.success(mock()) ) - accountManager.createPaymentDetails(mock(), mock(), "", mock()) + accountManager.createCardPaymentDetails(mock(), "", mock()) - verify(linkRepository) - .createPaymentDetails( - anyOrNull(), + verify(linkRepository, times(2)) + .createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull() ) - verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) + + assertThat(accountManager.linkAccount.value).isNotNull() } + @Test + fun `createPaymentDetails for card does not retry on auth error if no cookie exists`() = + runSuspendTest { + whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever( + linkRepository.createCardPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + ).thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) + ) + + accountManager.createCardPaymentDetails(mock(), "", mock()) + + verify(linkRepository) + .createCardPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + } + @Test fun `updatePaymentDetails retries on auth error`() = runSuspendTest { val accountManager = accountManager() diff --git a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt index ed14a079c2f..c06b0153d37 100644 --- a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt +++ b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt @@ -467,8 +467,7 @@ class LinkApiRepositoryTest { val email = "email@stripe.com" val consumerKey = "key" - linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = email, stripeIntent = paymentIntent, @@ -503,8 +502,7 @@ class LinkApiRepositoryTest { val secret = "secret" val email = "email@stripe.com" - linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = email, stripeIntent = paymentIntent, @@ -547,8 +545,7 @@ class LinkApiRepositoryTest { ) .thenReturn(paymentDetails) - val result = linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + val result = linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = email, stripeIntent = paymentIntent, @@ -594,8 +591,7 @@ class LinkApiRepositoryTest { ) .thenThrow(RuntimeException("error")) - val result = linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + val result = linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = "email@stripe.com", stripeIntent = paymentIntent, diff --git a/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt index 86bfdbdb71d..4d2eec148f1 100644 --- a/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt @@ -7,6 +7,7 @@ import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator import com.stripe.android.link.model.PaymentDetailsFixtures +import com.stripe.android.link.ui.wallet.PaymentDetailsResult import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.forms.FormFieldEntry @@ -85,7 +86,7 @@ class CardEditViewModelTest { val viewModel = createViewModel() viewModel.initWithPaymentDetailsId("UNKNOWN_ID") - verify(navigator).setResult(any(), argWhere { it is CardEditViewModel.Result.Failure }) + verify(navigator).setResult(any(), argWhere { it is PaymentDetailsResult.Failure }) } @Test @@ -96,7 +97,7 @@ class CardEditViewModelTest { val viewModel = createViewModel() viewModel.initWithPaymentDetailsId("any") - verify(navigator).setResult(any(), argWhere { it is CardEditViewModel.Result.Failure }) + verify(navigator).setResult(any(), argWhere { it is PaymentDetailsResult.Failure }) } @Test @@ -173,8 +174,8 @@ class CardEditViewModelTest { val viewModel = createAndInitViewModel() viewModel.dismiss() verify(navigator).setResult( - eq(CardEditViewModel.Result.KEY), - argWhere { it is CardEditViewModel.Result.Cancelled } + eq(PaymentDetailsResult.KEY), + argWhere { it is PaymentDetailsResult.Cancelled } ) verify(navigator).onBack() } diff --git a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index f122e73afd8..ca58ac44502 100644 --- a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -7,9 +7,12 @@ import androidx.savedstate.SavedStateRegistryOwner import com.google.common.truth.Truth.assertThat import com.stripe.android.core.Logger import com.stripe.android.core.injection.Injectable +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetLinkResult +import com.stripe.android.financialconnections.model.FinancialConnectionsAccount import com.stripe.android.link.LinkActivityContract import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.LinkScreen import com.stripe.android.link.R import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.confirmation.ConfirmationManager @@ -21,11 +24,15 @@ import com.stripe.android.link.model.PaymentDetailsFixtures import com.stripe.android.link.model.StripeIntentFixtures import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.PrimaryButtonState +import com.stripe.android.link.ui.wallet.PaymentDetailsResult import com.stripe.android.model.ConfirmStripeIntentParams +import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.FinancialConnectionsSession import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.ui.core.FieldValuesToParamsMapConverter import com.stripe.android.ui.core.elements.IdentifierSpec +import com.stripe.android.ui.core.elements.LayoutSpec import com.stripe.android.ui.core.forms.FormFieldEntry import com.stripe.android.ui.core.injection.FormControllerSubcomponent import com.stripe.android.ui.core.injection.NonFallbackInjector @@ -45,6 +52,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argWhere import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.verify @@ -90,8 +98,14 @@ class PaymentMethodViewModelTest { @Before fun before() { Dispatchers.setMain(UnconfinedTestDispatcher()) - linkAccountManager = mock() - whenever(args.stripeIntent).thenReturn(StripeIntentFixtures.PI_SUCCEEDED) + linkAccountManager = mock().apply { + whenever(consumerPublishableKey).thenReturn("consumerPublishableKey") + } + whenever(args.stripeIntent).thenReturn( + StripeIntentFixtures.PI_SUCCEEDED.copy( + linkFundingSources = listOf("card", "bank_account") + ) + ) } @After @@ -100,21 +114,26 @@ class PaymentMethodViewModelTest { } @Test - fun `startPayment creates PaymentDetails`() = runTest { + fun `onPaymentMethodSelected updates form`() { + val viewModel = createViewModel() + assertThat(viewModel.paymentMethod.value).isEqualTo(SupportedPaymentMethod.Card) + verify(formControllerSubcomponentBuilder).formSpec(eq(LayoutSpec(SupportedPaymentMethod.Card.formSpec))) + + viewModel.onPaymentMethodSelected(SupportedPaymentMethod.BankAccount) + assertThat(viewModel.paymentMethod.value).isEqualTo(SupportedPaymentMethod.BankAccount) + verify(formControllerSubcomponentBuilder).formSpec(eq(LayoutSpec(SupportedPaymentMethod.BankAccount.formSpec))) + } + + @Test + fun `startPayment for card creates PaymentDetails`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + linkAccountManager.createCardPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) ).thenReturn(Result.success(createLinkPaymentDetails())) createViewModel().startPayment(cardFormFieldValues) val paramsCaptor = argumentCaptor() - verify(linkAccountManager).createPaymentDetails( - any(), + verify(linkAccountManager).createCardPaymentDetails( paramsCaptor.capture(), any(), anyOrNull() @@ -140,16 +159,11 @@ class PaymentMethodViewModelTest { } @Test - fun `startPayment completes payment when PaymentDetails creation succeeds and completePayment is true`() = + fun `startPayment for card completes payment when PaymentDetails creation succeeds and completePayment is true`() = runTest { val value = createLinkPaymentDetails() whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + linkAccountManager.createCardPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) ).thenReturn(Result.success(value)) createViewModel().startPayment(cardFormFieldValues) @@ -186,14 +200,9 @@ class PaymentMethodViewModelTest { } @Test - fun `startPayment dismisses Link on success`() = runTest { + fun `startPayment for card dismisses Link on success`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + linkAccountManager.createCardPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) ).thenReturn(Result.success(createLinkPaymentDetails())) var callback: PaymentConfirmationCallback? = null @@ -220,14 +229,9 @@ class PaymentMethodViewModelTest { } @Test - fun `startPayment starts processing`() = runTest { + fun `startPayment for card starts processing`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + linkAccountManager.createCardPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) ).thenReturn(Result.success(createLinkPaymentDetails())) val viewModel = createViewModel() @@ -243,14 +247,9 @@ class PaymentMethodViewModelTest { } @Test - fun `startPayment stops processing on error`() = runTest { + fun `startPayment for card stops processing on error`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + linkAccountManager.createCardPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) ).thenReturn(Result.success(createLinkPaymentDetails())) var callback: PaymentConfirmationCallback? = null @@ -282,12 +281,7 @@ class PaymentMethodViewModelTest { fun `when startPayment fails then an error message is shown`() = runTest { val errorMessage = "Error message" whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + linkAccountManager.createCardPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) ).thenReturn(Result.failure(RuntimeException(errorMessage))) val viewModel = createViewModel() @@ -297,6 +291,78 @@ class PaymentMethodViewModelTest { assertThat(viewModel.errorMessage.value).isEqualTo(ErrorMessage.Raw(errorMessage)) } + @Test + fun `startPayment for bank account creates FinancialConnectionsSession`() = runTest { + val clientSecret = "secret" + whenever(linkAccountManager.createFinancialConnectionsSession()).thenReturn( + Result.success(FinancialConnectionsSession(clientSecret, "id")) + ) + val viewModel = createViewModel() + viewModel.onPaymentMethodSelected(SupportedPaymentMethod.BankAccount) + viewModel.startPayment(emptyMap()) + + assertThat(viewModel.financialConnectionsSessionClientSecret.value).isEqualTo(clientSecret) + } + + @Test + fun `onFinancialConnectionsAccountLinked cancelled then state is reset`() = runTest { + val viewModel = createViewModel() + viewModel.onFinancialConnectionsAccountLinked( + FinancialConnectionsSheetLinkResult.Canceled + ) + + assertThat(viewModel.primaryButtonState.value).isEqualTo(PrimaryButtonState.Enabled) + } + + @Test + fun `onFinancialConnectionsAccountLinked error then shows error message`() = runTest { + val errorMessage = "error" + + val viewModel = createViewModel() + viewModel.onFinancialConnectionsAccountLinked( + FinancialConnectionsSheetLinkResult.Failed(Exception(errorMessage)) + ) + + assertThat(viewModel.errorMessage.value).isEqualTo(ErrorMessage.Raw(errorMessage)) + } + + @Test + fun `when account linked at root screen then it navigates to wallet`() = runTest { + val sessionId = "session_id" + val account = mock() + whenever(linkAccountManager.createPaymentDetails(any())).thenReturn(Result.success(account)) + whenever(navigator.isOnRootScreen()).thenReturn(true) + + val viewModel = createViewModel() + viewModel.onFinancialConnectionsAccountLinked( + FinancialConnectionsSheetLinkResult.Completed(sessionId) + ) + + verify(navigator).navigateTo(LinkScreen.Wallet, true) + } + + @Test + fun `when account linked not at root screen then result is set`() = runTest { + val sessionId = "session_id" + val accountId = "account_id" + val account = mock().apply { + whenever(id).thenReturn(accountId) + } + whenever(linkAccountManager.createPaymentDetails(any())).thenReturn(Result.success(account)) + whenever(navigator.isOnRootScreen()).thenReturn(false) + + val viewModel = createViewModel() + viewModel.onFinancialConnectionsAccountLinked( + FinancialConnectionsSheetLinkResult.Completed(sessionId) + ) + + verify(navigator).setResult( + eq(PaymentDetailsResult.KEY), + argWhere { it is PaymentDetailsResult.Success && it.itemId == accountId } + ) + verify(navigator).onBack(any()) + } + @Test fun `when loading from arguments then form is prefilled`() = runTest { whenever(args.prefilledCardParams).thenReturn(createLinkPaymentDetails().originalParams) @@ -338,7 +404,7 @@ class PaymentMethodViewModelTest { createViewModel().onSecondaryButtonClick() - verify(navigator).onBack() + verify(navigator).onBack(true) } @Test @@ -414,6 +480,14 @@ class PaymentMethodViewModelTest { ) } + private fun createFinancialConnectionsAccount(id: String = "id") = FinancialConnectionsAccount( + created = 1, + id = id, + institutionName = "name", + livemode = false, + supportedPaymentMethodTypes = listOf() + ) + companion object { const val CLIENT_SECRET = "client_secret" } diff --git a/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt index 846b5ebb0f0..397dfcd78a5 100644 --- a/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt @@ -19,7 +19,6 @@ import com.stripe.android.link.model.PaymentDetailsFixtures import com.stripe.android.link.model.StripeIntentFixtures import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.PrimaryButtonState -import com.stripe.android.link.ui.cardedit.CardEditViewModel import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmStripeIntentParams import com.stripe.android.model.ConsumerPaymentDetails @@ -82,7 +81,7 @@ class WalletViewModelTest { fun `On initialization start collecting CardEdit result`() = runTest { createViewModel() - verify(navigator).getResultFlow(any()) + verify(navigator).getResultFlow(any()) } @Test @@ -320,26 +319,26 @@ class WalletViewModelTest { @Test fun `On CardEdit result successful then it reloads payment details`() = runTest { - val flow = MutableStateFlow(null) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val flow = MutableStateFlow(null) + whenever(navigator.getResultFlow(any())).thenReturn(flow) createViewModel() verify(linkAccountManager).listPaymentDetails() clearInvocations(linkAccountManager) - flow.emit(CardEditViewModel.Result.Success) + flow.emit(PaymentDetailsResult.Success("")) verify(linkAccountManager).listPaymentDetails() } @Test fun `On CardEdit result failure then it shows error`() = runTest { - val flow = MutableStateFlow(null) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val flow = MutableStateFlow(null) + whenever(navigator.getResultFlow(any())).thenReturn(flow) val viewModel = createViewModel() val error = ErrorMessage.Raw("Error message") - flow.emit(CardEditViewModel.Result.Failure(error)) + flow.emit(PaymentDetailsResult.Failure(error)) assertThat(viewModel.errorMessage.value).isEqualTo(error) } diff --git a/payments-core/api/payments-core.api b/payments-core/api/payments-core.api index bed84e62f89..b523e045100 100644 --- a/payments-core/api/payments-core.api +++ b/payments-core/api/payments-core.api @@ -2545,12 +2545,17 @@ public abstract class com/stripe/android/model/ConsumerPaymentDetailsCreateParam public final class com/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card : com/stripe/android/model/ConsumerPaymentDetailsCreateParams { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; + public static final field Companion Lcom/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card$Companion; public fun (Ljava/util/Map;Ljava/lang/String;)V public fun describeContents ()I public fun toParamMap ()Ljava/util/Map; public fun writeToParcel (Landroid/os/Parcel;I)V } +public final class com/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card$Companion { + public final fun extraConfirmationParams (Lcom/stripe/android/model/PaymentMethodCreateParams;)Ljava/util/Map; +} + public final class com/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card; diff --git a/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt b/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt index 757b6410f43..34c10892289 100644 --- a/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt +++ b/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt @@ -43,5 +43,16 @@ sealed class ConsumerPaymentDetailsCreateParams( } return params } + + companion object { + /** + * A map containing additional parameters that must be sent during payment confirmation. + * CVC is not passed during creation, and must be included when confirming the payment. + */ + fun extraConfirmationParams(paymentMethodCreateParams: PaymentMethodCreateParams) = + (paymentMethodCreateParams.toParamMap()["card"] as? Map<*, *>)?.let { card -> + mapOf("card" to mapOf("cvc" to card["cvc"])) + } + } } } diff --git a/payments-ui-core/res/values/totranslate.xml b/payments-ui-core/res/values/totranslate.xml index 71090027d63..f3524edb51e 100644 --- a/payments-ui-core/res/values/totranslate.xml +++ b/payments-ui-core/res/values/totranslate.xml @@ -14,4 +14,7 @@ Shipping Address Save Address Billing address is same as shipping + + + Bank From e9e17fe5bfc5112998c863bc93ea263cbe062adf Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Wed, 24 Aug 2022 16:38:25 -0700 Subject: [PATCH 08/22] WalletScreenTest --- .../api/financial-connections.api | 41 ------------------- .../paymentmethod/PaymentMethodScreenTest.kt | 6 +++ .../link/ui/wallet/WalletScreenTest.kt | 35 ++++++++++++++-- 3 files changed, 38 insertions(+), 44 deletions(-) diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index 8f673f1d8bd..6f5434a5201 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -333,24 +333,6 @@ public final class com/stripe/android/financialconnections/domain/GenerateFinanc public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$Companion { } -public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$Companion { -} - -public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public fun (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)V - public final fun component1 ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; - public final fun copy (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; - public static synthetic fun copy$default (Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData;Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;ILjava/lang/Object;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public fun getConfiguration ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForData; @@ -367,29 +349,6 @@ public final class com/stripe/android/financialconnections/launcher/FinancialCon public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForLink; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken : com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public fun (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)V - public final fun component1 ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; - public final fun copy (Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; - public static synthetic fun copy$default (Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken;Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration;ILjava/lang/Object;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public fun getConfiguration ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$Configuration; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$ForToken; diff --git a/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt b/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt index ec67ddab33a..e74304657a7 100644 --- a/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt +++ b/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt @@ -92,17 +92,23 @@ internal class PaymentMethodScreenTest { } private fun setContent( + supportedPaymentMethods: List = SupportedPaymentMethod.allValues, + selectedPaymentMethod: SupportedPaymentMethod = SupportedPaymentMethod.Card, primaryButtonState: PrimaryButtonState = PrimaryButtonState.Enabled, errorMessage: ErrorMessage? = null, + onPaymentMethodSelected: (SupportedPaymentMethod) -> Unit = {}, onPayButtonClick: () -> Unit = {}, onSecondaryButtonClick: () -> Unit = {} ) = composeTestRule.setContent { DefaultLinkTheme { PaymentMethodBody( + supportedPaymentMethods = supportedPaymentMethods, + selectedPaymentMethod = selectedPaymentMethod, primaryButtonLabel = primaryButtonLabel, primaryButtonState = primaryButtonState, secondaryButtonLabel = secondaryButtonLabel, errorMessage = errorMessage, + onPaymentMethodSelected = onPaymentMethodSelected, onPrimaryButtonClick = onPayButtonClick, onSecondaryButtonClick = onSecondaryButtonClick, formContent = {} diff --git a/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt b/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt index ebb8df08443..85881d12023 100644 --- a/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt +++ b/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt @@ -97,7 +97,10 @@ internal class WalletScreenTest { @Test fun selected_payment_method_is_shown_when_collapsed() { val initiallySelectedItem = paymentDetails[4] - setContent(selectedItem = initiallySelectedItem) + setContent( + isExpanded = false, + selectedItem = initiallySelectedItem + ) composeTestRule.onNodeWithText("Payment").onParent().onChildren() .filter(hasText(initiallySelectedItem.label, substring = true)) @@ -105,9 +108,31 @@ internal class WalletScreenTest { } @Test - fun when_no_payment_option_is_selected_then_list_is_expanded() { - setContent(selectedItem = null) + fun expand_list_triggers_callback() { + var expanded: Boolean? = null + setContent( + isExpanded = false, + setExpanded = { + expanded = it + } + ) + assertCollapsed() + composeTestRule.onNodeWithText("Payment").performClick() + assertThat(expanded).isTrue() + } + + @Test + fun collapse_list_triggers_callback() { + var expanded: Boolean? = null + setContent( + isExpanded = true, + setExpanded = { + expanded = it + } + ) assertExpanded() + composeTestRule.onNodeWithText("Payment methods").performClick() + assertThat(expanded).isFalse() } @Test @@ -357,8 +382,10 @@ internal class WalletScreenTest { private fun setContent( supportedTypes: Set = SupportedPaymentMethod.allTypes, selectedItem: ConsumerPaymentDetails.PaymentDetails? = paymentDetails.first(), + isExpanded: Boolean = true, primaryButtonState: PrimaryButtonState = PrimaryButtonState.Enabled, errorMessage: ErrorMessage? = null, + setExpanded: (Boolean) -> Unit = {}, onItemSelected: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {}, onAddNewPaymentMethodClick: () -> Unit = {}, onEditPaymentMethod: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {}, @@ -395,9 +422,11 @@ internal class WalletScreenTest { paymentDetailsList = paymentDetailsList, supportedTypes = supportedTypes, selectedItem = selectedItem, + isExpanded = isExpanded, primaryButtonLabel = primaryButtonLabel, primaryButtonState = primaryButtonState, errorMessage = errorMessage, + setExpanded = setExpanded, onItemSelected = onItemSelected, onAddNewPaymentMethodClick = onAddNewPaymentMethodClick, onEditPaymentMethod = onEditPaymentMethod, From bca8508c64183b5fdd38f9d823b7372f7f998c70 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Wed, 24 Aug 2022 16:40:59 -0700 Subject: [PATCH 09/22] createBankAccountPaymentDetails --- .../com/stripe/android/link/account/LinkAccountManager.kt | 2 +- .../stripe/android/link/repositories/LinkApiRepository.kt | 2 +- .../stripe/android/link/repositories/LinkRepository.kt | 2 +- .../stripe/android/link/account/LinkAccountManagerTest.kt | 8 ++++---- .../android/link/repositories/LinkApiRepositoryTest.kt | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt index b28607e1650..64d8b6ab295 100644 --- a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt +++ b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt @@ -233,7 +233,7 @@ internal class LinkAccountManager @Inject constructor( suspend fun createPaymentDetails( financialConnectionsAccountId: String ) = retryingOnAuthError { clientSecret -> - linkRepository.createPaymentDetails( + linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId, clientSecret, consumerPublishableKey diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt index 9b494bd14b7..882687da4b7 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt @@ -181,7 +181,7 @@ internal class LinkApiRepository @Inject constructor( } } - override suspend fun createPaymentDetails( + override suspend fun createBankAccountPaymentDetails( financialConnectionsAccountId: String, consumerSessionClientSecret: String, consumerPublishableKey: String? diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt index 1bf95b4a041..6ad9da981bd 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt @@ -80,7 +80,7 @@ internal interface LinkRepository { /** * Create a new linked bank account payment method in the consumer account. */ - suspend fun createPaymentDetails( + suspend fun createBankAccountPaymentDetails( financialConnectionsAccountId: String, consumerSessionClientSecret: String, consumerPublishableKey: String? diff --git a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt index ce5de59043c..24de3699dd6 100644 --- a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt +++ b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt @@ -429,7 +429,7 @@ class LinkAccountManagerTest { val accountManager = accountManager() accountManager.setAccountNullable(mockConsumerSession) - whenever(linkRepository.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) + whenever(linkRepository.createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn( Result.failure(AuthenticationException(StripeError())), Result.success(mock()) @@ -438,7 +438,7 @@ class LinkAccountManagerTest { accountManager.createPaymentDetails("") verify(linkRepository, times(2)) - .createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + .createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) assertThat(accountManager.linkAccount.value).isNotNull() @@ -451,7 +451,7 @@ class LinkAccountManagerTest { val accountManager = accountManager() accountManager.setAccountNullable(mockConsumerSession) - whenever(linkRepository.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) + whenever(linkRepository.createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn( Result.failure(AuthenticationException(StripeError())), Result.success(mock()) @@ -460,7 +460,7 @@ class LinkAccountManagerTest { accountManager.createPaymentDetails("") verify(linkRepository) - .createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + .createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) } diff --git a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt index c06b0153d37..e27eb1f094f 100644 --- a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt +++ b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt @@ -409,7 +409,7 @@ class LinkApiRepositoryTest { val secret = "secret" val consumerKey = "key" - linkRepository.createPaymentDetails( + linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId = accountId, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey @@ -433,7 +433,7 @@ class LinkApiRepositoryTest { whenever(stripeRepository.createPaymentDetails(any(), any(), any())) .thenReturn(paymentDetails) - val result = linkRepository.createPaymentDetails( + val result = linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId = accountId, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey @@ -452,7 +452,7 @@ class LinkApiRepositoryTest { whenever(stripeRepository.createPaymentDetails(any(), any(), any())) .thenThrow(RuntimeException("error")) - val result = linkRepository.createPaymentDetails( + val result = linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId = accountId, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey From 36a2200a8026a017ab23f16381513dd3cc8bc50a Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Wed, 24 Aug 2022 19:32:01 -0700 Subject: [PATCH 10/22] PaymentMethodScreenTest --- .../paymentmethod/PaymentMethodScreenTest.kt | 33 +++++++++++++++++++ .../link/account/LinkAccountManager.kt | 2 +- .../paymentmethod/PaymentMethodViewModel.kt | 2 +- .../link/account/LinkAccountManagerTest.kt | 4 +-- .../PaymentMethodViewModelTest.kt | 4 +-- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt b/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt index e74304657a7..ffba2090c6c 100644 --- a/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt +++ b/link/src/androidTest/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt @@ -24,6 +24,37 @@ internal class PaymentMethodScreenTest { private val primaryButtonLabel = "Pay $10.99" private val secondaryButtonLabel = "Cancel" + @Test + fun when_multiple_payment_methods_supported_then_shows_them_in_cells() { + setContent() + + onCard().assertExists() + onBank().assertExists() + } + + @Test + fun when_single_payment_method_supported_then_shows_no_cells() { + setContent(supportedPaymentMethods = listOf(SupportedPaymentMethod.Card)) + + onCard().assertDoesNotExist() + onBank().assertDoesNotExist() + } + + @Test + fun selecting_payment_method_triggers_callback() { + var selectedPaymentMethod: SupportedPaymentMethod? = null + setContent( + onPaymentMethodSelected = { + selectedPaymentMethod = it + } + ) + + onBank().performClick() + assertThat(selectedPaymentMethod).isEqualTo(SupportedPaymentMethod.BankAccount) + onCard().performClick() + assertThat(selectedPaymentMethod).isEqualTo(SupportedPaymentMethod.Card) + } + @Test fun primary_button_shows_progress_indicator_when_processing() { setContent(primaryButtonState = PrimaryButtonState.Processing) @@ -120,4 +151,6 @@ internal class PaymentMethodScreenTest { private fun onSecondaryButton() = composeTestRule.onNodeWithText(secondaryButtonLabel) private fun onProgressIndicator() = composeTestRule.onNodeWithTag(progressIndicatorTestTag) private fun onCompletedIcon() = composeTestRule.onNodeWithTag(completedIconTestTag, true) + private fun onCard() = composeTestRule.onNodeWithText("Card") + private fun onBank() = composeTestRule.onNodeWithText("Bank") } diff --git a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt index 64d8b6ab295..bc1bd79c5b9 100644 --- a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt +++ b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt @@ -230,7 +230,7 @@ internal class LinkAccountManager @Inject constructor( /** * Create a new Bank Account payment method attached to the consumer account. */ - suspend fun createPaymentDetails( + suspend fun createBankAccountPaymentDetails( financialConnectionsAccountId: String ) = retryingOnAuthError { clientSecret -> linkRepository.createBankAccountPaymentDetails( diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index 615f70953b3..a063098f76f 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -158,7 +158,7 @@ internal class PaymentMethodViewModel @Inject constructor( is FinancialConnectionsSheetLinkResult.Failed -> onError(result.error) is FinancialConnectionsSheetLinkResult.Completed -> { viewModelScope.launch { - linkAccountManager.createPaymentDetails(result.linkedAccountId) + linkAccountManager.createBankAccountPaymentDetails(result.linkedAccountId) .fold( onSuccess = ::navigateToWallet, onFailure = ::onError diff --git a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt index 24de3699dd6..5fcee2a0ffc 100644 --- a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt +++ b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt @@ -435,7 +435,7 @@ class LinkAccountManagerTest { Result.success(mock()) ) - accountManager.createPaymentDetails("") + accountManager.createBankAccountPaymentDetails("") verify(linkRepository, times(2)) .createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) @@ -457,7 +457,7 @@ class LinkAccountManagerTest { Result.success(mock()) ) - accountManager.createPaymentDetails("") + accountManager.createBankAccountPaymentDetails("") verify(linkRepository) .createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) diff --git a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index ca58ac44502..0d4ff3961a6 100644 --- a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -330,7 +330,7 @@ class PaymentMethodViewModelTest { fun `when account linked at root screen then it navigates to wallet`() = runTest { val sessionId = "session_id" val account = mock() - whenever(linkAccountManager.createPaymentDetails(any())).thenReturn(Result.success(account)) + whenever(linkAccountManager.createBankAccountPaymentDetails(any())).thenReturn(Result.success(account)) whenever(navigator.isOnRootScreen()).thenReturn(true) val viewModel = createViewModel() @@ -348,7 +348,7 @@ class PaymentMethodViewModelTest { val account = mock().apply { whenever(id).thenReturn(accountId) } - whenever(linkAccountManager.createPaymentDetails(any())).thenReturn(Result.success(account)) + whenever(linkAccountManager.createBankAccountPaymentDetails(any())).thenReturn(Result.success(account)) whenever(navigator.isOnRootScreen()).thenReturn(false) val viewModel = createViewModel() From 6d45936146c87486e0b5a182cf41cda987976c35 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 25 Aug 2022 13:26:16 -0700 Subject: [PATCH 11/22] Refactoring to support Bank Accounts in Link --- link/res/drawable/ic_link_add.xml | 19 +-- link/res/drawable/ic_link_add_green.xml | 16 ++ link/res/drawable/ic_link_bank.xml | 8 +- link/res/drawable/ic_link_card.xml | 9 ++ link/res/values/strings.xml | 4 +- .../android/link/LinkPaymentLauncher.kt | 3 +- .../link/account/LinkAccountManager.kt | 41 ++++-- .../stripe/android/link/model/Navigator.kt | 10 +- .../link/repositories/LinkApiRepository.kt | 15 +- .../link/repositories/LinkRepository.kt | 15 +- .../com/stripe/android/link/theme/Theme.kt | 1 + .../stripe/android/link/ui/PrimaryButton.kt | 80 ++++++---- .../link/ui/cardedit/CardEditViewModel.kt | 28 +--- .../ui/paymentmethod/PaymentMethodBody.kt | 12 +- .../paymentmethod/PaymentMethodViewModel.kt | 5 +- .../paymentmethod/SupportedPaymentMethod.kt | 78 +++++----- .../android/link/ui/signup/SignUpScreen.kt | 9 +- .../ui/verification/VerificationViewModel.kt | 2 +- .../link/ui/wallet/PaymentDetailsResult.kt | 24 +++ .../android/link/ui/wallet/WalletScreen.kt | 13 +- .../android/link/ui/wallet/WalletViewModel.kt | 11 +- .../link/account/LinkAccountManagerTest.kt | 137 ++++++++++++++---- .../repositories/LinkApiRepositoryTest.kt | 18 +-- .../link/ui/cardedit/CardEditViewModelTest.kt | 9 +- .../link/ui/wallet/WalletViewModelTest.kt | 15 +- payments-core/api/payments-core.api | 5 + .../ConsumerPaymentDetailsCreateParams.kt | 11 ++ payments-ui-core/res/values/totranslate.xml | 3 + 28 files changed, 387 insertions(+), 214 deletions(-) create mode 100644 link/res/drawable/ic_link_add_green.xml create mode 100644 link/res/drawable/ic_link_card.xml create mode 100644 link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt diff --git a/link/res/drawable/ic_link_add.xml b/link/res/drawable/ic_link_add.xml index 57e7183ca39..8afe25acf9b 100644 --- a/link/res/drawable/ic_link_add.xml +++ b/link/res/drawable/ic_link_add.xml @@ -1,16 +1,9 @@ + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> - + android:pathData="M8.75,0.75C8.75,0.336 8.414,0 8,0C7.586,0 7.25,0.336 7.25,0.75V7.25H0.75C0.336,7.25 0,7.586 0,8C0,8.414 0.336,8.75 0.75,8.75H7.25V15.25C7.25,15.664 7.586,16 8,16C8.414,16 8.75,15.664 8.75,15.25V8.75H15.25C15.664,8.75 16,8.414 16,8C16,7.586 15.664,7.25 15.25,7.25H8.75V0.75Z" + android:fillColor="#1D3944"/> diff --git a/link/res/drawable/ic_link_add_green.xml b/link/res/drawable/ic_link_add_green.xml new file mode 100644 index 00000000000..57e7183ca39 --- /dev/null +++ b/link/res/drawable/ic_link_add_green.xml @@ -0,0 +1,16 @@ + + + + diff --git a/link/res/drawable/ic_link_bank.xml b/link/res/drawable/ic_link_bank.xml index 71593285b99..79ed3f998e8 100644 --- a/link/res/drawable/ic_link_bank.xml +++ b/link/res/drawable/ic_link_bank.xml @@ -1,9 +1,9 @@ + android:pathData="M1.328,5.047C1.328,5.43 1.625,5.797 2.133,5.797H16.852C17.359,5.797 17.656,5.43 17.656,5.047C17.656,4.766 17.5,4.539 17.164,4.328L10.508,0.492C10.18,0.305 9.828,0.203 9.492,0.203C9.156,0.203 8.805,0.305 8.477,0.492L1.82,4.328C1.484,4.539 1.328,4.766 1.328,5.047ZM2.359,14.055C2.359,14.43 2.578,14.648 2.961,14.648H4.961C5.344,14.648 5.563,14.43 5.563,14.055V13.844C5.563,13.477 5.344,13.258 4.961,13.258H4.664V7.789H4.961C5.344,7.789 5.563,7.57 5.563,7.195V6.984C5.563,6.609 5.344,6.391 4.961,6.391H2.961C2.578,6.391 2.359,6.609 2.359,6.984V7.195C2.359,7.57 2.578,7.789 2.961,7.789H3.273V13.258H2.961C2.578,13.258 2.359,13.477 2.359,13.844V14.055ZM6.078,14.055C6.078,14.43 6.305,14.648 6.68,14.648H8.688C9.063,14.648 9.281,14.43 9.281,14.055V13.844C9.281,13.477 9.063,13.258 8.688,13.258H8.383V7.789H8.688C9.063,7.789 9.281,7.57 9.281,7.195V6.984C9.281,6.609 9.063,6.391 8.688,6.391H6.68C6.305,6.391 6.078,6.609 6.078,6.984V7.195C6.078,7.57 6.305,7.789 6.68,7.789H6.992V13.258H6.68C6.305,13.258 6.078,13.477 6.078,13.844V14.055ZM9.813,14.055C9.813,14.43 10.031,14.648 10.406,14.648H12.414C12.789,14.648 13.016,14.43 13.016,14.055V13.844C13.016,13.477 12.789,13.258 12.414,13.258H12.117V7.789H12.414C12.789,7.789 13.016,7.57 13.016,7.195V6.984C13.016,6.609 12.789,6.391 12.414,6.391H10.406C10.031,6.391 9.813,6.609 9.813,6.984V7.195C9.813,7.57 10.031,7.789 10.406,7.789H10.719V13.258H10.406C10.031,13.258 9.813,13.477 9.813,13.844V14.055ZM13.531,14.055C13.531,14.43 13.75,14.648 14.133,14.648H16.133C16.516,14.648 16.734,14.43 16.734,14.055V13.844C16.734,13.477 16.516,13.258 16.133,13.258H15.836V7.789H16.133C16.516,7.789 16.734,7.57 16.734,7.195V6.984C16.734,6.609 16.516,6.391 16.133,6.391H14.133C13.75,6.391 13.531,6.609 13.531,6.984V7.195C13.531,7.57 13.75,7.789 14.133,7.789H14.438V13.258H14.133C13.75,13.258 13.531,13.477 13.531,13.844V14.055ZM0.906,16C0.906,16.406 1.242,16.742 1.656,16.742H17.344C17.75,16.742 18.086,16.406 18.086,16C18.086,15.586 17.75,15.25 17.344,15.25H1.656C1.242,15.25 0.906,15.586 0.906,16Z" + android:fillColor="#6A7383"/> diff --git a/link/res/drawable/ic_link_card.xml b/link/res/drawable/ic_link_card.xml new file mode 100644 index 00000000000..826b19e80ca --- /dev/null +++ b/link/res/drawable/ic_link_card.xml @@ -0,0 +1,9 @@ + + + diff --git a/link/res/values/strings.xml b/link/res/values/strings.xml index 4161d3c0c92..cc5aea9a6ae 100644 --- a/link/res/values/strings.xml +++ b/link/res/values/strings.xml @@ -31,10 +31,10 @@ Are you sure you want to remove this account? By continuing, you agree to authorize payments pursuant to <a href=\"https://stripe.com/legal/ach-payments/authorization\">these terms</a>. - Add a payment method + Add a payment method + Add bank account Pay another way - Add a new card This is your default Set as default payment diff --git a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt index 90aabc6eff5..6a42ff280d5 100644 --- a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt +++ b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt @@ -183,8 +183,7 @@ class LinkPaymentLauncher @AssistedInject internal constructor( suspend fun attachNewCardToAccount( paymentMethodCreateParams: PaymentMethodCreateParams ): Result = - linkAccountManager.createPaymentDetails( - SupportedPaymentMethod.Card, + linkAccountManager.createCardPaymentDetails( paymentMethodCreateParams ) diff --git a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt index 2f9935c7230..bc1bd79c5b9 100644 --- a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt +++ b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt @@ -10,7 +10,6 @@ import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.repositories.LinkRepository import com.stripe.android.link.ui.inline.UserInput -import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.PaymentMethodCreateParams @@ -41,7 +40,6 @@ internal class LinkAccountManager @Inject constructor( /** * The publishable key for the signed in Link account. */ - @VisibleForTesting var consumerPublishableKey: String? = null val accountStatus = linkAccount.transform { value -> @@ -201,18 +199,16 @@ internal class LinkAccountManager @Inject constructor( } /** - * Creates a new PaymentDetails attached to the current account. + * Creates a new PaymentDetails.Card attached to the current account. * * @return The parameters needed to confirm the current Stripe Intent using the newly created * Payment Details. */ - suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams ): Result = linkAccount.value?.let { account -> - createPaymentDetails( - paymentMethod, + createCardPaymentDetails( paymentMethodCreateParams, account.email, stripeIntent @@ -222,16 +218,37 @@ internal class LinkAccountManager @Inject constructor( ) /** - * Create a new payment method in the signed in consumer account. + * Create a session used to connect a bank account through Financial Connections. + */ + suspend fun createFinancialConnectionsSession() = retryingOnAuthError { clientSecret -> + linkRepository.createFinancialConnectionsSession( + clientSecret, + consumerPublishableKey + ) + } + + /** + * Create a new Bank Account payment method attached to the consumer account. + */ + suspend fun createBankAccountPaymentDetails( + financialConnectionsAccountId: String + ) = retryingOnAuthError { clientSecret -> + linkRepository.createBankAccountPaymentDetails( + financialConnectionsAccountId, + clientSecret, + consumerPublishableKey + ) + } + + /** + * Create a new Card payment method attached to the consumer account. */ - suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams, userEmail: String, stripeIntent: StripeIntent ) = retryingOnAuthError { clientSecret -> - linkRepository.createPaymentDetails( - paymentMethod, + linkRepository.createCardPaymentDetails( paymentMethodCreateParams, userEmail, stripeIntent, diff --git a/link/src/main/java/com/stripe/android/link/model/Navigator.kt b/link/src/main/java/com/stripe/android/link/model/Navigator.kt index 06bf6e66025..66a8690d41b 100644 --- a/link/src/main/java/com/stripe/android/link/model/Navigator.kt +++ b/link/src/main/java/com/stripe/android/link/model/Navigator.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton */ @Singleton internal class Navigator @Inject constructor() { - var backNavigationEnabled = true + var userNavigationEnabled = true var navigationController: NavHostController? = null var onDismiss: ((LinkActivityResult) -> Unit)? = null @@ -41,10 +41,12 @@ internal class Navigator @Inject constructor() { /** * Behaves like a back button, popping the back stack and dismissing the Activity if this was * the last screen. - * Only performs any action if [backNavigationEnabled] is true. + * When [userInitiated] is true, only performs any action if [userNavigationEnabled] is true. + * + * @param userInitiated Whether the action was initiated by user interaction. */ - fun onBack() { - if (backNavigationEnabled) { + fun onBack(userInitiated: Boolean = false) { + if (!userInitiated || userNavigationEnabled) { navigationController?.let { navController -> if (!navController.popBackStack()) { dismiss() diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt index 26c685237a8..882687da4b7 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt @@ -8,6 +8,7 @@ import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.confirmation.ConfirmStripeIntentParamsFactory import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.ConsumerSessionLookup @@ -180,7 +181,7 @@ internal class LinkApiRepository @Inject constructor( } } - override suspend fun createPaymentDetails( + override suspend fun createBankAccountPaymentDetails( financialConnectionsAccountId: String, consumerSessionClientSecret: String, consumerPublishableKey: String? @@ -203,8 +204,7 @@ internal class LinkApiRepository @Inject constructor( } } - override suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + override suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams, userEmail: String, stripeIntent: StripeIntent, @@ -215,7 +215,10 @@ internal class LinkApiRepository @Inject constructor( requireNotNull( stripeRepository.createPaymentDetails( consumerSessionClientSecret, - paymentMethod.createParams(paymentMethodCreateParams, userEmail), + ConsumerPaymentDetailsCreateParams.Card( + paymentMethodCreateParams.toParamMap(), + userEmail + ), consumerPublishableKey?.let { ApiRequest.Options(it) } ?: ApiRequest.Options( @@ -229,7 +232,9 @@ internal class LinkApiRepository @Inject constructor( .createPaymentMethodCreateParams( consumerSessionClientSecret, it, - paymentMethod.extraConfirmationParams(paymentMethodCreateParams) + ConsumerPaymentDetailsCreateParams.Card.extraConfirmationParams( + paymentMethodCreateParams + ) ), paymentMethodCreateParams ) diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt index 89a2b129bb9..6ad9da981bd 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt @@ -1,7 +1,6 @@ package com.stripe.android.link.repositories import com.stripe.android.link.LinkPaymentDetails -import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession @@ -69,22 +68,28 @@ internal interface LinkRepository { consumerPublishableKey: String? ): Result + /** + * Create a new [FinancialConnectionsSession], used to link a bank account using the Financial + * Connections SDK. + */ suspend fun createFinancialConnectionsSession( consumerSessionClientSecret: String, consumerPublishableKey: String? ): Result - suspend fun createPaymentDetails( + /** + * Create a new linked bank account payment method in the consumer account. + */ + suspend fun createBankAccountPaymentDetails( financialConnectionsAccountId: String, consumerSessionClientSecret: String, consumerPublishableKey: String? ): Result /** - * Create a new payment method in the consumer account. + * Create a new card payment method in the consumer account. */ - suspend fun createPaymentDetails( - paymentMethod: SupportedPaymentMethod, + suspend fun createCardPaymentDetails( paymentMethodCreateParams: PaymentMethodCreateParams, userEmail: String, stripeIntent: StripeIntent, diff --git a/link/src/main/java/com/stripe/android/link/theme/Theme.kt b/link/src/main/java/com/stripe/android/link/theme/Theme.kt index 140a989c99c..e216b964aa5 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Theme.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Theme.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.unit.dp internal val MinimumTouchTargetSize = 48.dp internal val AppBarHeight = 56.dp +internal val PrimaryButtonHeight = 56.dp internal val HorizontalPadding = 20.dp private val LocalColors = staticCompositionLocalOf { LinkThemeConfig.colors(false) } diff --git a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt index 00aed43b323..4fce88836a0 100644 --- a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt +++ b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt @@ -3,6 +3,7 @@ package com.stripe.android.link.ui import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -26,13 +27,14 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.stripe.android.link.LinkActivityContract import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.HorizontalPadding +import com.stripe.android.link.theme.PrimaryButtonHeight import com.stripe.android.link.theme.linkColors import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent +import com.stripe.android.model.StripeIntent import com.stripe.android.ui.core.Amount /** @@ -56,13 +58,13 @@ internal enum class PrimaryButtonState(val isBlocking: Boolean) { internal const val progressIndicatorTestTag = "CircularProgressIndicator" internal const val completedIconTestTag = "CompletedIcon" -internal fun primaryButtonLabel( - args: LinkActivityContract.Args, +internal fun completePaymentButtonLabel( + stripeIntent: StripeIntent, resources: Resources -) = when (args.stripeIntent) { +) = when (stripeIntent) { is PaymentIntent -> Amount( - requireNotNull(args.stripeIntent.amount), - requireNotNull(args.stripeIntent.currency) + requireNotNull(stripeIntent.amount), + requireNotNull(stripeIntent.currency) ).buildPayButtonLabel(resources) is SetupIntent -> resources.getString(R.string.stripe_setup_button_label) } @@ -74,8 +76,8 @@ private fun PrimaryButton() { PrimaryButton( label = "Testing", state = PrimaryButtonState.Enabled, - icon = R.drawable.stripe_ic_lock, - onButtonClick = { } + onButtonClick = { }, + iconEnd = R.drawable.stripe_ic_lock ) } } @@ -84,8 +86,9 @@ private fun PrimaryButton() { internal fun PrimaryButton( label: String, state: PrimaryButtonState, - @DrawableRes icon: Int? = null, - onButtonClick: () -> Unit + onButtonClick: () -> Unit, + @DrawableRes iconStart: Int? = null, + @DrawableRes iconEnd: Int? = null ) { CompositionLocalProvider( LocalContentAlpha provides @@ -94,14 +97,12 @@ internal fun PrimaryButton( Box( modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp), - contentAlignment = Alignment.CenterEnd + .height(PrimaryButtonHeight + 32.dp) + .padding(vertical = 16.dp) ) { Button( onClick = onButtonClick, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), + modifier = Modifier.fillMaxSize(), enabled = state == PrimaryButtonState.Enabled, elevation = ButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp, 0.dp), shape = MaterialTheme.shapes.medium, @@ -137,25 +138,44 @@ internal fun PrimaryButton( ) } } - // Show icon only when button label is visible - if (icon != null && - state in setOf(PrimaryButtonState.Enabled, PrimaryButtonState.Disabled) - ) { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - modifier = Modifier - .height(16.dp) - // width should be 13dp and must include the horizontal padding - .width(13.dp + 40.dp) - .padding(horizontal = HorizontalPadding), - tint = MaterialTheme.linkColors.buttonLabel.copy(alpha = LocalContentAlpha.current) - ) + // Show icons only when button label is visible + if (state in setOf(PrimaryButtonState.Enabled, PrimaryButtonState.Disabled)) { + iconStart?.let { icon -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.CenterStart + ) { + PrimaryButtonIcon(icon) + } + } + iconEnd?.let { icon -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.CenterEnd + ) { + PrimaryButtonIcon(icon) + } + } } } } } +@Composable +private fun PrimaryButtonIcon( + @DrawableRes icon: Int +) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier + .height(16.dp) + .width(13.dp + 40.dp) + .padding(horizontal = HorizontalPadding), + tint = MaterialTheme.linkColors.buttonLabel.copy(alpha = LocalContentAlpha.current) + ) +} + @Composable internal fun SecondaryButton( enabled: Boolean, @@ -166,7 +186,7 @@ internal fun SecondaryButton( onClick = onClick, modifier = Modifier .fillMaxWidth() - .height(56.dp), + .height(PrimaryButtonHeight), enabled = enabled, shape = MaterialTheme.shapes.medium, colors = ButtonDefaults.buttonColors( diff --git a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt index 8b140b7cbd9..2c47cdda301 100644 --- a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt @@ -1,6 +1,5 @@ package com.stripe.android.link.ui.cardedit -import android.os.Parcelable import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -13,6 +12,7 @@ import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.getErrorMessage +import com.stripe.android.link.ui.wallet.PaymentDetailsResult import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.PaymentMethod @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize import javax.inject.Inject import javax.inject.Provider @@ -75,13 +74,13 @@ internal class CardEditViewModel @Inject constructor( .merchantName(args.merchantName) .build().formController } ?: dismiss( - Result.Failure( + PaymentDetailsResult.Failure( ErrorMessage.Raw("Payment details $paymentDetailsId not found.") ) ) }, onFailure = { - dismiss(Result.Failure(it.getErrorMessage())) + dismiss(PaymentDetailsResult.Failure(it.getErrorMessage())) } ) } @@ -112,15 +111,15 @@ internal class CardEditViewModel @Inject constructor( linkAccountManager.updatePaymentDetails(updateParams).fold( onSuccess = { _isProcessing.value = false - dismiss(Result.Success) + dismiss(PaymentDetailsResult.Success(paymentDetails.id)) }, onFailure = ::onError ) } } - fun dismiss(result: Result = Result.Cancelled) { - navigator.setResult(Result.KEY, result) + fun dismiss(result: PaymentDetailsResult = PaymentDetailsResult.Cancelled) { + navigator.setResult(PaymentDetailsResult.KEY, result) navigator.onBack() } @@ -161,19 +160,4 @@ internal class CardEditViewModel @Inject constructor( } as T } } - - sealed class Result : Parcelable { - @Parcelize - object Success : Result() - - @Parcelize - object Cancelled : Result() - - @Parcelize - class Failure(val error: ErrorMessage) : Result() - - companion object { - const val KEY = "CardEditScreenResult" - } - } } diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt index 44da62c8af1..deafeefc581 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt @@ -32,8 +32,8 @@ import com.stripe.android.link.ui.PrimaryButton import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.ScrollableTopLevelColumn import com.stripe.android.link.ui.SecondaryButton +import com.stripe.android.link.ui.completePaymentButtonLabel import com.stripe.android.link.ui.forms.Form -import com.stripe.android.link.ui.primaryButtonLabel import com.stripe.android.ui.core.injection.NonFallbackInjector @Preview @@ -81,8 +81,8 @@ internal fun PaymentMethodBody( val errorMessage by viewModel.errorMessage.collectAsState() PaymentMethodBody( - primaryButtonLabel = primaryButtonLabel( - viewModel.args, + primaryButtonLabel = completePaymentButtonLabel( + viewModel.args.stripeIntent, LocalContext.current.resources ), primaryButtonState = primaryButtonState.takeIf { formValues != null } @@ -117,7 +117,7 @@ internal fun PaymentMethodBody( ) { ScrollableTopLevelColumn { Text( - text = stringResource(R.string.pm_add_new_card), + text = stringResource(R.string.add_payment_method), modifier = Modifier .padding(top = 4.dp, bottom = 32.dp), textAlign = TextAlign.Center, @@ -137,8 +137,8 @@ internal fun PaymentMethodBody( PrimaryButton( label = primaryButtonLabel, state = primaryButtonState, - icon = R.drawable.stripe_ic_lock, - onButtonClick = onPrimaryButtonClick + onButtonClick = onPrimaryButtonClick, + iconEnd = R.drawable.stripe_ic_lock ) SecondaryButton( enabled = !primaryButtonState.isBlocking, diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index f4a998cee18..07cda0f5adf 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -102,8 +102,7 @@ internal class PaymentMethodViewModel @Inject constructor( ) viewModelScope.launch { - linkAccountManager.createPaymentDetails( - paymentMethod, + linkAccountManager.createCardPaymentDetails( paymentMethodCreateParams, linkAccount.email, args.stripeIntent @@ -169,7 +168,7 @@ internal class PaymentMethodViewModel @Inject constructor( private fun setState(state: PrimaryButtonState) { _primaryButtonState.value = state - navigator.backNavigationEnabled = !state.isBlocking + navigator.userNavigationEnabled = !state.isBlocking } internal class Factory( diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt index 6fa975d743e..1a500b92435 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt @@ -1,8 +1,12 @@ package com.stripe.android.link.ui.paymentmethod +import android.content.res.Resources +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.stripe.android.link.R +import com.stripe.android.link.ui.completePaymentButtonLabel import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams -import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.StripeIntent import com.stripe.android.ui.core.elements.FormItemSpec import com.stripe.android.ui.core.forms.LinkCardForm @@ -11,58 +15,58 @@ import com.stripe.android.ui.core.forms.LinkCardForm * * @param type The Payment Method type. Matches the [ConsumerPaymentDetails] types. * @param formSpec Specification of how the payment method data collection UI should look. + * @param nameResourceId String resource id for the name of this payment method. + * @param iconResourceId Drawable resource id for the icon representing this payment method. + * @param primaryButtonStartIconResourceId Drawable resource id for the icon to be displayed at the + * start of the primary button when this payment method is being created. + * @param primaryButtonEndIconResourceId Drawable resource id for the icon to be displayed at the + * end of the primary button when this payment method is being created. */ internal enum class SupportedPaymentMethod( val type: String, - val formSpec: List + val formSpec: List, + @StringRes val nameResourceId: Int, + @DrawableRes val iconResourceId: Int, + @DrawableRes val primaryButtonStartIconResourceId: Int? = null, + @DrawableRes val primaryButtonEndIconResourceId: Int? = null ) { Card( ConsumerPaymentDetails.Card.type, - LinkCardForm.items + LinkCardForm.items, + R.string.stripe_paymentsheet_payment_method_card, + R.drawable.ic_link_card, + primaryButtonEndIconResourceId = R.drawable.stripe_ic_lock ) { - override fun createParams( - paymentMethodCreateParams: PaymentMethodCreateParams, - email: String - ) = ConsumerPaymentDetailsCreateParams.Card( - paymentMethodCreateParams.toParamMap(), - email - ) - - /** - * CVC is not passed during creation, and must be included when confirming the payment. - */ - override fun extraConfirmationParams(paymentMethodCreateParams: PaymentMethodCreateParams) = - (paymentMethodCreateParams.toParamMap()["card"] as? Map<*, *>)?.let { card -> - mapOf("card" to mapOf("cvc" to card["cvc"])) - } + override fun primaryButtonLabel( + stripeIntent: StripeIntent, + resources: Resources + ) = completePaymentButtonLabel(stripeIntent, resources) }, BankAccount( ConsumerPaymentDetails.BankAccount.type, - emptyList() + emptyList(), + R.string.stripe_payment_method_bank, + R.drawable.ic_link_bank, + primaryButtonStartIconResourceId = R.drawable.ic_link_add ) { - override fun createParams( - paymentMethodCreateParams: PaymentMethodCreateParams, - email: String - ): ConsumerPaymentDetailsCreateParams { - TODO("Not yet implemented") - } + override fun primaryButtonLabel( + stripeIntent: StripeIntent, + resources: Resources + ) = resources.getString(R.string.add_bank_account) }; - /** - * Build the [ConsumerPaymentDetailsCreateParams] that will to create this payment method. - */ - abstract fun createParams( - paymentMethodCreateParams: PaymentMethodCreateParams, - email: String - ): ConsumerPaymentDetailsCreateParams + val showsForm = formSpec.isNotEmpty() /** - * A map containing additional parameters that must be sent during payment confirmation. + * The label for the primary button when this payment method is being created. */ - open fun extraConfirmationParams(paymentMethodCreateParams: PaymentMethodCreateParams): - Map? = null + abstract fun primaryButtonLabel( + stripeIntent: StripeIntent, + resources: Resources + ): String internal companion object { - val allTypes = setOf(Card.type, BankAccount.type) + val allValues = listOf(Card, BankAccount) + val allTypes = allValues.map { it.type }.toSet() } } diff --git a/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt b/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt index b589e672970..93be4ef2141 100644 --- a/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/signup/SignUpScreen.kt @@ -156,11 +156,12 @@ internal fun SignUpBody( PrimaryButtonState.Enabled } else { PrimaryButtonState.Disabled + }, + onButtonClick = { + onSignUpClick() + keyboardController?.hide() } - ) { - onSignUpClick() - keyboardController?.hide() - } + ) } } } diff --git a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt index 79c9a77ca59..14e5952357d 100644 --- a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt @@ -108,7 +108,7 @@ internal class VerificationViewModel @Inject constructor( fun onBack() { clearError() - navigator.onBack() + navigator.onBack(true) linkEventsReporter.on2FACancel() linkAccountManager.logout() } diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt new file mode 100644 index 00000000000..61fa6cade00 --- /dev/null +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetailsResult.kt @@ -0,0 +1,24 @@ +package com.stripe.android.link.ui.wallet + +import android.os.Parcelable +import com.stripe.android.link.ui.ErrorMessage +import kotlinx.parcelize.Parcelize + +/** + * The result of an operation to add or edit a PaymentDetails. + */ +internal sealed class PaymentDetailsResult : Parcelable { + + @Parcelize + class Success(val itemId: String) : PaymentDetailsResult() + + @Parcelize + object Cancelled : PaymentDetailsResult() + + @Parcelize + class Failure(val error: ErrorMessage) : PaymentDetailsResult() + + companion object { + const val KEY = "PaymentDetailsResult" + } +} diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt index 44b916a6903..152c34b68be 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt @@ -49,8 +49,8 @@ import com.stripe.android.link.ui.PrimaryButton import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.ScrollableTopLevelColumn import com.stripe.android.link.ui.SecondaryButton +import com.stripe.android.link.ui.completePaymentButtonLabel import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod -import com.stripe.android.link.ui.primaryButtonLabel import com.stripe.android.model.CardBrand import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.ui.core.elements.Html @@ -130,7 +130,10 @@ internal fun WalletBody( paymentDetailsList = paymentDetailsList, supportedTypes = viewModel.supportedTypes, selectedItem = selectedItem, - primaryButtonLabel = primaryButtonLabel(viewModel.args, LocalContext.current.resources), + primaryButtonLabel = completePaymentButtonLabel( + viewModel.args.stripeIntent, + LocalContext.current.resources + ), primaryButtonState = primaryButtonState, errorMessage = errorMessage, onItemSelected = viewModel::onItemSelected, @@ -260,8 +263,8 @@ internal fun WalletBody( } else { PrimaryButtonState.Disabled }, - icon = R.drawable.stripe_ic_lock, - onButtonClick = onPrimaryButtonClick + onButtonClick = onPrimaryButtonClick, + iconEnd = R.drawable.stripe_ic_lock ) SecondaryButton( enabled = !primaryButtonState.isBlocking, @@ -401,7 +404,7 @@ private fun ExpandedPaymentDetails( tint = Color.Unspecified ) Text( - text = stringResource(id = R.string.wallet_add_payment_method), + text = stringResource(id = R.string.add_payment_method), modifier = Modifier.padding(end = HorizontalPadding, bottom = 4.dp), color = MaterialTheme.linkColors.actionLabel, style = MaterialTheme.typography.button diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt index 7ad1b56c045..9527731278e 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt @@ -16,7 +16,6 @@ import com.stripe.android.link.model.Navigator import com.stripe.android.link.model.supportedPaymentMethodTypes import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.PrimaryButtonState -import com.stripe.android.link.ui.cardedit.CardEditViewModel import com.stripe.android.link.ui.getErrorMessage import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.payments.paymentlauncher.PaymentResult @@ -60,12 +59,12 @@ internal class WalletViewModel @Inject constructor( loadPaymentDetails(true) viewModelScope.launch { - navigator.getResultFlow(CardEditViewModel.Result.KEY) + navigator.getResultFlow(PaymentDetailsResult.KEY) ?.collect { when (it) { - CardEditViewModel.Result.Success -> loadPaymentDetails() - CardEditViewModel.Result.Cancelled -> {} - is CardEditViewModel.Result.Failure -> onError(it.error) + is PaymentDetailsResult.Success -> loadPaymentDetails() + PaymentDetailsResult.Cancelled -> {} + is PaymentDetailsResult.Failure -> onError(it.error) } } } @@ -198,7 +197,7 @@ internal class WalletViewModel @Inject constructor( private fun setState(state: PrimaryButtonState) { _primaryButtonState.value = state - navigator.backNavigationEnabled = !state.isBlocking + navigator.userNavigationEnabled = !state.isBlocking } /** diff --git a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt index 4c3bea0803e..5fcee2a0ffc 100644 --- a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt +++ b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt @@ -385,49 +385,92 @@ class LinkAccountManagerTest { } @Test - fun `createPaymentDetails retries on auth error`() = runSuspendTest { + fun `createFinancialConnectionsSession retries on auth error`() = runSuspendTest { val accountManager = accountManager() accountManager.setAccountNullable(mockConsumerSession) - whenever( - linkRepository.createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() + whenever(linkRepository.createFinancialConnectionsSession(anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) ) - ).thenReturn( - Result.failure(AuthenticationException(StripeError())), - Result.success(mock()) - ) - accountManager.createPaymentDetails(mock(), mock(), "", mock()) + accountManager.createFinancialConnectionsSession() verify(linkRepository, times(2)) - .createPaymentDetails( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() + .createFinancialConnectionsSession(anyOrNull(), anyOrNull()) + verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) + + assertThat(accountManager.linkAccount.value).isNotNull() + } + + @Test + fun `createFinancialConnectionsSession does not retry on auth error if no cookie exists`() = + runSuspendTest { + whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever(linkRepository.createFinancialConnectionsSession(anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) + ) + + accountManager.createFinancialConnectionsSession() + + verify(linkRepository) + .createFinancialConnectionsSession(anyOrNull(), anyOrNull()) + verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + } + + @Test + fun `createPaymentDetails for bank account retries on auth error`() = runSuspendTest { + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever(linkRepository.createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) ) + + accountManager.createBankAccountPaymentDetails("") + + verify(linkRepository, times(2)) + .createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) assertThat(accountManager.linkAccount.value).isNotNull() } @Test - fun `createPaymentDetails does not retry on auth error if no cookie exists`() = runSuspendTest { - whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + fun `createPaymentDetails for bank account does not retry on auth error if no cookie exists`() = + runSuspendTest { + whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever(linkRepository.createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) + ) + + accountManager.createBankAccountPaymentDetails("") + + verify(linkRepository) + .createBankAccountPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + } + + @Test + fun `createPaymentDetails for card retries on auth error`() = runSuspendTest { val accountManager = accountManager() accountManager.setAccountNullable(mockConsumerSession) whenever( - linkRepository.createPaymentDetails( - anyOrNull(), + linkRepository.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull(), @@ -439,20 +482,54 @@ class LinkAccountManagerTest { Result.success(mock()) ) - accountManager.createPaymentDetails(mock(), mock(), "", mock()) + accountManager.createCardPaymentDetails(mock(), "", mock()) - verify(linkRepository) - .createPaymentDetails( - anyOrNull(), + verify(linkRepository, times(2)) + .createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull() ) - verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) + + assertThat(accountManager.linkAccount.value).isNotNull() } + @Test + fun `createPaymentDetails for card does not retry on auth error if no cookie exists`() = + runSuspendTest { + whenever(cookieStore.getAuthSessionCookie()).thenReturn(null) + val accountManager = accountManager() + accountManager.setAccountNullable(mockConsumerSession) + + whenever( + linkRepository.createCardPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + ).thenReturn( + Result.failure(AuthenticationException(StripeError())), + Result.success(mock()) + ) + + accountManager.createCardPaymentDetails(mock(), "", mock()) + + verify(linkRepository) + .createCardPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) + } + @Test fun `updatePaymentDetails retries on auth error`() = runSuspendTest { val accountManager = accountManager() diff --git a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt index ed14a079c2f..e27eb1f094f 100644 --- a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt +++ b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt @@ -409,7 +409,7 @@ class LinkApiRepositoryTest { val secret = "secret" val consumerKey = "key" - linkRepository.createPaymentDetails( + linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId = accountId, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey @@ -433,7 +433,7 @@ class LinkApiRepositoryTest { whenever(stripeRepository.createPaymentDetails(any(), any(), any())) .thenReturn(paymentDetails) - val result = linkRepository.createPaymentDetails( + val result = linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId = accountId, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey @@ -452,7 +452,7 @@ class LinkApiRepositoryTest { whenever(stripeRepository.createPaymentDetails(any(), any(), any())) .thenThrow(RuntimeException("error")) - val result = linkRepository.createPaymentDetails( + val result = linkRepository.createBankAccountPaymentDetails( financialConnectionsAccountId = accountId, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey @@ -467,8 +467,7 @@ class LinkApiRepositoryTest { val email = "email@stripe.com" val consumerKey = "key" - linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = email, stripeIntent = paymentIntent, @@ -503,8 +502,7 @@ class LinkApiRepositoryTest { val secret = "secret" val email = "email@stripe.com" - linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = email, stripeIntent = paymentIntent, @@ -547,8 +545,7 @@ class LinkApiRepositoryTest { ) .thenReturn(paymentDetails) - val result = linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + val result = linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = email, stripeIntent = paymentIntent, @@ -594,8 +591,7 @@ class LinkApiRepositoryTest { ) .thenThrow(RuntimeException("error")) - val result = linkRepository.createPaymentDetails( - paymentMethod = SupportedPaymentMethod.Card, + val result = linkRepository.createCardPaymentDetails( paymentMethodCreateParams = cardPaymentMethodCreateParams, userEmail = "email@stripe.com", stripeIntent = paymentIntent, diff --git a/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt index 86bfdbdb71d..4d2eec148f1 100644 --- a/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt @@ -7,6 +7,7 @@ import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator import com.stripe.android.link.model.PaymentDetailsFixtures +import com.stripe.android.link.ui.wallet.PaymentDetailsResult import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.forms.FormFieldEntry @@ -85,7 +86,7 @@ class CardEditViewModelTest { val viewModel = createViewModel() viewModel.initWithPaymentDetailsId("UNKNOWN_ID") - verify(navigator).setResult(any(), argWhere { it is CardEditViewModel.Result.Failure }) + verify(navigator).setResult(any(), argWhere { it is PaymentDetailsResult.Failure }) } @Test @@ -96,7 +97,7 @@ class CardEditViewModelTest { val viewModel = createViewModel() viewModel.initWithPaymentDetailsId("any") - verify(navigator).setResult(any(), argWhere { it is CardEditViewModel.Result.Failure }) + verify(navigator).setResult(any(), argWhere { it is PaymentDetailsResult.Failure }) } @Test @@ -173,8 +174,8 @@ class CardEditViewModelTest { val viewModel = createAndInitViewModel() viewModel.dismiss() verify(navigator).setResult( - eq(CardEditViewModel.Result.KEY), - argWhere { it is CardEditViewModel.Result.Cancelled } + eq(PaymentDetailsResult.KEY), + argWhere { it is PaymentDetailsResult.Cancelled } ) verify(navigator).onBack() } diff --git a/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt index 846b5ebb0f0..397dfcd78a5 100644 --- a/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt @@ -19,7 +19,6 @@ import com.stripe.android.link.model.PaymentDetailsFixtures import com.stripe.android.link.model.StripeIntentFixtures import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.PrimaryButtonState -import com.stripe.android.link.ui.cardedit.CardEditViewModel import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmStripeIntentParams import com.stripe.android.model.ConsumerPaymentDetails @@ -82,7 +81,7 @@ class WalletViewModelTest { fun `On initialization start collecting CardEdit result`() = runTest { createViewModel() - verify(navigator).getResultFlow(any()) + verify(navigator).getResultFlow(any()) } @Test @@ -320,26 +319,26 @@ class WalletViewModelTest { @Test fun `On CardEdit result successful then it reloads payment details`() = runTest { - val flow = MutableStateFlow(null) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val flow = MutableStateFlow(null) + whenever(navigator.getResultFlow(any())).thenReturn(flow) createViewModel() verify(linkAccountManager).listPaymentDetails() clearInvocations(linkAccountManager) - flow.emit(CardEditViewModel.Result.Success) + flow.emit(PaymentDetailsResult.Success("")) verify(linkAccountManager).listPaymentDetails() } @Test fun `On CardEdit result failure then it shows error`() = runTest { - val flow = MutableStateFlow(null) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val flow = MutableStateFlow(null) + whenever(navigator.getResultFlow(any())).thenReturn(flow) val viewModel = createViewModel() val error = ErrorMessage.Raw("Error message") - flow.emit(CardEditViewModel.Result.Failure(error)) + flow.emit(PaymentDetailsResult.Failure(error)) assertThat(viewModel.errorMessage.value).isEqualTo(error) } diff --git a/payments-core/api/payments-core.api b/payments-core/api/payments-core.api index bed84e62f89..b523e045100 100644 --- a/payments-core/api/payments-core.api +++ b/payments-core/api/payments-core.api @@ -2545,12 +2545,17 @@ public abstract class com/stripe/android/model/ConsumerPaymentDetailsCreateParam public final class com/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card : com/stripe/android/model/ConsumerPaymentDetailsCreateParams { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; + public static final field Companion Lcom/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card$Companion; public fun (Ljava/util/Map;Ljava/lang/String;)V public fun describeContents ()I public fun toParamMap ()Ljava/util/Map; public fun writeToParcel (Landroid/os/Parcel;I)V } +public final class com/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card$Companion { + public final fun extraConfirmationParams (Lcom/stripe/android/model/PaymentMethodCreateParams;)Ljava/util/Map; +} + public final class com/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/model/ConsumerPaymentDetailsCreateParams$Card; diff --git a/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt b/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt index 757b6410f43..34c10892289 100644 --- a/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt +++ b/payments-core/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt @@ -43,5 +43,16 @@ sealed class ConsumerPaymentDetailsCreateParams( } return params } + + companion object { + /** + * A map containing additional parameters that must be sent during payment confirmation. + * CVC is not passed during creation, and must be included when confirming the payment. + */ + fun extraConfirmationParams(paymentMethodCreateParams: PaymentMethodCreateParams) = + (paymentMethodCreateParams.toParamMap()["card"] as? Map<*, *>)?.let { card -> + mapOf("card" to mapOf("cvc" to card["cvc"])) + } + } } } diff --git a/payments-ui-core/res/values/totranslate.xml b/payments-ui-core/res/values/totranslate.xml index 71090027d63..f3524edb51e 100644 --- a/payments-ui-core/res/values/totranslate.xml +++ b/payments-ui-core/res/values/totranslate.xml @@ -14,4 +14,7 @@ Shipping Address Save Address Billing address is same as shipping + + + Bank From e3da511a9de4ac04bf6245a11a4d77ab1057a704 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 25 Aug 2022 13:38:22 -0700 Subject: [PATCH 12/22] createCardPaymentDetails --- .../PaymentMethodViewModelTest.kt | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index f122e73afd8..d7bc2ebd07d 100644 --- a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -102,8 +102,7 @@ class PaymentMethodViewModelTest { @Test fun `startPayment creates PaymentDetails`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), + linkAccountManager.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull() @@ -113,8 +112,7 @@ class PaymentMethodViewModelTest { createViewModel().startPayment(cardFormFieldValues) val paramsCaptor = argumentCaptor() - verify(linkAccountManager).createPaymentDetails( - any(), + verify(linkAccountManager).createCardPaymentDetails( paramsCaptor.capture(), any(), anyOrNull() @@ -144,8 +142,7 @@ class PaymentMethodViewModelTest { runTest { val value = createLinkPaymentDetails() whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), + linkAccountManager.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull() @@ -188,8 +185,7 @@ class PaymentMethodViewModelTest { @Test fun `startPayment dismisses Link on success`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), + linkAccountManager.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull() @@ -222,8 +218,7 @@ class PaymentMethodViewModelTest { @Test fun `startPayment starts processing`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), + linkAccountManager.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull() @@ -245,8 +240,7 @@ class PaymentMethodViewModelTest { @Test fun `startPayment stops processing on error`() = runTest { whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), + linkAccountManager.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull() @@ -282,8 +276,7 @@ class PaymentMethodViewModelTest { fun `when startPayment fails then an error message is shown`() = runTest { val errorMessage = "Error message" whenever( - linkAccountManager.createPaymentDetails( - anyOrNull(), + linkAccountManager.createCardPaymentDetails( anyOrNull(), anyOrNull(), anyOrNull() From 8f9d6d14393890ab0033da4cecc54b699077c188 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 25 Aug 2022 13:56:44 -0700 Subject: [PATCH 13/22] apidump --- link/api/link.api | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/link/api/link.api b/link/api/link.api index 24ccd5d6c70..ad86b4a825a 100644 --- a/link/api/link.api +++ b/link/api/link.api @@ -362,30 +362,6 @@ public final class com/stripe/android/link/ui/LinkTermsKt { public static final fun LinkTerms-5stqomU (Landroidx/compose/ui/Modifier;ILandroidx/compose/runtime/Composer;II)V } -public final class com/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Cancelled$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Cancelled; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Cancelled; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Failure$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Failure; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Failure; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Success$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Success; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/link/ui/cardedit/CardEditViewModel$Result$Success; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/stripe/android/link/ui/cardedit/CardEditViewModel_Factory : dagger/internal/Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/link/ui/cardedit/CardEditViewModel_Factory; @@ -585,6 +561,30 @@ public final class com/stripe/android/link/ui/wallet/ComposableSingletons$Wallet public final fun getLambda-2$link_release ()Lkotlin/jvm/functions/Function2; } +public final class com/stripe/android/link/ui/wallet/PaymentDetailsResult$Cancelled$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Cancelled; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Cancelled; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/link/ui/wallet/PaymentDetailsResult$Failure$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Failure; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Failure; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/link/ui/wallet/PaymentDetailsResult$Success$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Success; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/link/ui/wallet/PaymentDetailsResult$Success; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/link/ui/wallet/WalletViewModel_Factory : dagger/internal/Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/link/ui/wallet/WalletViewModel_Factory; From b8d6bcafc1823cd5bcdda50e5bdc7776037c0aa2 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 25 Aug 2022 16:50:03 -0700 Subject: [PATCH 14/22] comments --- .../com/stripe/android/link/LinkActivity.kt | 2 +- .../stripe/android/link/model/Navigator.kt | 2 +- .../stripe/android/link/ui/PrimaryButton.kt | 76 +++++++++---------- .../link/ui/cardedit/CardEditScreen.kt | 5 +- .../link/ui/cardedit/CardEditViewModel.kt | 14 ++-- .../paymentmethod/PaymentMethodViewModel.kt | 2 +- .../paymentmethod/SupportedPaymentMethod.kt | 3 +- .../ui/verification/VerificationViewModel.kt | 2 +- .../link/ui/cardedit/CardEditViewModelTest.kt | 4 +- .../PaymentMethodViewModelTest.kt | 2 +- 10 files changed, 56 insertions(+), 56 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/LinkActivity.kt b/link/src/main/java/com/stripe/android/link/LinkActivity.kt index 05307bd2b91..544b848f11c 100644 --- a/link/src/main/java/com/stripe/android/link/LinkActivity.kt +++ b/link/src/main/java/com/stripe/android/link/LinkActivity.kt @@ -124,7 +124,7 @@ internal class LinkActivity : ComponentActivity() { LinkAppBar( state = appBarState, - onButtonClick = { viewModel.navigator.onBack() } + onButtonClick = { viewModel.navigator.onBack(userInitiated = true) } ) NavHost(navController, LinkScreen.Loading.route) { diff --git a/link/src/main/java/com/stripe/android/link/model/Navigator.kt b/link/src/main/java/com/stripe/android/link/model/Navigator.kt index 66a8690d41b..529dbb9862f 100644 --- a/link/src/main/java/com/stripe/android/link/model/Navigator.kt +++ b/link/src/main/java/com/stripe/android/link/model/Navigator.kt @@ -45,7 +45,7 @@ internal class Navigator @Inject constructor() { * * @param userInitiated Whether the action was initiated by user interaction. */ - fun onBack(userInitiated: Boolean = false) { + fun onBack(userInitiated: Boolean) { if (!userInitiated || userNavigationEnabled) { navigationController?.let { navController -> if (!navController.popBackStack()) { diff --git a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt index 4fce88836a0..f8fbb6f9997 100644 --- a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt +++ b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt @@ -3,7 +3,7 @@ package com.stripe.android.link.ui import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -25,11 +25,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme -import com.stripe.android.link.theme.HorizontalPadding import com.stripe.android.link.theme.PrimaryButtonHeight import com.stripe.android.link.theme.linkColors import com.stripe.android.model.PaymentIntent @@ -55,6 +55,8 @@ internal enum class PrimaryButtonState(val isBlocking: Boolean) { } } +private val PrimaryButtonIconWidth = 13.dp +private val PrimaryButtonIconHeight = 16.dp internal const val progressIndicatorTestTag = "CircularProgressIndicator" internal const val completedIconTestTag = "CompletedIcon" @@ -94,15 +96,12 @@ internal fun PrimaryButton( LocalContentAlpha provides if (state == PrimaryButtonState.Disabled) ContentAlpha.disabled else ContentAlpha.high ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(PrimaryButtonHeight + 32.dp) - .padding(vertical = 16.dp) - ) { + Box(modifier = Modifier.padding(vertical = 16.dp)) { Button( onClick = onButtonClick, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .height(PrimaryButtonHeight) + .fillMaxWidth(), enabled = state == PrimaryButtonState.Enabled, elevation = ButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp, 0.dp), shape = MaterialTheme.shapes.medium, @@ -131,29 +130,16 @@ internal fun PrimaryButton( }, tint = MaterialTheme.linkColors.buttonLabel ) - else -> Text( - text = label, - color = MaterialTheme.linkColors.buttonLabel - .copy(alpha = LocalContentAlpha.current) - ) - } - } - // Show icons only when button label is visible - if (state in setOf(PrimaryButtonState.Enabled, PrimaryButtonState.Disabled)) { - iconStart?.let { icon -> - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.CenterStart - ) { - PrimaryButtonIcon(icon) - } - } - iconEnd?.let { icon -> - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.CenterEnd - ) { - PrimaryButtonIcon(icon) + else -> Row(Modifier.fillMaxWidth()) { + PrimaryButtonIcon(iconStart) + Text( + text = label, + modifier = Modifier.weight(1f), + color = MaterialTheme.linkColors.buttonLabel + .copy(alpha = LocalContentAlpha.current), + textAlign = TextAlign.Center + ) + PrimaryButtonIcon(iconEnd) } } } @@ -163,17 +149,25 @@ internal fun PrimaryButton( @Composable private fun PrimaryButtonIcon( - @DrawableRes icon: Int + @DrawableRes icon: Int? ) { - Icon( - painter = painterResource(id = icon), - contentDescription = null, + Box( modifier = Modifier - .height(16.dp) - .width(13.dp + 40.dp) - .padding(horizontal = HorizontalPadding), - tint = MaterialTheme.linkColors.buttonLabel.copy(alpha = LocalContentAlpha.current) - ) + .width(PrimaryButtonIconWidth) + .height(PrimaryButtonIconHeight), + contentAlignment = Alignment.Center + ) { + icon?.let { icon -> + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier + .width(PrimaryButtonIconWidth) + .height(PrimaryButtonIconHeight), + tint = MaterialTheme.linkColors.buttonLabel.copy(alpha = LocalContentAlpha.current) + ) + } + } } @Composable diff --git a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditScreen.kt b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditScreen.kt index f10ac62eced..d821ea2c259 100644 --- a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditScreen.kt @@ -38,6 +38,7 @@ import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.ScrollableTopLevelColumn import com.stripe.android.link.ui.SecondaryButton import com.stripe.android.link.ui.forms.Form +import com.stripe.android.link.ui.wallet.PaymentDetailsResult import com.stripe.android.ui.core.injection.NonFallbackInjector @Preview @@ -100,7 +101,9 @@ internal fun CardEditBody( viewModel.updateCard(it) } }, - onCancelClick = viewModel::dismiss + onCancelClick = { + viewModel.dismiss(PaymentDetailsResult.Cancelled, userInitiated = true) + } ) { Form( it, diff --git a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt index 2c47cdda301..2fc7e56c676 100644 --- a/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/cardedit/CardEditViewModel.kt @@ -76,11 +76,15 @@ internal class CardEditViewModel @Inject constructor( } ?: dismiss( PaymentDetailsResult.Failure( ErrorMessage.Raw("Payment details $paymentDetailsId not found.") - ) + ), + userInitiated = false ) }, onFailure = { - dismiss(PaymentDetailsResult.Failure(it.getErrorMessage())) + dismiss( + PaymentDetailsResult.Failure(it.getErrorMessage()), + userInitiated = false + ) } ) } @@ -111,16 +115,16 @@ internal class CardEditViewModel @Inject constructor( linkAccountManager.updatePaymentDetails(updateParams).fold( onSuccess = { _isProcessing.value = false - dismiss(PaymentDetailsResult.Success(paymentDetails.id)) + dismiss(PaymentDetailsResult.Success(paymentDetails.id), userInitiated = false) }, onFailure = ::onError ) } } - fun dismiss(result: PaymentDetailsResult = PaymentDetailsResult.Cancelled) { + fun dismiss(result: PaymentDetailsResult, userInitiated: Boolean) { navigator.setResult(PaymentDetailsResult.KEY, result) - navigator.onBack() + navigator.onBack(userInitiated) } private fun clearError() { diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index 07cda0f5adf..037db25bb02 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -117,7 +117,7 @@ internal class PaymentMethodViewModel @Inject constructor( if (isRootScreen) { payAnotherWay() } else { - navigator.onBack() + navigator.onBack(userInitiated = true) } } diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt index 1a500b92435..08ebf54e995 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt @@ -66,7 +66,6 @@ internal enum class SupportedPaymentMethod( ): String internal companion object { - val allValues = listOf(Card, BankAccount) - val allTypes = allValues.map { it.type }.toSet() + val allTypes = values().map { it.type }.toSet() } } diff --git a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt index 14e5952357d..f9da53b48f4 100644 --- a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt @@ -108,7 +108,7 @@ internal class VerificationViewModel @Inject constructor( fun onBack() { clearError() - navigator.onBack(true) + navigator.onBack(userInitiated = true) linkEventsReporter.on2FACancel() linkAccountManager.logout() } diff --git a/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt index 4d2eec148f1..ad8a6d41c80 100644 --- a/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/cardedit/CardEditViewModelTest.kt @@ -172,12 +172,12 @@ class CardEditViewModelTest { @Test fun `dismiss navigates back`() = runTest { val viewModel = createAndInitViewModel() - viewModel.dismiss() + viewModel.dismiss(PaymentDetailsResult.Cancelled, userInitiated = true) verify(navigator).setResult( eq(PaymentDetailsResult.KEY), argWhere { it is PaymentDetailsResult.Cancelled } ) - verify(navigator).onBack() + verify(navigator).onBack(userInitiated = true) } private fun createViewModel() = diff --git a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index d7bc2ebd07d..a497c5a12ed 100644 --- a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -331,7 +331,7 @@ class PaymentMethodViewModelTest { createViewModel().onSecondaryButtonClick() - verify(navigator).onBack() + verify(navigator).onBack(userInitiated = true) } @Test From 03fd6f36efd9c8382cf5f0fb2d3c373bdf60d0b5 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 25 Aug 2022 17:06:08 -0700 Subject: [PATCH 15/22] Fix positioning --- .../main/java/com/stripe/android/link/ui/PrimaryButton.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt index f8fbb6f9997..dc7d5067813 100644 --- a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt +++ b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt @@ -130,7 +130,10 @@ internal fun PrimaryButton( }, tint = MaterialTheme.linkColors.buttonLabel ) - else -> Row(Modifier.fillMaxWidth()) { + else -> Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { PrimaryButtonIcon(iconStart) Text( text = label, From 3d5c2cb8b50801682ef88f6a5ebc502938d9b300 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 25 Aug 2022 17:16:06 -0700 Subject: [PATCH 16/22] Fix errors --- .../stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt | 2 +- .../android/link/ui/paymentmethod/PaymentMethodViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt index 8ad39836930..64003ae6f60 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt @@ -59,7 +59,7 @@ private fun PaymentMethodBodyPreview() { DefaultLinkTheme { Surface { PaymentMethodBody( - supportedPaymentMethods = SupportedPaymentMethod.allValues, + supportedPaymentMethods = SupportedPaymentMethod.values().toList(), selectedPaymentMethod = SupportedPaymentMethod.Card, primaryButtonLabel = "Pay $10.99", primaryButtonState = PrimaryButtonState.Enabled, diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index 28b35c5dd7a..c230106f724 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -74,7 +74,7 @@ internal class PaymentMethodViewModel @Inject constructor( val supportedTypes = args.stripeIntent.supportedPaymentMethodTypes(linkAccount) .let { supportedTypes -> - SupportedPaymentMethod.allValues.filter { supportedTypes.contains(it.type) } + SupportedPaymentMethod.values().filter { supportedTypes.contains(it.type) } } private val _paymentMethod = MutableStateFlow(supportedTypes.first()) @@ -190,7 +190,7 @@ internal class PaymentMethodViewModel @Inject constructor( PaymentDetailsResult.KEY, PaymentDetailsResult.Success(selectedAccount.id) ) - navigator.onBack() + navigator.onBack(userInitiated = false) } else { navigator.navigateTo(LinkScreen.Wallet, clearBackStack = true) } From 694ee8df94656ded98702e92d36e36509a0b852a Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Fri, 26 Aug 2022 09:58:42 -0700 Subject: [PATCH 17/22] Check funding sources --- link/api/link.api | 4 ---- .../main/java/com/stripe/android/link/LinkPaymentLauncher.kt | 3 +++ .../android/paymentsheet/BaseAddPaymentMethodFragment.kt | 2 ++ .../com/stripe/android/paymentsheet/PaymentSheetViewModel.kt | 5 ++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/link/api/link.api b/link/api/link.api index ad86b4a825a..e0dea44cbc9 100644 --- a/link/api/link.api +++ b/link/api/link.api @@ -178,10 +178,6 @@ public final class com/stripe/android/link/LinkPaymentDetails$Saved$Creator : an public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/stripe/android/link/LinkPaymentLauncher$Companion { - public final fun getLINK_ENABLED ()Z -} - public final class com/stripe/android/link/LinkPaymentLauncher_Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/link/LinkPaymentLauncher_Factory; diff --git a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt index 014cba3ea2e..005bf17ed6f 100644 --- a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt +++ b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt @@ -26,6 +26,7 @@ import com.stripe.android.link.ui.cardedit.CardEditViewModel import com.stripe.android.link.ui.inline.InlineSignupViewModel import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.link.ui.paymentmethod.PaymentMethodViewModel +import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.link.ui.signup.SignUpViewModel import com.stripe.android.link.ui.verification.VerificationViewModel import com.stripe.android.link.ui.wallet.WalletViewModel @@ -236,7 +237,9 @@ class LinkPaymentLauncher @AssistedInject internal constructor( WeakMapInjectorRegistry.register(injector, injectorKey) } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) companion object { val LINK_ENABLED = BuildConfig.DEBUG + val supportedFundingSources = SupportedPaymentMethod.allTypes } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt index 012c64ebfd5..cec5c000bf4 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt @@ -219,6 +219,8 @@ internal abstract class BaseAddPaymentMethodFragment : Fragment() { private fun updateLinkInlineSignupVisibility(selectedPaymentMethod: SupportedPaymentMethod) { showLinkInlineSignup = sheetViewModel.isLinkEnabled.value == true && + (sheetViewModel.stripeIntent.value + ?.linkFundingSources?.contains(PaymentMethod.Type.Card.code) ?: false) && selectedPaymentMethod.code == PaymentMethod.Type.Card.code && sheetViewModel.linkLauncher.accountStatus.value == AccountStatus.SignedOut 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 06663c62496..1e70d91f06c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt @@ -29,6 +29,7 @@ import com.stripe.android.googlepaylauncher.injection.GooglePayPaymentMethodLaun import com.stripe.android.link.LinkActivityContract import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.LinkPaymentLauncher import com.stripe.android.link.LinkPaymentLauncher.Companion.LINK_ENABLED import com.stripe.android.link.injection.LinkPaymentLauncherFactory import com.stripe.android.link.model.AccountStatus @@ -430,7 +431,9 @@ internal class PaymentSheetViewModel @Inject internal constructor( override fun setupLink(stripeIntent: StripeIntent) { if (LINK_ENABLED && - stripeIntent.paymentMethodTypes.contains(PaymentMethod.Type.Link.code) + stripeIntent.paymentMethodTypes.contains(PaymentMethod.Type.Link.code) && + stripeIntent.linkFundingSources.intersect(LinkPaymentLauncher.supportedFundingSources) + .isNotEmpty() ) { viewModelScope.launch { val accountStatus = linkLauncher.setup(stripeIntent, this) From 3f2444973b7c95ba49161c60907bab38c0b72881 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Fri, 26 Aug 2022 10:15:32 -0700 Subject: [PATCH 18/22] lint --- .../android/paymentsheet/BaseAddPaymentMethodFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt index cec5c000bf4..7e731404855 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt @@ -219,8 +219,8 @@ internal abstract class BaseAddPaymentMethodFragment : Fragment() { private fun updateLinkInlineSignupVisibility(selectedPaymentMethod: SupportedPaymentMethod) { showLinkInlineSignup = sheetViewModel.isLinkEnabled.value == true && - (sheetViewModel.stripeIntent.value - ?.linkFundingSources?.contains(PaymentMethod.Type.Card.code) ?: false) && + sheetViewModel.stripeIntent.value + ?.linkFundingSources?.contains(PaymentMethod.Type.Card.code) ?: false && selectedPaymentMethod.code == PaymentMethod.Type.Card.code && sheetViewModel.linkLauncher.accountStatus.value == AccountStatus.SignedOut From 0de2b981736a17d826a958da9fceb5c73893558b Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Fri, 26 Aug 2022 16:08:48 -0700 Subject: [PATCH 19/22] comments --- .../com/stripe/android/link/LinkActivity.kt | 3 ++- .../com/stripe/android/link/theme/Color.kt | 11 +++++++++ .../com/stripe/android/link/theme/Theme.kt | 5 ++++ .../stripe/android/link/ui/LinkButtonView.kt | 3 ++- .../stripe/android/link/ui/PrimaryButton.kt | 5 ++-- .../link/ui/inline/LinkInlineSignupView.kt | 5 ++-- .../ui/paymentmethod/PaymentMethodBody.kt | 24 +++++++++---------- .../ui/verification/VerificationDialog.kt | 3 ++- .../ui/verification/VerificationScreen.kt | 3 ++- .../android/link/ui/wallet/PaymentDetails.kt | 3 ++- .../android/link/ui/wallet/WalletScreen.kt | 9 +++---- 11 files changed, 48 insertions(+), 26 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/LinkActivity.kt b/link/src/main/java/com/stripe/android/link/LinkActivity.kt index 544b848f11c..9cd54c0930a 100644 --- a/link/src/main/java/com/stripe/android/link/LinkActivity.kt +++ b/link/src/main/java/com/stripe/android/link/LinkActivity.kt @@ -43,6 +43,7 @@ import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.model.isOnRootScreen import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.BottomSheetContent import com.stripe.android.link.ui.LinkAppBar import com.stripe.android.link.ui.cardedit.CardEditBody @@ -101,7 +102,7 @@ internal class LinkActivity : ComponentActivity() { }, modifier = Modifier.fillMaxHeight(), sheetState = sheetState, - sheetShape = MaterialTheme.shapes.large.copy( + sheetShape = MaterialTheme.linkShapes.large.copy( bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp) ), diff --git a/link/src/main/java/com/stripe/android/link/theme/Color.kt b/link/src/main/java/com/stripe/android/link/theme/Color.kt index c61de78aced..71af94f23a6 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Color.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Color.kt @@ -1,11 +1,13 @@ package com.stripe.android.link.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Colors import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import com.stripe.android.ui.core.PaymentsTheme import com.stripe.android.ui.core.PaymentsThemeDefaults import com.stripe.android.ui.core.elements.OTPElementColors @@ -66,6 +68,13 @@ internal data class LinkColors( val materialColors: Colors ) +internal object LinkShapes { + val extraSmall = RoundedCornerShape(4.dp) + val small = RoundedCornerShape(8.dp) + val medium = RoundedCornerShape(12.dp) + val large = RoundedCornerShape(14.dp) +} + @Composable internal fun PaymentsThemeForLink( content: @Composable () -> Unit @@ -90,6 +99,8 @@ internal object LinkThemeConfig { return if (isDark) colorsDark else colorsLight } + val shapes = LinkShapes + private val colorsLight = LinkColors( componentBackground = LightComponentBackground, componentBorder = LightComponentBorder, diff --git a/link/src/main/java/com/stripe/android/link/theme/Theme.kt b/link/src/main/java/com/stripe/android/link/theme/Theme.kt index e216b964aa5..121d045d6f6 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Theme.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Theme.kt @@ -36,3 +36,8 @@ internal val MaterialTheme.linkColors: LinkColors @Composable @ReadOnlyComposable get() = LocalColors.current + +internal val MaterialTheme.linkShapes: LinkShapes + @Composable + @ReadOnlyComposable + get() = LinkShapes diff --git a/link/src/main/java/com/stripe/android/link/ui/LinkButtonView.kt b/link/src/main/java/com/stripe/android/link/ui/LinkButtonView.kt index 7db39a58bcd..8213817e3c7 100644 --- a/link/src/main/java/com/stripe/android/link/ui/LinkButtonView.kt +++ b/link/src/main/java/com/stripe/android/link/ui/LinkButtonView.kt @@ -35,6 +35,7 @@ import com.stripe.android.link.LinkPaymentLauncher import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes private val LinkButtonVerticalPadding = 6.dp private val LinkButtonHorizontalPadding = 10.dp @@ -106,7 +107,7 @@ private fun LinkButton( modifier = Modifier .background( color = Color.Black.copy(alpha = 0.05f), - shape = MaterialTheme.shapes.small + shape = MaterialTheme.linkShapes.extraSmall ) ) { Text( diff --git a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt index dc7d5067813..d3f9ce74ae7 100644 --- a/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt +++ b/link/src/main/java/com/stripe/android/link/ui/PrimaryButton.kt @@ -32,6 +32,7 @@ import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.PrimaryButtonHeight import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.model.StripeIntent @@ -104,7 +105,7 @@ internal fun PrimaryButton( .fillMaxWidth(), enabled = state == PrimaryButtonState.Enabled, elevation = ButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp, 0.dp), - shape = MaterialTheme.shapes.medium, + shape = MaterialTheme.linkShapes.medium, colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.primary @@ -185,7 +186,7 @@ internal fun SecondaryButton( .fillMaxWidth() .height(PrimaryButtonHeight), enabled = enabled, - shape = MaterialTheme.shapes.medium, + shape = MaterialTheme.linkShapes.medium, colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.secondary, disabledBackgroundColor = MaterialTheme.colors.secondary diff --git a/link/src/main/java/com/stripe/android/link/ui/inline/LinkInlineSignupView.kt b/link/src/main/java/com/stripe/android/link/ui/inline/LinkInlineSignupView.kt index dde270696c8..0e0867ded76 100644 --- a/link/src/main/java/com/stripe/android/link/ui/inline/LinkInlineSignupView.kt +++ b/link/src/main/java/com/stripe/android/link/ui/inline/LinkInlineSignupView.kt @@ -37,6 +37,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.stripe.android.link.LinkPaymentLauncher import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.LinkTerms import com.stripe.android.link.ui.signup.EmailCollectionSection import com.stripe.android.link.ui.signup.SignUpState @@ -131,11 +132,11 @@ internal fun LinkInlineSignup( .fillMaxWidth() .border( border = MaterialTheme.getBorderStroke(isSelected = false), - shape = MaterialTheme.shapes.medium + shape = MaterialTheme.linkShapes.medium ) .background( color = MaterialTheme.paymentsColors.component, - shape = MaterialTheme.shapes.medium + shape = MaterialTheme.linkShapes.medium ) ) { Row( diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt index 64003ae6f60..d9491d2c422 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodBody.kt @@ -4,6 +4,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -15,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme @@ -44,6 +44,7 @@ import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.PaymentsThemeForLink import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.ErrorText import com.stripe.android.link.ui.PrimaryButton @@ -105,8 +106,8 @@ internal fun PaymentMethodBody( val formController by viewModel.formController.collectAsState() - formController?.let { - val formValues by it.completeFormValues.collectAsState(null) + formController?.let { controller -> + val formValues by controller.completeFormValues.collectAsState(null) val primaryButtonState by viewModel.primaryButtonState.collectAsState() val errorMessage by viewModel.errorMessage.collectAsState() val paymentMethod by viewModel.paymentMethod.collectAsState() @@ -131,7 +132,7 @@ internal fun PaymentMethodBody( onSecondaryButtonClick = viewModel::onSecondaryButtonClick, formContent = { Form( - it, + controller, viewModel.isEnabled ) } @@ -172,22 +173,19 @@ internal fun PaymentMethodBody( ) if (supportedPaymentMethods.size > 1) { Row( - Modifier + modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - supportedPaymentMethods.forEachIndexed { index, paymentMethod -> + supportedPaymentMethods.forEach { paymentMethod -> PaymentMethodTypeCell( paymentMethod = paymentMethod, selected = paymentMethod == selectedPaymentMethod, enabled = !primaryButtonState.isBlocking, onSelected = { onPaymentMethodSelected(paymentMethod) - }, - modifier = Modifier.padding( - start = if (index > 0) 10.dp else 0.dp, - end = if (index < supportedPaymentMethods.lastIndex) 10.dp else 0.dp - ) + } ) } } @@ -233,7 +231,7 @@ private fun RowScope.PaymentMethodTypeCell( modifier = modifier .height(56.dp) .weight(1f), - shape = RoundedCornerShape(8.dp), + shape = MaterialTheme.linkShapes.small, color = MaterialTheme.linkColors.componentBackground, border = BorderStroke( width = if (selected) { diff --git a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationDialog.kt b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationDialog.kt index af51c9c6a15..6416d9af589 100644 --- a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationDialog.kt +++ b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationDialog.kt @@ -24,6 +24,7 @@ import com.stripe.android.link.LinkPaymentLauncher import com.stripe.android.link.LinkScreen import com.stripe.android.link.R import com.stripe.android.link.theme.DefaultLinkTheme +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.LinkAppBar import com.stripe.android.link.ui.rememberLinkAppBarState @@ -74,7 +75,7 @@ fun LinkVerificationDialog( modifier = Modifier .fillMaxWidth() .padding(16.dp), - shape = MaterialTheme.shapes.medium + shape = MaterialTheme.linkShapes.medium ) { Column { val appBarState = rememberLinkAppBarState( diff --git a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt index d86a87618e4..a5c0bd87759 100644 --- a/link/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt @@ -35,6 +35,7 @@ import com.stripe.android.link.R import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.ErrorText import com.stripe.android.link.ui.ScrollableTopLevelColumn @@ -209,7 +210,7 @@ internal fun VerificationBody( .border( width = 1.dp, color = MaterialTheme.linkColors.componentBorder, - shape = MaterialTheme.shapes.small + shape = MaterialTheme.linkShapes.extraSmall ) .clickable( enabled = !isProcessing, diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetails.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetails.kt index a06076b6c27..8be7c62df19 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetails.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/PaymentDetails.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp import com.stripe.android.link.R import com.stripe.android.link.model.icon import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.ErrorText import com.stripe.android.link.ui.ErrorTextStyle import com.stripe.android.model.ConsumerPaymentDetails @@ -81,7 +82,7 @@ internal fun PaymentDetailsListItem( .height(20.dp) .background( color = MaterialTheme.colors.secondary, - shape = MaterialTheme.shapes.small + shape = MaterialTheme.linkShapes.extraSmall ), contentAlignment = Alignment.Center ) { diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt index 143da7f3ee8..9041e3c734b 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt @@ -41,6 +41,7 @@ import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.theme.DefaultLinkTheme import com.stripe.android.link.theme.HorizontalPadding import com.stripe.android.link.theme.linkColors +import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.BottomSheetContent import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.link.ui.ErrorText @@ -291,11 +292,11 @@ internal fun CollapsedPaymentDetails( .border( width = 1.dp, color = MaterialTheme.linkColors.componentBorder, - shape = MaterialTheme.shapes.large + shape = MaterialTheme.linkShapes.large ) .background( color = MaterialTheme.linkColors.componentBackground, - shape = MaterialTheme.shapes.large + shape = MaterialTheme.linkShapes.large ) .clickable( enabled = enabled, @@ -343,11 +344,11 @@ private fun ExpandedPaymentDetails( .border( width = 1.dp, color = MaterialTheme.linkColors.componentBorder, - shape = MaterialTheme.shapes.large + shape = MaterialTheme.linkShapes.large ) .background( color = MaterialTheme.linkColors.componentBackground, - shape = MaterialTheme.shapes.large + shape = MaterialTheme.linkShapes.large ) ) { Row( From ce6bff2ded336f366c83ab5b39d7781ec6a9abbb Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Fri, 26 Aug 2022 16:11:57 -0700 Subject: [PATCH 20/22] LinkShapes --- .../main/java/com/stripe/android/link/theme/Color.kt | 11 ----------- .../main/java/com/stripe/android/link/theme/Shape.kt | 12 ++++++------ 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/theme/Color.kt b/link/src/main/java/com/stripe/android/link/theme/Color.kt index 71af94f23a6..c61de78aced 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Color.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Color.kt @@ -1,13 +1,11 @@ package com.stripe.android.link.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Colors import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.stripe.android.ui.core.PaymentsTheme import com.stripe.android.ui.core.PaymentsThemeDefaults import com.stripe.android.ui.core.elements.OTPElementColors @@ -68,13 +66,6 @@ internal data class LinkColors( val materialColors: Colors ) -internal object LinkShapes { - val extraSmall = RoundedCornerShape(4.dp) - val small = RoundedCornerShape(8.dp) - val medium = RoundedCornerShape(12.dp) - val large = RoundedCornerShape(14.dp) -} - @Composable internal fun PaymentsThemeForLink( content: @Composable () -> Unit @@ -99,8 +90,6 @@ internal object LinkThemeConfig { return if (isDark) colorsDark else colorsLight } - val shapes = LinkShapes - private val colorsLight = LinkColors( componentBackground = LightComponentBackground, componentBorder = LightComponentBorder, diff --git a/link/src/main/java/com/stripe/android/link/theme/Shape.kt b/link/src/main/java/com/stripe/android/link/theme/Shape.kt index 110ee5ee9f3..a45121f807e 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Shape.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Shape.kt @@ -1,11 +1,11 @@ package com.stripe.android.link.theme import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Shapes import androidx.compose.ui.unit.dp -internal val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(12.dp), - large = RoundedCornerShape(14.dp) -) +internal object LinkShapes { + val extraSmall = RoundedCornerShape(4.dp) + val small = RoundedCornerShape(8.dp) + val medium = RoundedCornerShape(12.dp) + val large = RoundedCornerShape(14.dp) +} From ec0c6512e80aca1a144f0c287c2637a1331c627b Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Fri, 26 Aug 2022 16:14:20 -0700 Subject: [PATCH 21/22] lint --- .../stripe/android/link/theme/{Shape.kt => LinkShapes.kt} | 0 .../stripe/android/ui/core/elements/PostalCodeConfig.kt | 7 +++++-- 2 files changed, 5 insertions(+), 2 deletions(-) rename link/src/main/java/com/stripe/android/link/theme/{Shape.kt => LinkShapes.kt} (100%) diff --git a/link/src/main/java/com/stripe/android/link/theme/Shape.kt b/link/src/main/java/com/stripe/android/link/theme/LinkShapes.kt similarity index 100% rename from link/src/main/java/com/stripe/android/link/theme/Shape.kt rename to link/src/main/java/com/stripe/android/link/theme/LinkShapes.kt diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt index 84c062ced9d..b365197b7ca 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt @@ -27,8 +27,9 @@ internal class PostalCodeConfig( override fun isValid(): Boolean { return when (format) { is CountryPostalFormat.Other -> input.isNotBlank() - else -> input.length in format.minimumLength..format.maximumLength && - input.matches(format.regexPattern) + else -> + input.length in format.minimumLength..format.maximumLength && + input.matches(format.regexPattern) } } @@ -63,11 +64,13 @@ internal class PostalCodeConfig( maximumLength = 6, regexPattern = Regex("[a-zA-Z]\\d[a-zA-Z][\\s-]?\\d[a-zA-Z]\\d") ) + object US : CountryPostalFormat( minimumLength = 5, maximumLength = 5, regexPattern = Regex("\\d+") ) + object Other : CountryPostalFormat( minimumLength = 1, maximumLength = Int.MAX_VALUE, From b2479cdbadc5065d6e78f8b0b56c513206ce1e17 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Fri, 26 Aug 2022 16:15:45 -0700 Subject: [PATCH 22/22] lint --- link/src/main/java/com/stripe/android/link/theme/Theme.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/link/src/main/java/com/stripe/android/link/theme/Theme.kt b/link/src/main/java/com/stripe/android/link/theme/Theme.kt index 121d045d6f6..1c9d49f3ee2 100644 --- a/link/src/main/java/com/stripe/android/link/theme/Theme.kt +++ b/link/src/main/java/com/stripe/android/link/theme/Theme.kt @@ -26,7 +26,7 @@ internal fun DefaultLinkTheme( MaterialTheme( colors = colors.materialColors, typography = Typography, - shapes = Shapes, + shapes = MaterialTheme.shapes, content = content ) }