Skip to content

Commit

Permalink
LUXE next action support in PaymentSheet (#5318)
Browse files Browse the repository at this point in the history
  • Loading branch information
michelleb-stripe committed Aug 11, 2022
1 parent abcc652 commit d5791b2
Show file tree
Hide file tree
Showing 23 changed files with 1,109 additions and 143 deletions.
37 changes: 21 additions & 16 deletions payments-core/api/payments-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -3055,6 +3055,10 @@ public final class com/stripe/android/model/ListPaymentMethodsParams$Creator : a
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/model/LuxeNextActionRepository$Companion {
public final fun getInstance ()Lcom/stripe/android/model/LuxeNextActionRepository;
}

public final class com/stripe/android/model/MandateDataParams : android/os/Parcelable, com/stripe/android/model/StripeParamsModel {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
Expand Down Expand Up @@ -3309,24 +3313,24 @@ public final class com/stripe/android/model/PaymentMethod : com/stripe/android/c
public final field upi Lcom/stripe/android/model/PaymentMethod$Upi;
public final field usBankAccount Lcom/stripe/android/model/PaymentMethod$USBankAccount;
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()Lcom/stripe/android/model/PaymentMethod$Ideal;
public final fun component11 ()Lcom/stripe/android/model/PaymentMethod$SepaDebit;
public final fun component12 ()Lcom/stripe/android/model/PaymentMethod$AuBecsDebit;
public final fun component13 ()Lcom/stripe/android/model/PaymentMethod$BacsDebit;
public final fun component14 ()Lcom/stripe/android/model/PaymentMethod$Sofort;
public final fun component15 ()Lcom/stripe/android/model/PaymentMethod$Upi;
public final fun component16 ()Lcom/stripe/android/model/PaymentMethod$Netbanking;
public final fun component17 ()Lcom/stripe/android/model/PaymentMethod$USBankAccount;
public final fun component10 ()Lcom/stripe/android/model/PaymentMethod$Fpx;
public final fun component11 ()Lcom/stripe/android/model/PaymentMethod$Ideal;
public final fun component12 ()Lcom/stripe/android/model/PaymentMethod$SepaDebit;
public final fun component13 ()Lcom/stripe/android/model/PaymentMethod$AuBecsDebit;
public final fun component14 ()Lcom/stripe/android/model/PaymentMethod$BacsDebit;
public final fun component15 ()Lcom/stripe/android/model/PaymentMethod$Sofort;
public final fun component16 ()Lcom/stripe/android/model/PaymentMethod$Upi;
public final fun component17 ()Lcom/stripe/android/model/PaymentMethod$Netbanking;
public final fun component18 ()Lcom/stripe/android/model/PaymentMethod$USBankAccount;
public final fun component2 ()Ljava/lang/Long;
public final fun component3 ()Z
public final fun component4 ()Lcom/stripe/android/model/PaymentMethod$Type;
public final fun component5 ()Lcom/stripe/android/model/PaymentMethod$BillingDetails;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Lcom/stripe/android/model/PaymentMethod$Card;
public final fun component8 ()Lcom/stripe/android/model/PaymentMethod$CardPresent;
public final fun component9 ()Lcom/stripe/android/model/PaymentMethod$Fpx;
public final fun copy (Ljava/lang/String;Ljava/lang/Long;ZLcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/model/PaymentMethod$BillingDetails;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethod$Card;Lcom/stripe/android/model/PaymentMethod$CardPresent;Lcom/stripe/android/model/PaymentMethod$Fpx;Lcom/stripe/android/model/PaymentMethod$Ideal;Lcom/stripe/android/model/PaymentMethod$SepaDebit;Lcom/stripe/android/model/PaymentMethod$AuBecsDebit;Lcom/stripe/android/model/PaymentMethod$BacsDebit;Lcom/stripe/android/model/PaymentMethod$Sofort;Lcom/stripe/android/model/PaymentMethod$Upi;Lcom/stripe/android/model/PaymentMethod$Netbanking;Lcom/stripe/android/model/PaymentMethod$USBankAccount;)Lcom/stripe/android/model/PaymentMethod;
public static synthetic fun copy$default (Lcom/stripe/android/model/PaymentMethod;Ljava/lang/String;Ljava/lang/Long;ZLcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/model/PaymentMethod$BillingDetails;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethod$Card;Lcom/stripe/android/model/PaymentMethod$CardPresent;Lcom/stripe/android/model/PaymentMethod$Fpx;Lcom/stripe/android/model/PaymentMethod$Ideal;Lcom/stripe/android/model/PaymentMethod$SepaDebit;Lcom/stripe/android/model/PaymentMethod$AuBecsDebit;Lcom/stripe/android/model/PaymentMethod$BacsDebit;Lcom/stripe/android/model/PaymentMethod$Sofort;Lcom/stripe/android/model/PaymentMethod$Upi;Lcom/stripe/android/model/PaymentMethod$Netbanking;Lcom/stripe/android/model/PaymentMethod$USBankAccount;ILjava/lang/Object;)Lcom/stripe/android/model/PaymentMethod;
public final fun component5 ()Lcom/stripe/android/model/PaymentMethod$Type;
public final fun component6 ()Lcom/stripe/android/model/PaymentMethod$BillingDetails;
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()Lcom/stripe/android/model/PaymentMethod$Card;
public final fun component9 ()Lcom/stripe/android/model/PaymentMethod$CardPresent;
public final fun copy (Ljava/lang/String;Ljava/lang/Long;ZLjava/lang/String;Lcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/model/PaymentMethod$BillingDetails;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethod$Card;Lcom/stripe/android/model/PaymentMethod$CardPresent;Lcom/stripe/android/model/PaymentMethod$Fpx;Lcom/stripe/android/model/PaymentMethod$Ideal;Lcom/stripe/android/model/PaymentMethod$SepaDebit;Lcom/stripe/android/model/PaymentMethod$AuBecsDebit;Lcom/stripe/android/model/PaymentMethod$BacsDebit;Lcom/stripe/android/model/PaymentMethod$Sofort;Lcom/stripe/android/model/PaymentMethod$Upi;Lcom/stripe/android/model/PaymentMethod$Netbanking;Lcom/stripe/android/model/PaymentMethod$USBankAccount;)Lcom/stripe/android/model/PaymentMethod;
public static synthetic fun copy$default (Lcom/stripe/android/model/PaymentMethod;Ljava/lang/String;Ljava/lang/Long;ZLjava/lang/String;Lcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/model/PaymentMethod$BillingDetails;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethod$Card;Lcom/stripe/android/model/PaymentMethod$CardPresent;Lcom/stripe/android/model/PaymentMethod$Fpx;Lcom/stripe/android/model/PaymentMethod$Ideal;Lcom/stripe/android/model/PaymentMethod$SepaDebit;Lcom/stripe/android/model/PaymentMethod$AuBecsDebit;Lcom/stripe/android/model/PaymentMethod$BacsDebit;Lcom/stripe/android/model/PaymentMethod$Sofort;Lcom/stripe/android/model/PaymentMethod$Upi;Lcom/stripe/android/model/PaymentMethod$Netbanking;Lcom/stripe/android/model/PaymentMethod$USBankAccount;ILjava/lang/Object;)Lcom/stripe/android/model/PaymentMethod;
public fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public static final fun fromJson (Lorg/json/JSONObject;)Lcom/stripe/android/model/PaymentMethod;
Expand Down Expand Up @@ -3446,6 +3450,7 @@ public final class com/stripe/android/model/PaymentMethod$Builder : com/stripe/a
public final fun setBillingDetails (Lcom/stripe/android/model/PaymentMethod$BillingDetails;)Lcom/stripe/android/model/PaymentMethod$Builder;
public final fun setCard (Lcom/stripe/android/model/PaymentMethod$Card;)Lcom/stripe/android/model/PaymentMethod$Builder;
public final fun setCardPresent (Lcom/stripe/android/model/PaymentMethod$CardPresent;)Lcom/stripe/android/model/PaymentMethod$Builder;
public final fun setCode (Ljava/lang/String;)Lcom/stripe/android/model/PaymentMethod$Builder;
public final fun setCreated (Ljava/lang/Long;)Lcom/stripe/android/model/PaymentMethod$Builder;
public final fun setCustomerId (Ljava/lang/String;)Lcom/stripe/android/model/PaymentMethod$Builder;
public final fun setFpx (Lcom/stripe/android/model/PaymentMethod$Fpx;)Lcom/stripe/android/model/PaymentMethod$Builder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.stripe.android

import androidx.annotation.IntDef
import androidx.annotation.VisibleForTesting
import com.stripe.android.core.model.StripeModel
import com.stripe.android.model.LuxeNextActionRepository
import com.stripe.android.model.StripeIntent

/**
Expand All @@ -16,60 +18,74 @@ abstract class StripeIntentResult<out T : StripeIntent> internal constructor(
abstract val intent: T
abstract val failureMessage: String?

@VisibleForTesting
internal var luxeNextActionRepository: LuxeNextActionRepository =
LuxeNextActionRepository.Instance

@Outcome
@get:Outcome
val outcome: Int
get() = determineOutcome(intent.status, outcomeFromFlow)
get() = determineOutcome(intent, outcomeFromFlow)

@StripeIntentResult.Outcome
private fun determineOutcome(
stripeIntentStatus: StripeIntent.Status?,
stripeIntent: StripeIntent,
@StripeIntentResult.Outcome outcome: Int
): Int {
if (outcome != Outcome.UNKNOWN) {
return outcome
}

return when (stripeIntentStatus) {
StripeIntent.Status.RequiresAction -> {
if (isNextActionSuccessState(intent)) {
Outcome.SUCCEEDED
} else {
return getOutcome(stripeIntent)
}

private fun getOutcome(stripeIntent: StripeIntent): Int {
return luxeNextActionRepository.getPostAuthorizeIntentOutcome(
stripeIntent
) ?: run {
when (stripeIntent.status) {
StripeIntent.Status.RequiresAction -> {
if (stripeIntent.nextActionData == null) {
Outcome.FAILED
} else if (isRequireActionSuccessState(intent)) {
Outcome.SUCCEEDED
} else {
Outcome.CANCELED
}
}
StripeIntent.Status.Canceled -> {
Outcome.CANCELED
}
}
StripeIntent.Status.Canceled -> {
Outcome.CANCELED
}
StripeIntent.Status.RequiresPaymentMethod -> {
Outcome.FAILED
}
StripeIntent.Status.Succeeded,
StripeIntent.Status.RequiresCapture,
StripeIntent.Status.RequiresConfirmation -> {
Outcome.SUCCEEDED
}
StripeIntent.Status.Processing -> {
if (intent.paymentMethod?.type?.hasDelayedSettlement() == true) {
StripeIntent.Status.RequiresPaymentMethod -> {
Outcome.FAILED
}
StripeIntent.Status.Succeeded,
StripeIntent.Status.RequiresCapture,
StripeIntent.Status.RequiresConfirmation -> {
Outcome.SUCCEEDED
} else {
}
StripeIntent.Status.Processing -> {
if (intent.paymentMethod?.type?.hasDelayedSettlement() == true) {
Outcome.SUCCEEDED
} else {
Outcome.UNKNOWN
}
}
else -> {
Outcome.UNKNOWN
}
}
else -> {
Outcome.UNKNOWN
}
}
}

/**
* Check if the [nextAction] is expected state after a successful on-session transaction
* Check if the [stripeIntent] is in expected state after a successful on-session transaction
* e.g. for voucher-based payment methods like OXXO that require out-of-band payment and
* ACHv2 payments which requires verification of the customers bank details before
* confirming payment.
*/
private fun isNextActionSuccessState(nextAction: StripeIntent): Boolean {
return when (nextAction.nextActionType) {
private fun isRequireActionSuccessState(stripeIntent: StripeIntent): Boolean {
return when (stripeIntent.nextActionType) {
StripeIntent.NextActionType.RedirectToUrl,
StripeIntent.NextActionType.UseStripeSdk,
StripeIntent.NextActionType.AlipayRedirect,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.stripe.android.model

import android.net.Uri
import org.json.JSONObject

internal data class LuxeActionCreatorForStatus(
val status: StripeIntent.Status,
val actionCreator: ActionCreator
) {
internal sealed class ActionCreator {
fun create(stripeIntentJsonString: String) =
create(JSONObject(stripeIntentJsonString))

internal abstract fun create(stripeIntentJson: JSONObject): LuxeNextActionRepository.Result
internal data class RedirectActionCreator(
val redirectPagePath: String,
val returnToUrlPath: String
) : ActionCreator() {
override fun create(stripeIntentJson: JSONObject): LuxeNextActionRepository.Result {
val returnUrl = getPath(returnToUrlPath, stripeIntentJson)
val url = getPath(redirectPagePath, stripeIntentJson)
return if ((returnUrl != null) && (url != null)
) {
LuxeNextActionRepository.Result.Action(
StripeIntent.NextActionData.RedirectToUrl(
returnUrl = returnUrl,
url = Uri.parse(url)
)
)
} else {
LuxeNextActionRepository.Result.NotSupported
}
}
}

object NoActionCreator : ActionCreator() {
override fun create(stripeIntentJson: JSONObject) =
LuxeNextActionRepository.Result.NoAction
}
}

internal companion object {
/**
* This function will take a path string like: next_action\[redirect]\[url] and
* find that key path in the json object.
*/
internal fun getPath(path: String?, json: JSONObject): String? {
if (path == null) {
return null
}
val pathArray = ("[*" + "([A-Za-z_0-9]+)" + "]*").toRegex().findAll(path)
.map { it.value }
.distinct()
.filterNot { it.isEmpty() }
.toList()
var jsonObject: JSONObject? = json
var pathIndex = 0
while (pathIndex < pathArray.size &&
jsonObject != null &&
jsonObject.opt(pathArray[pathIndex]) !is String
) {
val key = pathArray[pathIndex]
if (jsonObject.has(key)) {
val tempJsonObject = jsonObject.optJSONObject(key)

if (tempJsonObject != null) {
jsonObject = tempJsonObject
}
}
pathIndex++
}
return jsonObject?.opt(pathArray[pathArray.size - 1]) as? String
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.stripe.android.model

import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import com.stripe.android.StripeIntentResult
import org.json.JSONObject

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class LuxeNextActionRepository {

private val codeToNextActionSpec = mutableMapOf<String, LuxeAction>()

internal fun update(additionalData: Map<String, LuxeAction>) {
codeToNextActionSpec.putAll(additionalData)
}

@VisibleForTesting
internal fun isPresent(code: PaymentMethodCode) = codeToNextActionSpec.contains(code)

/**
* Given the PaymentIntent retrieved after the returnUrl (not redirectUrl), based on
* the Payment Method code and Status of the Intent what is the [StripeIntentResult.Outcome]
* of the operation.
*/
internal fun getPostAuthorizeIntentOutcome(stripeIntent: StripeIntent) =
// This handles the case where the next action is not understood so
// the PI is still in the requires action state.
if (stripeIntent.requiresAction() && stripeIntent.nextActionData == null) {
StripeIntentResult.Outcome.FAILED
} else {
codeToNextActionSpec[stripeIntent.paymentMethod?.code]
?.postAuthorizeIntentStatus?.get(stripeIntent.status)
}

/**
* Given the Intent returned from the confirm call, the payment method code and status
* will be used to lookup the "instructions" for how to pull a next action from the
* payment intent
*
* Return a [Result] that indicates if there is a next action, no next action, or
* if it is not supported by the data in this repository.
*/
internal fun getAction(
lpmCode: PaymentMethodCode?,
status: StripeIntent.Status?,
stripeIntentJson: JSONObject
) = getActionCreator(lpmCode, status)
?.actionCreator?.create(stripeIntentJson)
?: Result.NotSupported

private fun getActionCreator(lpmCode: PaymentMethodCode?, status: StripeIntent.Status?) =
codeToNextActionSpec[lpmCode]?.postConfirmStatusNextStatus.takeIf { it?.status == status }

companion object {
val Instance: LuxeNextActionRepository = LuxeNextActionRepository()
}

internal data class LuxeAction(
/**
* This should be null to use custom next action behavior coded in the SDK
*/
val postConfirmStatusNextStatus: LuxeActionCreatorForStatus?,

// Int here is @StripeIntentResult.Outcome
val postAuthorizeIntentStatus: Map<StripeIntent.Status, Int>
)

internal sealed class Result {
data class Action(val nextActionData: StripeIntent.NextActionData) : Result()
object NoAction : Result()
object NotSupported : Result()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ constructor(
*/
@JvmField val liveMode: Boolean,

/**
* The code of the PaymentMethod. This is useful when the PaymentMethodType is not
* hard coded in the SDK.
*
* [livemode](https://stripe.com/docs/api/payment_methods/object#payment_method_object-type)
*/
@JvmField internal val code: PaymentMethodCode?,

/**
* The type of the PaymentMethod. An additional hash is included on the PaymentMethod with a
* name matching this value. It contains additional information specific to the
Expand Down Expand Up @@ -358,6 +366,7 @@ constructor(
private var created: Long? = null
private var liveMode: Boolean = false
private var type: Type? = null
private var code: PaymentMethodCode? = null
private var billingDetails: BillingDetails? = null
private var metadata: Map<String, String>? = null
private var customerId: String? = null
Expand Down Expand Up @@ -445,12 +454,17 @@ constructor(
this.upi = upi
}

fun setCode(code: String?): Builder = apply {
this.code = code
}

override fun build(): PaymentMethod {
return PaymentMethod(
id = id,
created = created,
liveMode = liveMode,
type = type,
code = code,
billingDetails = billingDetails,
customerId = customerId,
card = card,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ sealed interface StripeIntent : StripeModel {
override fun hashCode(): Int {
return 0
}

override fun equals(other: Any?): Boolean {
return this === other
}
Expand Down

0 comments on commit d5791b2

Please sign in to comment.