Skip to content

Commit

Permalink
PaymentSheet support for Jetpack Compose (#3892)
Browse files Browse the repository at this point in the history
  • Loading branch information
michelleb-stripe committed Jul 2, 2021
1 parent b478c85 commit 223a680
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 52 deletions.
30 changes: 30 additions & 0 deletions payments-core/api/payments-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -5591,6 +5591,36 @@ public final class com/stripe/android/paymentsheet/PaymentSheet$GooglePayConfigu
public static fun values ()[Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration$Environment;
}

public final class com/stripe/android/paymentsheet/PaymentSheetContract : androidx/activity/result/contract/ActivityResultContract {
public static final field EXTRA_ARGS Ljava/lang/String;
public fun <init> ()V
public fun createIntent (Landroid/content/Context;Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;)Landroid/content/Intent;
public synthetic fun createIntent (Landroid/content/Context;Ljava/lang/Object;)Landroid/content/Intent;
public fun parseResult (ILandroid/content/Intent;)Lcom/stripe/android/paymentsheet/PaymentSheetResult;
public synthetic fun parseResult (ILandroid/content/Intent;)Ljava/lang/Object;
}

public final class com/stripe/android/paymentsheet/PaymentSheetContract$Args : com/stripe/android/view/ActivityStarter$Args {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public static final field Companion Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args$Companion;
public final fun copy (Lcom/stripe/android/paymentsheet/model/ClientSecret;Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;Ljava/lang/Integer;)Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;
public static synthetic fun copy$default (Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;Lcom/stripe/android/paymentsheet/model/ClientSecret;Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;
public fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getGooglePayConfig ()Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;
public fun hashCode ()I
public final fun isGooglePayEnabled ()Z
public fun toString ()Ljava/lang/String;
public fun writeToParcel (Landroid/os/Parcel;I)V
}

public final class com/stripe/android/paymentsheet/PaymentSheetContract$Args$Companion {
public final fun createPaymentIntentArgs (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;)Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;
public static synthetic fun createPaymentIntentArgs$default (Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args$Companion;Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;
public final fun createSetupIntentArgs (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;)Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;
public static synthetic fun createSetupIntentArgs$default (Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args$Companion;Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheetContract$Args;
}

public abstract class com/stripe/android/paymentsheet/PaymentSheetResult : android/os/Parcelable {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.stripe.android.paymentsheet

import android.app.Activity
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.fragment.app.Fragment
import com.stripe.android.paymentsheet.model.PaymentIntentClientSecret
import com.stripe.android.paymentsheet.model.SetupIntentClientSecret
import org.jetbrains.annotations.TestOnly

/**
* This is used internally for integrations that don't use Jetpack Compose and are
* able to pass in an activity.
*/
internal class DefaultPaymentSheetLauncher(
private val activityResultLauncher: ActivityResultLauncher<PaymentSheetContract.Args>,
private val statusBarColor: () -> Int?
) : PaymentSheetLauncher {

constructor(
Expand All @@ -22,11 +22,7 @@ internal class DefaultPaymentSheetLauncher(
PaymentSheetContract()
) {
callback.onPaymentSheetResult(it)
},

// lazily access the statusBarColor in case the value changes between when this
// class is instantiated and the payment sheet is launched
{ getStatusBarColor(activity) }
}
)

constructor(
Expand All @@ -37,11 +33,7 @@ internal class DefaultPaymentSheetLauncher(
PaymentSheetContract()
) {
callback.onPaymentSheetResult(it)
},

// lazily access the statusBarColor in case the value changes between when this
// class is instantiated and the payment sheet is launched
{ getStatusBarColor(fragment.activity) }
}
)

@TestOnly
Expand All @@ -55,20 +47,15 @@ internal class DefaultPaymentSheetLauncher(
registry
) {
callback.onPaymentSheetResult(it)
},

// lazily access the statusBarColor in case the value changes between when this
// class is instantiated and the payment sheet is launched
{ getStatusBarColor(fragment.activity) }
}
)

override fun presentWithPaymentIntent(
paymentIntentClientSecret: String,
configuration: PaymentSheet.Configuration?
) = present(
PaymentSheetContract.Args(
PaymentIntentClientSecret(paymentIntentClientSecret),
statusBarColor(),
PaymentSheetContract.Args.createPaymentIntentArgs(
paymentIntentClientSecret,
configuration
)
)
Expand All @@ -77,18 +64,13 @@ internal class DefaultPaymentSheetLauncher(
setupIntentClientSecret: String,
configuration: PaymentSheet.Configuration?
) = present(
PaymentSheetContract.Args(
SetupIntentClientSecret(setupIntentClientSecret),
statusBarColor(),
PaymentSheetContract.Args.createSetupIntentArgs(
setupIntentClientSecret,
configuration
)
)

private fun present(args: PaymentSheetContract.Args) {
activityResultLauncher.launch(args)
}

private companion object {
private fun getStatusBarColor(activity: Activity?) = activity?.window?.statusBarColor
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
package com.stripe.android.paymentsheet

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.ColorInt
import androidx.annotation.VisibleForTesting
import androidx.core.os.bundleOf
import com.stripe.android.paymentsheet.model.ClientSecret
import com.stripe.android.paymentsheet.model.PaymentIntentClientSecret
import com.stripe.android.view.ActivityStarter
import kotlinx.parcelize.Parcelize

internal class PaymentSheetContract :
class PaymentSheetContract :
ActivityResultContract<PaymentSheetContract.Args, PaymentSheetResult>() {
override fun createIntent(
context: Context,
input: Args
): Intent {
val statusBarColor = (context as? Activity)?.window?.statusBarColor
return Intent(context, PaymentSheetActivity::class.java)
.putExtra(EXTRA_ARGS, input)
.putExtra(EXTRA_ARGS, input.copy(statusBarColor = statusBarColor))
}

override fun parseResult(
Expand All @@ -32,19 +35,35 @@ internal class PaymentSheetContract :
}

@Parcelize
internal data class Args(
val clientSecret: ClientSecret,
@ColorInt val statusBarColor: Int?,
val config: PaymentSheet.Configuration?
data class Args @VisibleForTesting internal constructor(
internal val clientSecret: ClientSecret,
internal val config: PaymentSheet.Configuration?,
@ColorInt internal val statusBarColor: Int? = null,
) : ActivityStarter.Args {
val googlePayConfig: PaymentSheet.GooglePayConfiguration? get() = config?.googlePay
val isGooglePayEnabled: Boolean
get() = googlePayConfig != null && clientSecret is PaymentIntentClientSecret

internal companion object {
companion object {
internal fun fromIntent(intent: Intent): Args? {
return intent.getParcelableExtra(EXTRA_ARGS)
}

fun createPaymentIntentArgs(
clientSecret: String,
config: PaymentSheet.Configuration? = null
) = Args(
PaymentIntentClientSecret(clientSecret),
config
)

fun createSetupIntentArgs(
clientSecret: String,
config: PaymentSheet.Configuration? = null
) = Args(
PaymentIntentClientSecret(clientSecret),
config
)
}
}

Expand All @@ -57,8 +76,10 @@ internal class PaymentSheetContract :
}
}

private companion object {
private const val EXTRA_ARGS =
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal companion object {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
const val EXTRA_ARGS =
"com.stripe.android.paymentsheet.PaymentSheetContract.extra_args"
private const val EXTRA_RESULT =
"com.stripe.android.paymentsheet.PaymentSheetContract.extra_result"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.stripe.android.utils.InjectableActivityScenario
import com.stripe.android.utils.TestUtils.idleLooper
import com.stripe.android.utils.TestUtils.viewModelFactoryFor
import com.stripe.android.utils.injectableActivityScenario
import com.stripe.android.view.ActivityScenarioFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -75,8 +76,8 @@ internal class PaymentSheetActivityTest {
context,
PaymentSheetContract.Args(
PaymentIntentClientSecret("client_secret"),
PaymentSheetFixtures.CONFIG_CUSTOMER,
statusBarColor = PaymentSheetFixtures.STATUS_BAR_COLOR,
PaymentSheetFixtures.CONFIG_CUSTOMER
)
)

Expand Down Expand Up @@ -592,13 +593,24 @@ internal class PaymentSheetActivityTest {

@Test
fun `sets expected statusBarColor`() {
val scenario = activityScenario()
scenario.launch(intent).use {
it.onActivity { activity ->
assertThat(activity.window.statusBarColor)
.isEqualTo(PaymentSheetFixtures.STATUS_BAR_COLOR)
scenario.moveToState(Lifecycle.State.DESTROYED)
}
val activityScenarioFactory = ActivityScenarioFactory(context)
val activityScenario = activityScenarioFactory.createAddPaymentMethodActivity()
activityScenario.moveToState(Lifecycle.State.CREATED)
activityScenario.onActivity { activity ->
activity.window.statusBarColor = PaymentSheetFixtures.STATUS_BAR_COLOR

val intent = contract.createIntent(
activity,
PaymentSheetContract.Args(
PaymentIntentClientSecret("client_secret"),
PaymentSheetFixtures.CONFIG_CUSTOMER
)
)

val args =
intent.extras?.get(PaymentSheetContract.EXTRA_ARGS) as PaymentSheetContract.Args
assertThat(args.statusBarColor)
.isEqualTo(PaymentSheetFixtures.STATUS_BAR_COLOR)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,25 @@ internal object PaymentSheetFixtures {

internal val ARGS_CUSTOMER_WITH_GOOGLEPAY_SETUP = PaymentSheetContract.Args(
SetupIntentClientSecret(CLIENT_SECRET),
CONFIG_CUSTOMER_WITH_GOOGLEPAY,
STATUS_BAR_COLOR,
CONFIG_CUSTOMER_WITH_GOOGLEPAY
)

internal val ARGS_CUSTOMER_WITH_GOOGLEPAY = PaymentSheetContract.Args(
PAYMENT_INTENT_CLIENT_SECRET,
CONFIG_CUSTOMER_WITH_GOOGLEPAY,
STATUS_BAR_COLOR,
CONFIG_CUSTOMER_WITH_GOOGLEPAY
)

internal val ARGS_CUSTOMER_WITHOUT_GOOGLEPAY = PaymentSheetContract.Args(
PAYMENT_INTENT_CLIENT_SECRET,
CONFIG_CUSTOMER,
STATUS_BAR_COLOR,
CONFIG_CUSTOMER
)

internal val ARGS_WITHOUT_CUSTOMER = PaymentSheetContract.Args(
PAYMENT_INTENT_CLIENT_SECRET,
config = null,
STATUS_BAR_COLOR,
config = null
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import com.stripe.android.model.PaymentIntentFixtures
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParamsFixtures
import com.stripe.android.model.PaymentMethodFixtures
import com.stripe.android.model.SetupIntentFixtures
import com.stripe.android.model.StripeIntent
import com.stripe.android.networking.AbsFakeStripeRepository
import com.stripe.android.networking.ApiRequest
Expand Down Expand Up @@ -663,7 +662,6 @@ internal class PaymentSheetViewModelTest {

private val PAYMENT_METHODS = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD)

val SETUP_INTENT = SetupIntentFixtures.SI_REQUIRES_PAYMENT_METHOD
val PAYMENT_INTENT = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD
val PAYMENT_INTENT_RESULT = PaymentIntentResult(
intent = PAYMENT_INTENT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.stripe.android.paymentsheet.example.activity

import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.LocalContext
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PaymentSheetContract

internal class LaunchPaymentSheetWithComposeActivity :
BasePaymentSheetActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MaterialTheme {
val stripeLauncher =
rememberLauncherForActivityResult(contract = PaymentSheetContract())
{
onPaymentSheetResult(it)
}

val inProgress by viewModel.inProgress.observeAsState(false)
val status by viewModel.status.observeAsState("")

if (status.isNotBlank()) {
Toast.makeText(LocalContext.current, status, Toast.LENGTH_SHORT).show()
viewModel.statusDisplayed()
}

Receipt(inProgress) {
BuyButton(
buyButtonEnabled = !inProgress,
onClick = {
prepareCheckout { customerConfig, clientSecret ->
stripeLauncher.launch(
PaymentSheetContract.Args.createPaymentIntentArgs(
clientSecret,
PaymentSheet.Configuration(
merchantDisplayName = merchantName,
customer = customerConfig,
googlePay = googlePayConfig,
)
)
)
}
}
)
}
}
}
}
}

0 comments on commit 223a680

Please sign in to comment.