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

PaymentSheet support for Jetpack Compose #3892

Merged
merged 11 commits into from
Jun 30, 2021
30 changes: 30 additions & 0 deletions payments-core/api/payments-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -4588,6 +4588,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 @@ -78,8 +79,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 @@ -595,13 +596,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 @@ -19,7 +19,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 @@ -689,7 +688,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,
)
)
)
}
}
)
}
}
}
}
}