Skip to content

Commit

Permalink
Link API for updating payment details (#9717)
Browse files Browse the repository at this point in the history
  • Loading branch information
toluo-stripe authored Dec 12, 2024
1 parent f68875e commit b820a5a
Show file tree
Hide file tree
Showing 17 changed files with 345 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSignUpConsentAction
Expand Down Expand Up @@ -263,6 +264,17 @@ internal class DefaultLinkAccountManager @Inject constructor(
)
}

override suspend fun updatePaymentDetails(
updateParams: ConsumerPaymentDetailsUpdateParams
): Result<ConsumerPaymentDetails> {
val clientSecret = linkAccount.value?.clientSecret ?: return Result.failure(NoLinkAccountFoundException())
return linkRepository.updatePaymentDetails(
updateParams = updateParams,
consumerSessionClientSecret = clientSecret,
consumerPublishableKey = consumerPublishableKey
)
}

@VisibleForTesting
internal fun setAccountNullable(
consumerSession: ConsumerSession?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ 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.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.PaymentMethodCreateParams
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

@SuppressWarnings("TooManyFunctions")
internal interface LinkAccountManager {
val linkAccount: StateFlow<LinkAccount?>
val accountStatus: Flow<AccountStatus>
Expand Down Expand Up @@ -80,4 +82,9 @@ internal interface LinkAccountManager {
* Delete the payment method from the signed in consumer account.
*/
suspend fun deletePaymentDetails(paymentDetailsId: String): Result<Unit>

/**
* Update an existing payment method in the signed in consumer account.
*/
suspend fun updatePaymentDetails(updateParams: ConsumerPaymentDetailsUpdateParams): Result<ConsumerPaymentDetails>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetailsCreateParams
import com.stripe.android.model.ConsumerPaymentDetailsCreateParams.Card.Companion.extraConfirmationParams
import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
Expand Down Expand Up @@ -233,6 +234,23 @@ internal class LinkApiRepository @Inject constructor(
)
}

override suspend fun updatePaymentDetails(
updateParams: ConsumerPaymentDetailsUpdateParams,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails> {
return stripeRepository.updatePaymentDetails(
clientSecret = consumerSessionClientSecret,
paymentDetailsUpdateParams = updateParams,
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
Expand Up @@ -2,6 +2,7 @@ package com.stripe.android.link.repositories

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

/**
* Update an existing payment method in the consumer account.
*/
suspend fun updatePaymentDetails(
updateParams: ConsumerPaymentDetailsUpdateParams,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails>
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.mockito.kotlin.any

class DefaultLinkAccountManagerTest {

Expand Down Expand Up @@ -772,6 +773,35 @@ class DefaultLinkAccountManagerTest {
assertThat(result.getOrNull()).isEqualTo(Unit)
}

@Test
fun `updatePaymentDetails returns error when repository call fails`() = runSuspendTest {
val error = AuthenticationException(StripeError())
val linkRepository = FakeLinkRepository()

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

linkRepository.updatePaymentDetailsResult = Result.failure(error)

val result = accountManager.updatePaymentDetails(any())

assertThat(result.exceptionOrNull()).isEqualTo(error)
}

@Test
fun `updatePaymentDetails returns success when repository call succeeds`() = runSuspendTest {
val linkRepository = FakeLinkRepository()

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

linkRepository.updatePaymentDetailsResult = Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS)

val result = accountManager.updatePaymentDetails(any())

assertThat(result.getOrNull()).isEqualTo(TestFactory.CONSUMER_PAYMENT_DETAILS)
}

private fun runSuspendTest(testBody: suspend TestScope.() -> Unit) = runTest {
testBody()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.PaymentMethodCreateParams
Expand All @@ -31,6 +32,7 @@ internal open class FakeLinkAccountManager : LinkAccountManager {
value = TestFactory.LINK_NEW_PAYMENT_DETAILS
)
var listPaymentDetailsResult: Result<ConsumerPaymentDetails> = Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS)
var updatePaymentDetailsResult = Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS)
var linkAccountFromLookupResult: LinkAccount? = null
override var consumerPublishableKey: String? = null

Expand Down Expand Up @@ -87,4 +89,9 @@ internal open class FakeLinkAccountManager : LinkAccountManager {
}

override suspend fun deletePaymentDetails(paymentDetailsId: String) = Result.success(Unit)
override suspend fun updatePaymentDetails(
updateParams: ConsumerPaymentDetailsUpdateParams
): Result<ConsumerPaymentDetails> {
return updatePaymentDetailsResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.stripe.android.link.analytics

import com.google.common.truth.Truth.assertThat
import com.stripe.android.link.LinkActivityResult
import com.stripe.android.model.PaymentMethod
import org.junit.Test
import org.mockito.Mockito.mock

internal class LinkAnalyticsHelperTest {
@Test
Expand All @@ -26,7 +26,17 @@ internal class LinkAnalyticsHelperTest {
}
}
val analyticsHelper = LinkAnalyticsHelper(eventReporter)
analyticsHelper.onLinkResult(LinkActivityResult.Completed(mock()))
analyticsHelper.onLinkResult(
linkActivityResult = LinkActivityResult.Completed(
paymentMethod = PaymentMethod(
id = null,
created = null,
liveMode = false,
code = null,
type = null
)
)
)
assertThat(eventReporter.calledCount).isEqualTo(1)
}

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

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

override suspend fun lookupConsumer(email: String) = lookupConsumerResult
Expand Down Expand Up @@ -70,4 +72,10 @@ open class FakeLinkRepository : LinkRepository {
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<Unit> = deletePaymentDetailsResult

override suspend fun updatePaymentDetails(
updateParams: ConsumerPaymentDetailsUpdateParams,
consumerSessionClientSecret: String,
consumerPublishableKey: String?
): Result<ConsumerPaymentDetails> = updatePaymentDetailsResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.stripe.android.link.repositories
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.TestFactory
import com.stripe.android.link.model.PaymentDetailsFixtures
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
import com.stripe.android.model.ConsumerSessionSignup
Expand Down Expand Up @@ -549,6 +551,72 @@ class LinkApiRepositoryTest {
assertThat(result.exceptionOrNull()).isEqualTo(error)
}

@Test
fun `updatePaymentDetails sends correct parameters`() = runTest {
val secret = "secret"
val params = ConsumerPaymentDetailsUpdateParams("id")
val consumerKey = "key"

linkRepository.updatePaymentDetails(
params,
secret,
consumerKey
)

verify(stripeRepository).updatePaymentDetails(
eq(secret),
eq(params),
eq(ApiRequest.Options(consumerKey))
)
}

@Test
fun `updatePaymentDetails without consumerPublishableKey sends correct parameters`() = runTest {
val secret = "secret"
val params = ConsumerPaymentDetailsUpdateParams("id")

linkRepository.updatePaymentDetails(
params,
secret,
null
)

verify(stripeRepository).updatePaymentDetails(
eq(secret),
eq(params),
eq(ApiRequest.Options(PUBLISHABLE_KEY, STRIPE_ACCOUNT_ID))
)
}

@Test
fun `updatePaymentDetails returns successful result`() = runTest {
whenever(stripeRepository.updatePaymentDetails(any(), any(), any()))
.thenReturn(Result.success(TestFactory.CONSUMER_PAYMENT_DETAILS))

val result = linkRepository.updatePaymentDetails(
ConsumerPaymentDetailsUpdateParams("id"),
"secret",
"key"
)

assertThat(result.getOrNull()).isEqualTo(TestFactory.CONSUMER_PAYMENT_DETAILS)
}

@Test
fun `updatePaymentDetails returns error result when repository fails`() = runTest {
val error = RuntimeException("error")
whenever(stripeRepository.updatePaymentDetails(any(), any(), any()))
.thenReturn(Result.failure(error))

val result = linkRepository.updatePaymentDetails(
ConsumerPaymentDetailsUpdateParams("id"),
"secret",
"key"
)

assertThat(result.exceptionOrNull()).isEqualTo(error)
}

private val cardPaymentMethodCreateParams =
FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams(
mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.CreateFinancialConnectionsSessionForDeferredPaymentParams
import com.stripe.android.model.CreateFinancialConnectionsSessionParams
Expand Down Expand Up @@ -438,4 +439,12 @@ abstract class AbsFakeStripeRepository : StripeRepository {
): Result<Unit> {
TODO("Not yet implemented")
}

override suspend fun updatePaymentDetails(
clientSecret: String,
paymentDetailsUpdateParams: ConsumerPaymentDetailsUpdateParams,
requestOptions: ApiRequest.Options
): Result<ConsumerPaymentDetails> {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ 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.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.CreateFinancialConnectionsSessionForDeferredPaymentParams
import com.stripe.android.model.CreateFinancialConnectionsSessionParams
Expand Down Expand Up @@ -1480,6 +1481,28 @@ class StripeApiRepository @JvmOverloads internal constructor(
}
}

override suspend fun updatePaymentDetails(
clientSecret: String,
paymentDetailsUpdateParams: ConsumerPaymentDetailsUpdateParams,
requestOptions: ApiRequest.Options
): Result<ConsumerPaymentDetails> {
return fetchStripeModelResult(
apiRequestFactory.createPost(
getConsumerPaymentDetailsUrl(paymentDetailsUpdateParams.id),
requestOptions,
mapOf(
"request_surface" to "android_payment_element",
"credentials" to mapOf(
"consumer_session_client_secret" to clientSecret
)
).plus(
paymentDetailsUpdateParams.toParamMap()
)
),
ConsumerPaymentDetailsJsonParser
)
}

private suspend fun retrieveElementsSession(
params: ElementsSessionParams,
options: ApiRequest.Options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.CreateFinancialConnectionsSessionForDeferredPaymentParams
import com.stripe.android.model.CreateFinancialConnectionsSessionParams
Expand Down Expand Up @@ -395,6 +396,13 @@ interface StripeRepository {
requestOptions: ApiRequest.Options
): Result<Unit>

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
suspend fun updatePaymentDetails(
clientSecret: String,
paymentDetailsUpdateParams: ConsumerPaymentDetailsUpdateParams,
requestOptions: ApiRequest.Options
): Result<ConsumerPaymentDetails>

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

0 comments on commit b820a5a

Please sign in to comment.