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

Add minimal user key auth support to PaymentSheet #4481

Merged
merged 6 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions payments-core/api/payments-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,7 @@ public final class com/stripe/android/model/ConfirmPaymentIntentParams : com/str
public static final fun create (Ljava/lang/String;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
public static final fun create (Ljava/lang/String;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
public static final fun createAlipay (Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
public static final fun createForDashboard$payments_core_release (Ljava/lang/String;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
lng-stripe marked this conversation as resolved.
Show resolved Hide resolved
public static final fun createWithPaymentMethodCreateParams (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
public static final fun createWithPaymentMethodCreateParams (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
public static final fun createWithPaymentMethodCreateParams (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams;
Expand Down Expand Up @@ -3383,8 +3384,8 @@ public final class com/stripe/android/model/PaymentMethodOptionsParams$Card : co
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/stripe/android/model/PaymentMethodOptionsParams$Card;
public static synthetic fun copy$default (Lcom/stripe/android/model/PaymentMethodOptionsParams$Card;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/model/PaymentMethodOptionsParams$Card;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/stripe/android/model/PaymentMethodOptionsParams$Card;
public static synthetic fun copy$default (Lcom/stripe/android/model/PaymentMethodOptionsParams$Card;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/stripe/android/model/PaymentMethodOptionsParams$Card;
michelleb-stripe marked this conversation as resolved.
Show resolved Hide resolved
public fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getCvc ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ data class ConfirmPaymentIntentParams internal constructor(

/**
* Create the parameters necessary for confirming a PaymentIntent while attaching a
* PaymentMethod that already exits.
* PaymentMethod that already exists.
*
* @param paymentMethodId the ID of the PaymentMethod that is being attached to the
* PaymentIntent being confirmed
Expand Down Expand Up @@ -503,5 +503,20 @@ data class ConfirmPaymentIntentParams internal constructor(
returnUrl = "stripe://return_url"
)
}

@JvmStatic
internal fun createForDashboard(
clientSecret: String,
paymentMethodId: String
): ConfirmPaymentIntentParams {
// Dashboard only supports a specific payment flow today.
return ConfirmPaymentIntentParams(
clientSecret = clientSecret,
paymentMethodId = paymentMethodId,
paymentMethodOptions = PaymentMethodOptionsParams.Card(moto = true),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

iOS

savePaymentMethod = false,
useStripeSdk = true,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,33 @@ sealed class PaymentMethodOptionsParams(
}

@Parcelize
data class Card(
data class Card internal constructor(
var cvc: String? = null,
var network: String? = null
var network: String? = null,
internal var moto: Boolean? = null
) : PaymentMethodOptionsParams(PaymentMethod.Type.Card) {

constructor(
cvc: String? = null,
network: String? = null,
) : this(
cvc = cvc,
network = network,
moto = null
)

override fun createTypeParams(): List<Pair<String, Any?>> {
return listOf(
PARAM_CVC to cvc,
PARAM_NETWORK to network
PARAM_NETWORK to network,
PARAM_MOTO to moto,
)
}

private companion object {
private const val PARAM_CVC = "cvc"
private const val PARAM_NETWORK = "network"
private const val PARAM_MOTO = "moto"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ data class ApiRequest internal constructor(
internal val idempotencyKey: String? = null
) : Parcelable {

internal val apiKeyIsUserKey: Boolean
get() = apiKey.startsWith("uk_")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

iOS


/**
* Dedicated constructor for injection.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.networking

import android.system.Os
import com.stripe.android.ApiVersion
import com.stripe.android.AppInfo
import com.stripe.android.Stripe
Expand Down Expand Up @@ -64,6 +65,13 @@ internal sealed class RequestHeadersFactory {
HEADER_AUTHORIZATION to "Bearer ${options.apiKey}"
).plus(
stripeClientUserAgentHeaderFactory.create(appInfo)
).plus(
if (options.apiKeyIsUserKey) {
val isLiveMode = Os.getenv("Stripe-Livemode") != "false"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

iOS

mapOf(HEADER_STRIPE_LIVEMODE to isLiveMode.toString())
} else {
emptyMap()
}
).plus(
options.stripeAccount?.let {
mapOf(HEADER_STRIPE_ACCOUNT to it)
Expand Down Expand Up @@ -152,6 +160,7 @@ internal sealed class RequestHeadersFactory {
internal const val HEADER_ACCEPT = "Accept"
internal const val HEADER_STRIPE_VERSION = "Stripe-Version"
internal const val HEADER_STRIPE_ACCOUNT = "Stripe-Account"
internal const val HEADER_STRIPE_LIVEMODE = "Stripe-Livemode"
internal const val HEADER_AUTHORIZATION = "Authorization"
internal const val HEADER_IDEMPOTENCY_KEY = "Idempotency-Key"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.stripe.android.model.CardMetadata
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.Customer
import com.stripe.android.model.ListPaymentMethodsParams
import com.stripe.android.model.PaymentIntent
Expand Down Expand Up @@ -186,11 +187,25 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
confirmPaymentIntentParams: ConfirmPaymentIntentParams,
options: ApiRequest.Options,
expandFields: List<String>
): PaymentIntent? {
return confirmPaymentIntentInternal(
confirmPaymentIntentParams = confirmPaymentIntentParams.maybeForDashboard(options),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

iOS

options = options,
expandFields = expandFields
)
}

private suspend fun confirmPaymentIntentInternal(
confirmPaymentIntentParams: ConfirmPaymentIntentParams,
options: ApiRequest.Options,
expandFields: List<String>
): PaymentIntent? {
val params = fraudDetectionDataParamsUtils.addFraudDetectionData(
// Add payment_user_agent if the Payment Method is being created on this call
maybeAddPaymentUserAgent(
confirmPaymentIntentParams.toParamMap(),
confirmPaymentIntentParams.toParamMap()
// Omit client_secret with user key auth.
.let { if (options.apiKeyIsUserKey) it.minus(PARAM_CLIENT_SECRET) else it },
Copy link
Contributor Author

Choose a reason for hiding this comment

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

iOS

confirmPaymentIntentParams.paymentMethodCreateParams,
confirmPaymentIntentParams.sourceParams
).plus(createExpandParam(expandFields)),
Expand Down Expand Up @@ -235,16 +250,24 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
options: ApiRequest.Options,
expandFields: List<String>
): PaymentIntent? {
val paymentIntentId = PaymentIntent.ClientSecret(clientSecret).paymentIntentId
val paymentIntentId: String
val params: Map<String, Any?>
if (options.apiKeyIsUserKey) {
lng-stripe marked this conversation as resolved.
Show resolved Hide resolved
require(clientSecret.startsWith("pi_")) {
"`clientSecret` format does not match expected identifier formatting."
}
paymentIntentId = clientSecret
params = createExpandParam(expandFields)
} else {
paymentIntentId = PaymentIntent.ClientSecret(clientSecret).paymentIntentId
params = createClientSecretParam(clientSecret, expandFields)
}
michelleb-stripe marked this conversation as resolved.
Show resolved Hide resolved
val url = getRetrievePaymentIntentUrl(paymentIntentId)

fireFraudDetectionDataRequest()

return fetchStripeModel(
apiRequestFactory.createGet(
getRetrievePaymentIntentUrl(paymentIntentId),
options,
createClientSecretParam(clientSecret, expandFields)
),
apiRequestFactory.createGet(url, options, params),
PaymentIntentJsonParser()
) {
fireAnalyticsRequest(
Expand Down Expand Up @@ -1137,6 +1160,9 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
parser: PaymentMethodPreferenceJsonParser<T>,
analyticsEvent: PaymentAnalyticsEvent
): T? {
// Unsupported for user key sessions.
if (options.apiKeyIsUserKey) return null
Copy link
Contributor Author

Choose a reason for hiding this comment

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

iOS


fireFraudDetectionDataRequest()

val params = createClientSecretParam(
Expand Down Expand Up @@ -1352,6 +1378,24 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
)
} ?: params

private suspend fun ConfirmPaymentIntentParams.maybeForDashboard(
options: ApiRequest.Options,
): ConfirmPaymentIntentParams {
val paymentMethodCreateParams = paymentMethodCreateParams
lng-stripe marked this conversation as resolved.
Show resolved Hide resolved
if (!options.apiKeyIsUserKey || paymentMethodCreateParams == null) {
return this
}

// For user key auth, we must create the PM first.
val paymentMethodId = requireNotNull(
createPaymentMethod(paymentMethodCreateParams, options)?.id
)
return ConfirmPaymentIntentParams.createForDashboard(
clientSecret = clientSecret,
paymentMethodId = paymentMethodId
)
}

private sealed class DnsCacheData {
data class Success(
val originalDnsCacheTtl: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,34 @@ internal class StripeApiRepositoryTest {
)
}

@Test
fun confirmPaymentIntent_withApiUserKey_sendsValidRequest() =
testDispatcher.runBlockingTest {
val apiKey = "uk_12345"
val clientSecret = "pi_12345_secret_fake"
val confirmPaymentIntentParams = ConfirmPaymentIntentParams.create(clientSecret)
whenever(stripeNetworkClient.executeRequest(any<ApiRequest>()))
.thenReturn(
StripeResponse(
200,
PaymentIntentFixtures.PI_REQUIRES_MASTERCARD_3DS2_JSON.toString(),
emptyMap()
)
)

create().confirmPaymentIntent(
confirmPaymentIntentParams = confirmPaymentIntentParams,
options = ApiRequest.Options(apiKey),
)

verify(stripeNetworkClient).executeRequest(apiRequestArgumentCaptor.capture())
val apiRequest = apiRequestArgumentCaptor.firstValue
assertThat(apiRequest.baseUrl)
.contains("pi_12345/confirm")
assertThat(apiRequest.params)
.doesNotContainKey(ConfirmStripeIntentParams.PARAM_CLIENT_SECRET)
}

@Test
fun confirmSetupIntent_setsCorrectPaymentUserAgent() =
testDispatcher.runBlockingTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import com.stripe.android.paymentsheet.model.PaymentOptionFactory
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.model.SavedSelection
import com.stripe.android.paymentsheet.model.SetupIntentClientSecret
import com.stripe.android.paymentsheet.model.getTypedClientSecret
import com.stripe.android.paymentsheet.validate
import com.stripe.android.ui.core.forms.resources.ResourceRepository
import dagger.Lazy
Expand Down Expand Up @@ -264,7 +265,7 @@ internal class DefaultFlowController @Inject internal constructor(
initData: InitData
) {
val confirmParamsFactory =
ConfirmStripeIntentParamsFactory.createFactory(initData.clientSecret)
ConfirmStripeIntentParamsFactory.createFactory(initData.preferredClientSecret())
lng-stripe marked this conversation as resolved.
Show resolved Hide resolved

when (paymentSelection) {
is PaymentSelection.Saved -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package com.stripe.android.paymentsheet.flowcontroller

import com.stripe.android.core.Logger
import com.stripe.android.core.injection.IOContext
import com.stripe.android.googlepaylauncher.GooglePayEnvironment
import com.stripe.android.googlepaylauncher.GooglePayRepository
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.StripeIntent
import com.stripe.android.core.injection.IOContext
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PrefsRepository
import com.stripe.android.paymentsheet.model.ClientSecret
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.model.SavedSelection
import com.stripe.android.paymentsheet.model.StripeIntentValidator
import com.stripe.android.paymentsheet.model.SupportedPaymentMethod
import com.stripe.android.paymentsheet.model.getTypedClientSecret
import com.stripe.android.paymentsheet.repositories.CustomerRepository
import com.stripe.android.paymentsheet.repositories.StripeIntentRepository
import kotlinx.coroutines.flow.first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.stripe.android.model.StripeIntent
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.model.ClientSecret
import com.stripe.android.paymentsheet.model.SavedSelection
import com.stripe.android.paymentsheet.model.getTypedClientSecret
import kotlinx.parcelize.Parcelize

@Parcelize
Expand All @@ -17,4 +18,7 @@ internal data class InitData(
val paymentMethods: List<PaymentMethod>,
val savedSelection: SavedSelection,
val isGooglePayReady: Boolean
) : Parcelable
) : Parcelable {
fun preferredClientSecret(): ClientSecret =
stripeIntent.getTypedClientSecret() ?: clientSecret
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal abstract class FlowControllerModule {
fun provideClientSecret(
viewModel: FlowControllerViewModel
): ClientSecret {
return viewModel.initData.clientSecret
return viewModel.initData.preferredClientSecret()
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet.model
import android.os.Parcelable
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.SetupIntent
import com.stripe.android.model.StripeIntent
import kotlinx.parcelize.Parcelize
import java.security.InvalidParameterException

Expand Down Expand Up @@ -45,3 +46,13 @@ internal data class SetupIntentClientSecret(
}
}
}

internal fun StripeIntent.getTypedClientSecret(): ClientSecret? =
clientSecret?.let {
when (this) {
is PaymentIntent ->
PaymentIntentClientSecret(it)
is SetupIntent ->
SetupIntentClientSecret(it)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal class PaymentSheetActivityTest {
private val intent = contract.createIntent(
context,
PaymentSheetContract.Args(
PaymentIntentClientSecret("client_secret"),
PaymentIntentClientSecret(PaymentSheetFixtures.CLIENT_SECRET),
PaymentSheetFixtures.CONFIG_CUSTOMER,
statusBarColor = PaymentSheetFixtures.STATUS_BAR_COLOR,
)
Expand Down Expand Up @@ -325,7 +325,7 @@ internal class PaymentSheetActivityTest {
.isEqualTo(
ConfirmPaymentIntentParams.createWithPaymentMethodId(
paymentMethodId = "pm_123456789",
clientSecret = "client_secret"
clientSecret = PaymentSheetFixtures.CLIENT_SECRET
)
)
}
Expand Down Expand Up @@ -591,7 +591,7 @@ internal class PaymentSheetActivityTest {
val intent = contract.createIntent(
activity,
PaymentSheetContract.Args(
PaymentIntentClientSecret("client_secret"),
PaymentIntentClientSecret(PaymentSheetFixtures.CLIENT_SECRET),
PaymentSheetFixtures.CONFIG_CUSTOMER
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal object PaymentSheetFixtures {
get() = "#121212".toColorInt()

internal const val MERCHANT_DISPLAY_NAME = "Merchant, Inc."
internal const val CLIENT_SECRET = "client_secret"
internal const val CLIENT_SECRET = "pi_1F7J1aCRMbs6FrXfaJcvbxF6_secret_mIuDLsSfoo1m6s"

internal val PAYMENT_INTENT_CLIENT_SECRET = PaymentIntentClientSecret(
CLIENT_SECRET
Expand Down