Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

LinkAccount payment methods list #9616

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.stripe.android.link

internal class NoLinkAccountFoundException : IllegalStateException("No link account found")
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import com.stripe.android.core.exception.StripeException
import com.stripe.android.link.BuildConfig
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.NoLinkAccountFoundException
import com.stripe.android.link.analytics.LinkEventsReporter
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.SignUpConsentAction
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSignUpConsentAction
Expand Down Expand Up @@ -242,6 +244,15 @@ internal class DefaultLinkAccountManager @Inject constructor(
}
}

override suspend fun listPaymentDetails(paymentMethodTypes: Set<String>): Result<ConsumerPaymentDetails> {
val clientSecret = linkAccount.value?.clientSecret ?: return Result.failure(NoLinkAccountFoundException())
return linkRepository.listPaymentDetails(
paymentMethodTypes = paymentMethodTypes,
consumerSessionClientSecret = clientSecret,
consumerPublishableKey = consumerPublishableKey
)
}

@VisibleForTesting
internal fun setAccountNullable(
consumerSession: ConsumerSession?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.stripe.android.link.model.AccountStatus
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.inline.SignUpConsentAction
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.PaymentMethodCreateParams
Expand Down Expand Up @@ -69,4 +70,9 @@ internal interface LinkAccountManager {
* Confirms a verification code sent to the user.
*/
suspend fun confirmVerification(code: String): Result<LinkAccount>

/**
* Fetch all saved payment methods for the signed in consumer.
*/
suspend fun listPaymentDetails(paymentMethodTypes: Set<String>): Result<ConsumerPaymentDetails>
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,23 @@ internal class LinkApiRepository @Inject constructor(
}
}

override suspend fun listPaymentDetails(
paymentMethodTypes: Set<String>,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails> {
return stripeRepository.listPaymentDetails(
clientSecret = consumerSessionClientSecret,
paymentMethodTypes = paymentMethodTypes,
requestOptions = consumerPublishableKey?.let {
ApiRequest.Options(it)
} ?: ApiRequest.Options(
publishableKeyProvider(),
stripeAccountIdProvider()
)
)
}

private fun buildRequestOptions(
consumerAccountPublishableKey: String? = null,
): ApiRequest.Options {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.link.repositories

import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
Expand Down Expand Up @@ -71,4 +72,13 @@ internal interface LinkRepository {
consumerSessionClientSecret: String,
consumerPublishableKey: String?,
): Result<ConsumerSession>

/**
* Fetch all saved payment methods for the signed in consumer.
*/
suspend fun listPaymentDetails(
paymentMethodTypes: Set<String>,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails>
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.stripe.android.link.LinkAction
import com.stripe.android.link.LinkActivityResult
import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.LinkScreen
import com.stripe.android.link.NoLinkAccountFound
import com.stripe.android.link.NoLinkAccountFoundException
import com.stripe.android.link.linkViewModel
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.theme.DefaultLinkTheme
Expand Down Expand Up @@ -142,7 +142,7 @@ private fun Screens(

composable(LinkScreen.Verification.route) {
val linkAccount = getLinkAccount()
?: return@composable dismissWithResult(LinkActivityResult.Failed(NoLinkAccountFound()))
?: return@composable dismissWithResult(LinkActivityResult.Failed(NoLinkAccountFoundException()))
val viewModel: VerificationViewModel = linkViewModel { parentComponent ->
VerificationViewModel.factory(
parentComponent = parentComponent,
Expand Down
9 changes: 9 additions & 0 deletions link/src/test/java/com/stripe/android/link/TestFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,13 @@ internal object TestFactory {
)

val LINK_ACCOUNT = LinkAccount(CONSUMER_SESSION)

val CONSUMER_PAYMENT_DETAILS: ConsumerPaymentDetails = ConsumerPaymentDetails(
paymentDetails = listOf(
ConsumerPaymentDetails.Card(
id = "pm_123",
last4 = "4242",
)
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.stripe.android.link.repositories.FakeLinkRepository
import com.stripe.android.link.repositories.LinkRepository
import com.stripe.android.link.ui.inline.SignUpConsentAction
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
Expand Down Expand Up @@ -699,6 +700,53 @@ class DefaultLinkAccountManagerTest {
assertThat(linkEventsReporter.callCount).isEqualTo(1)
}

@Test
fun `listPaymentDetails returns error when repository call fails`() = runSuspendTest {
val error = AuthenticationException(StripeError())
val linkRepository = object : FakeLinkRepository() {
var paymentMethodTypes: Set<String>? = null
override suspend fun listPaymentDetails(
paymentMethodTypes: Set<String>,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails> {
this.paymentMethodTypes = paymentMethodTypes
return Result.failure(error)
}
}

val accountManager = accountManager(linkRepository = linkRepository)
accountManager.setAccountNullable(TestFactory.CONSUMER_SESSION, TestFactory.PUBLISHABLE_KEY)

val result = accountManager.listPaymentDetails(setOf("card"))

assertThat(result.exceptionOrNull()).isEqualTo(error)
assertThat(linkRepository.paymentMethodTypes).isEqualTo(setOf("card"))
}

@Test
fun `listPaymentDetails returns success when repository call succeeds`() = runSuspendTest {
val linkRepository = object : FakeLinkRepository() {
var paymentMethodTypes: Set<String>? = null
override suspend fun listPaymentDetails(
paymentMethodTypes: Set<String>,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails> {
this.paymentMethodTypes = paymentMethodTypes
return Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS)
}
}

val accountManager = accountManager(linkRepository = linkRepository)
accountManager.setAccountNullable(TestFactory.CONSUMER_SESSION, TestFactory.PUBLISHABLE_KEY)

val result = accountManager.listPaymentDetails(setOf("card"))

assertThat(result.getOrNull()).isEqualTo(TestFactory.CONSUMER_PAYMENT_DETAILS)
assertThat(linkRepository.paymentMethodTypes).isEqualTo(setOf("card"))
}

private fun runSuspendTest(testBody: suspend TestScope.() -> Unit) = runTest {
testBody()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.link.account

import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.TestFactory
import com.stripe.android.link.model.AccountStatus
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.inline.SignUpConsentAction
Expand Down Expand Up @@ -36,6 +37,7 @@ internal open class FakeLinkAccountManager : LinkAccountManager {
paymentMethodCreateParams = mock(),
)
)
var listPaymentDetailsResult: Result<ConsumerPaymentDetails> = Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS)
var linkAccountFromLookupResult: LinkAccount? = null
override var consumerPublishableKey: String? = null

Expand Down Expand Up @@ -86,4 +88,8 @@ internal open class FakeLinkAccountManager : LinkAccountManager {
override suspend fun confirmVerification(code: String): Result<LinkAccount> {
return confirmVerificationResult
}

override suspend fun listPaymentDetails(paymentMethodTypes: Set<String>): Result<ConsumerPaymentDetails> {
return listPaymentDetailsResult
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.link.repositories

import com.stripe.android.link.TestFactory
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.StripeIntent
Expand All @@ -13,6 +14,7 @@ open class FakeLinkRepository : LinkRepository {
var logOutResult = Result.success(TestFactory.CONSUMER_SESSION)
var startVerificationResult = Result.success(TestFactory.CONSUMER_SESSION)
var confirmVerificationResult = Result.success(TestFactory.CONSUMER_SESSION)
var listPaymentDetailsResult = Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS)

override suspend fun lookupConsumer(email: String) = lookupConsumerResult

Expand Down Expand Up @@ -55,4 +57,10 @@ open class FakeLinkRepository : LinkRepository {
consumerSessionClientSecret: String,
consumerPublishableKey: String?
) = confirmVerificationResult

override suspend fun listPaymentDetails(
paymentMethodTypes: Set<String>,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails> = listPaymentDetailsResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,31 @@ class LinkApiRepositoryTest {
assertThat(result.isFailure).isTrue()
}

@Test
fun `listPaymentDetails sends correct parameters`() = runTest {
val secret = "secret"
val consumerKey = "key"
linkRepository.listPaymentDetails(setOf("card"), secret, consumerKey)

verify(stripeRepository).listPaymentDetails(
eq(secret),
eq(setOf("card")),
eq(ApiRequest.Options(consumerKey))
)
}

@Test
fun `listPaymentDetails without consumerPublishableKey sends correct parameters`() = runTest {
val secret = "secret"
linkRepository.listPaymentDetails(setOf("card"), secret, null)

verify(stripeRepository).listPaymentDetails(
eq(secret),
eq(setOf("card")),
eq(ApiRequest.Options(PUBLISHABLE_KEY, STRIPE_ACCOUNT_ID))
)
}

private val cardPaymentMethodCreateParams =
FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams(
mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.stripe.android.model.BankStatuses
import com.stripe.android.model.CardMetadata
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.ConfirmSetupIntentParams
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.CreateFinancialConnectionsSessionForDeferredPaymentParams
import com.stripe.android.model.CreateFinancialConnectionsSessionParams
Expand Down Expand Up @@ -421,4 +422,12 @@ abstract class AbsFakeStripeRepository : StripeRepository {
override fun buildPaymentUserAgent(attribution: Set<String>): String {
TODO("Not yet implemented")
}

override suspend fun listPaymentDetails(
clientSecret: String,
paymentMethodTypes: Set<String>,
requestOptions: ApiRequest.Options
): Result<ConsumerPaymentDetails> {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.ConfirmSetupIntentParams
import com.stripe.android.model.ConfirmStripeIntentParams
import com.stripe.android.model.ConfirmStripeIntentParams.Companion.PARAM_CLIENT_SECRET
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.CreateFinancialConnectionsSessionForDeferredPaymentParams
import com.stripe.android.model.CreateFinancialConnectionsSessionParams
Expand All @@ -77,6 +78,7 @@ import com.stripe.android.model.StripeIntent
import com.stripe.android.model.Token
import com.stripe.android.model.TokenParams
import com.stripe.android.model.parsers.CardMetadataJsonParser
import com.stripe.android.model.parsers.ConsumerPaymentDetailsJsonParser
import com.stripe.android.model.parsers.ConsumerPaymentDetailsShareJsonParser
import com.stripe.android.model.parsers.ConsumerSessionJsonParser
import com.stripe.android.model.parsers.CustomerJsonParser
Expand Down Expand Up @@ -1435,6 +1437,27 @@ class StripeApiRepository @JvmOverloads internal constructor(
)
}

override suspend fun listPaymentDetails(
clientSecret: String,
paymentMethodTypes: Set<String>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this parameter needed? It looks like it's only set to emptySet and is otherwise unused (I could be missing something though)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. I was supposed to add it to the other interfaces as well. Just pushed a fix.

requestOptions: ApiRequest.Options
): Result<ConsumerPaymentDetails> {
return fetchStripeModelResult(
apiRequestFactory.createPost(
listConsumerPaymentDetailsUrl,
requestOptions,
mapOf(
"request_surface" to "android_payment_element",
"credentials" to mapOf(
"consumer_session_client_secret" to clientSecret
),
"types" to paymentMethodTypes.toList()
)
),
ConsumerPaymentDetailsJsonParser
)
}

private suspend fun retrieveElementsSession(
params: ElementsSessionParams,
options: ApiRequest.Options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.stripe.android.model.BankStatuses
import com.stripe.android.model.CardMetadata
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.ConfirmSetupIntentParams
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.CreateFinancialConnectionsSessionForDeferredPaymentParams
import com.stripe.android.model.CreateFinancialConnectionsSessionParams
Expand Down Expand Up @@ -380,6 +381,13 @@ interface StripeRepository {
params: Map<String, String>? = null
): Result<MobileCardElementConfig>

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
suspend fun listPaymentDetails(
clientSecret: String,
paymentMethodTypes: Set<String>,
requestOptions: ApiRequest.Options
): Result<ConsumerPaymentDetails>

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun buildPaymentUserAgent(attribution: Set<String> = emptySet()): String
}
Loading
Loading