From 8b77ca2487a01f786cf845e5f644b98460036708 Mon Sep 17 00:00:00 2001 From: Chen Cen <79880926+ccen-stripe@users.noreply.github.com> Date: Thu, 26 Aug 2021 11:01:38 -0700 Subject: [PATCH] [PaymentLauncher] Replace `Stripe` with `PaymentLauncher` in example app (#4125) * change * change --- example/AndroidManifest.xml | 1 + example/build.gradle | 15 ++ example/res/values/strings.xml | 1 + .../activity/ComposeExampleActivity.kt | 181 ++++++++++++++++++ .../activity/ConfirmSepaDebitActivity.kt | 19 +- .../example/activity/LauncherActivity.kt | 4 + .../example/activity/StripeIntentActivity.kt | 114 +++++------ .../example/activity/UpiPaymentActivity.kt | 18 +- .../example/module/StripeIntentViewModel.kt | 6 +- 9 files changed, 270 insertions(+), 89 deletions(-) create mode 100644 example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt diff --git a/example/AndroidManifest.xml b/example/AndroidManifest.xml index ca1b3c486a0..031d38f928a 100644 --- a/example/AndroidManifest.xml +++ b/example/AndroidManifest.xml @@ -65,6 +65,7 @@ + diff --git a/example/build.gradle b/example/build.gradle index 44a1f70862e..7b04b931f95 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -57,6 +57,17 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion" + + // Jetpack Compose + // Integration with activities + implementation 'androidx.activity:activity-compose:1.3.1' + // Compose Material Design + implementation "androidx.compose.material:material:$composeVersion" + // Integration with observables + implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" + // end of Jetpack Compose + + ktlint "com.pinterest:ktlint:$ktlintVersion" testImplementation "androidx.test:core:$androidTestVersion" @@ -158,6 +169,10 @@ android { buildFeatures { viewBinding true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion "$composeVersion" } } diff --git a/example/res/values/strings.xml b/example/res/values/strings.xml index b7acf93b3fa..f369174df00 100644 --- a/example/res/values/strings.xml +++ b/example/res/values/strings.xml @@ -91,6 +91,7 @@ Confirm with NetBanking Connect Example Simple PaymentMethod Confirmation Example + Compose Example Virtual Payment Address (VPA) diff --git a/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt b/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt new file mode 100644 index 00000000000..bb040b63e99 --- /dev/null +++ b/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt @@ -0,0 +1,181 @@ +package com.stripe.example.activity + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.stripe.android.model.Address +import com.stripe.android.model.ConfirmPaymentIntentParams +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.payments.paymentlauncher.PaymentLauncher +import com.stripe.android.payments.paymentlauncher.PaymentResult +import com.stripe.example.R +import com.stripe.example.Settings +import com.stripe.example.module.StripeIntentViewModel + +/** + * An Activity to demonstrate [PaymentLauncher] with Jetpack Compose. + */ +class ComposeExampleActivity : AppCompatActivity() { + private val viewModel: StripeIntentViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ComposeScreen() + } + } + + @Composable + fun ComposeScreen() { + val inProgress by viewModel.inProgress.observeAsState(false) + val status by viewModel.status.observeAsState("") + + createPaymentLauncher().let { paymentLauncher -> + Column(modifier = Modifier.padding(horizontal = 10.dp)) { + if (inProgress) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + ) + } + Text( + stringResource(R.string.payment_auth_intro), + modifier = Modifier.padding(vertical = 5.dp), + ) + ConfirmButton( + params = confirmParams3ds1, + buttonName = R.string.confirm_with_3ds1_button, + paymentLauncher = paymentLauncher, + inProgress = inProgress + ) + ConfirmButton( + params = confirmParams3ds2, + buttonName = R.string.confirm_with_3ds2_button, + paymentLauncher = paymentLauncher, + inProgress = inProgress + ) + Divider(modifier = Modifier.padding(vertical = 5.dp)) + Text(text = status) + } + } + } + + /** + * Create [PaymentLauncher] in a [Composable] + */ + @Composable + fun createPaymentLauncher(): PaymentLauncher { + val settings = Settings(LocalContext.current) + return PaymentLauncher.createForCompose( + publishableKey = settings.publishableKey, + stripeAccountId = settings.stripeAccountId + ) { + when (it) { + is PaymentResult.Completed -> { + viewModel.status.value += "\n\nPaymentIntent confirmation succeeded\n\n" + viewModel.inProgress.value = false + } + is PaymentResult.Canceled -> { + viewModel.status.value += "\n\nPaymentIntent confirmation cancelled\n\n" + viewModel.inProgress.value = false + } + is PaymentResult.Failed -> { + viewModel.status.value += "\n\nPaymentIntent confirmation failed with " + + "throwable ${it.throwable} \n\n" + viewModel.inProgress.value = false + } + } + } + } + + @Composable + fun ConfirmButton( + params: PaymentMethodCreateParams, + @StringRes buttonName: Int, + paymentLauncher: PaymentLauncher, + inProgress: Boolean + ) { + Button( + onClick = { createAndConfirmPaymentIntent(params, paymentLauncher) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + enabled = !inProgress + ) { + Text(stringResource(buttonName)) + } + } + + private fun createAndConfirmPaymentIntent( + params: PaymentMethodCreateParams, + paymentLauncher: PaymentLauncher, + ) { + viewModel.createPaymentIntent("us").observe( + this + ) { + it.onSuccess { responseData -> + val confirmPaymentIntentParams = + ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams( + paymentMethodCreateParams = params, + clientSecret = responseData.getString("secret"), + shipping = SHIPPING + ) + paymentLauncher.confirm(confirmPaymentIntentParams) + } + } + } + + private companion object { + /** + * See https://stripe.com/docs/payments/3d-secure#three-ds-cards for more options. + */ + private val confirmParams3ds2 = + PaymentMethodCreateParams.create( + PaymentMethodCreateParams.Card.Builder() + .setNumber("4000000000003238") + .setExpiryMonth(1) + .setExpiryYear(2025) + .setCvc("123") + .build() + ) + + private val confirmParams3ds1 = + PaymentMethodCreateParams.create( + PaymentMethodCreateParams.Card.Builder() + .setNumber("4000000000003063") + .setExpiryMonth(1) + .setExpiryYear(2025) + .setCvc("123") + .build() + ) + + private val SHIPPING = ConfirmPaymentIntentParams.Shipping( + address = Address.Builder() + .setCity("San Francisco") + .setCountry("US") + .setLine1("123 Market St") + .setLine2("#345") + .setPostalCode("94107") + .setState("CA") + .build(), + name = "Jenny Rosen", + carrier = "Fedex", + trackingNumber = "12345" + ) + } +} diff --git a/example/src/main/java/com/stripe/example/activity/ConfirmSepaDebitActivity.kt b/example/src/main/java/com/stripe/example/activity/ConfirmSepaDebitActivity.kt index 77cdfb8aafe..cb4adc4110e 100644 --- a/example/src/main/java/com/stripe/example/activity/ConfirmSepaDebitActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/ConfirmSepaDebitActivity.kt @@ -3,11 +3,11 @@ package com.stripe.example.activity import android.os.Bundle import android.view.View import androidx.lifecycle.Observer -import com.stripe.android.PaymentIntentResult import com.stripe.android.model.Address import com.stripe.android.model.MandateDataParams import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.example.R import com.stripe.example.databinding.CreateSepaDebitActivityBinding @@ -54,14 +54,19 @@ class ConfirmSepaDebitActivity : StripeIntentActivity() { viewBinding.confirmButton.isEnabled = enabled } - override fun onConfirmSuccess(result: PaymentIntentResult) { - super.onConfirmSuccess(result) - snackbarController.show("Status after confirmation: ${result.intent.status}") + override fun onConfirmSuccess() { + super.onConfirmSuccess() + snackbarController.show("Confirmation succeeded.") } - override fun onConfirmError(throwable: Throwable) { - super.onConfirmError(throwable) - snackbarController.show("Error during confirmation: ${throwable.message}") + override fun onConfirmCanceled() { + super.onConfirmCanceled() + snackbarController.show("Confirmation canceled.") + } + + override fun onConfirmError(failedResult: PaymentResult.Failed) { + super.onConfirmError(failedResult) + snackbarController.show("Error during confirmation: ${failedResult.throwable.message}") } private fun createPaymentMethodParams(iban: String): PaymentMethodCreateParams { diff --git a/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt b/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt index 5a7ff8905b8..18003b41ebb 100644 --- a/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt @@ -119,6 +119,10 @@ class LauncherActivity : AppCompatActivity() { Item( activity.getString(R.string.connect_example), ConnectExampleActivity::class.java + ), + Item( + activity.getString(R.string.compose_example), + ComposeExampleActivity::class.java ) ) diff --git a/example/src/main/java/com/stripe/example/activity/StripeIntentActivity.kt b/example/src/main/java/com/stripe/example/activity/StripeIntentActivity.kt index abc1e301afa..be2d19ec38d 100644 --- a/example/src/main/java/com/stripe/example/activity/StripeIntentActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/StripeIntentActivity.kt @@ -1,24 +1,17 @@ package com.stripe.example.activity -import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import com.stripe.android.PaymentIntentResult -import com.stripe.android.SetupIntentResult -import com.stripe.android.Stripe -import com.stripe.android.getPaymentIntentResult -import com.stripe.android.getSetupIntentResult +import com.stripe.android.PaymentConfiguration import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams import com.stripe.android.model.MandateDataParams import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.example.R +import com.stripe.android.payments.paymentlauncher.PaymentLauncher +import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.example.Settings -import com.stripe.example.StripeFactory import com.stripe.example.module.StripeIntentViewModel -import kotlinx.coroutines.launch import org.json.JSONObject /** @@ -31,35 +24,40 @@ abstract class StripeIntentActivity : AppCompatActivity() { private val stripeAccountId: String? by lazy { Settings(this).stripeAccountId } - protected val stripe: Stripe by lazy { - StripeFactory(this, stripeAccountId).create() - } + + private lateinit var paymentLauncher: PaymentLauncher + private val keyboardController: KeyboardController by lazy { KeyboardController(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - viewModel.paymentIntentResultLiveData - .observe( + paymentLauncher = + PaymentLauncher.create( this, - { - it.fold( - onSuccess = ::onConfirmSuccess, - onFailure = ::onConfirmError - ) - } - ) + PaymentConfiguration.getInstance(this).publishableKey, + stripeAccountId + ) { paymentResult -> + viewModel.status.value += "\n\nPayment authentication completed, getting result" + viewModel.paymentResultLiveData.postValue(paymentResult) + } - viewModel.setupIntentResultLiveData + viewModel.paymentResultLiveData .observe( this, { - it.fold( - onSuccess = ::onConfirmSuccess, - onFailure = ::onConfirmError - ) + when (it) { + is PaymentResult.Completed -> { + onConfirmSuccess() + } + is PaymentResult.Canceled -> { + onConfirmCanceled() + } + is PaymentResult.Failed -> { + onConfirmError(it) + } + } } ) } @@ -70,7 +68,8 @@ abstract class StripeIntentActivity : AppCompatActivity() { shippingDetails: ConfirmPaymentIntentParams.Shipping? = null, stripeAccountId: String? = null, existingPaymentMethodId: String? = null, - mandateDataParams: MandateDataParams? = null + mandateDataParams: MandateDataParams? = null, + onPaymentIntentCreated: (String) -> Unit = {} ) { requireNotNull(paymentMethodCreateParams ?: existingPaymentMethodId) @@ -86,7 +85,8 @@ abstract class StripeIntentActivity : AppCompatActivity() { shippingDetails, stripeAccountId, existingPaymentMethodId, - mandateDataParams + mandateDataParams, + onPaymentIntentCreated ) } } @@ -116,9 +116,11 @@ abstract class StripeIntentActivity : AppCompatActivity() { shippingDetails: ConfirmPaymentIntentParams.Shipping?, stripeAccountId: String?, existingPaymentMethodId: String?, - mandateDataParams: MandateDataParams? + mandateDataParams: MandateDataParams?, + onPaymentIntentCreated: (String) -> Unit = {} ) { val secret = responseData.getString("secret") + onPaymentIntentCreated(secret) viewModel.status.postValue( viewModel.status.value + "\n\nStarting PaymentIntent confirmation" + ( @@ -140,7 +142,7 @@ abstract class StripeIntentActivity : AppCompatActivity() { mandateData = mandateDataParams ) } - stripe.confirmPayment(this, confirmPaymentIntentParams, stripeAccountId) + paymentLauncher.confirm(confirmPaymentIntentParams) } private fun handleCreateSetupIntentResponse( @@ -157,57 +159,27 @@ abstract class StripeIntentActivity : AppCompatActivity() { } ?: "" ) ) - stripe.confirmSetupIntent( - this, + paymentLauncher.confirm( ConfirmSetupIntentParams.create( paymentMethodCreateParams = params, clientSecret = secret - ), - stripeAccountId + ) ) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - keyboardController.hide() - - viewModel.status.value += "\n\nPayment authentication completed, getting result" - if (stripe.isPaymentResult(requestCode, data)) { - lifecycleScope.launch { - viewModel.paymentIntentResultLiveData.value = runCatching { - // stripe.isPaymentResult already verifies data is not null - stripe.getPaymentIntentResult(requestCode, data!!) - } - } - } else if (stripe.isSetupResult(requestCode, data)) { - lifecycleScope.launch { - viewModel.setupIntentResultLiveData.value = runCatching { - // stripe.isSetupResult already verifies data is not null - stripe.getSetupIntentResult(requestCode, data!!) - } - } - } - } - - protected open fun onConfirmSuccess(result: PaymentIntentResult) { - val paymentIntent = result.intent - viewModel.status.value += "\n\n" + - "PaymentIntent confirmation outcome: ${result.outcome}\n\n" + - getString(R.string.payment_intent_status, paymentIntent.status) + protected open fun onConfirmSuccess() { + viewModel.status.value += "\n\nPaymentIntent confirmation succeeded\n\n" viewModel.inProgress.value = false } - protected open fun onConfirmSuccess(result: SetupIntentResult) { - val setupIntentResult = result.intent - viewModel.status.value += "\n\n" + - "SetupIntent confirmation outcome: ${result.outcome}\n\n" + - getString(R.string.setup_intent_status, setupIntentResult.status) + protected open fun onConfirmCanceled() { + viewModel.status.value += "\n\nPaymentIntent confirmation cancelled\n\n" viewModel.inProgress.value = false } - protected open fun onConfirmError(throwable: Throwable) { - viewModel.status.value += "\n\nException: " + throwable.message + protected open fun onConfirmError(failedResult: PaymentResult.Failed) { + viewModel.status.value += "\n\nPaymentIntent confirmation failed with throwable " + + "${failedResult.throwable} \n\n" viewModel.inProgress.value = false } } diff --git a/example/src/main/java/com/stripe/example/activity/UpiPaymentActivity.kt b/example/src/main/java/com/stripe/example/activity/UpiPaymentActivity.kt index 02e5f0907cd..a785f7fef46 100644 --- a/example/src/main/java/com/stripe/example/activity/UpiPaymentActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/UpiPaymentActivity.kt @@ -3,10 +3,10 @@ package com.stripe.example.activity import android.content.Intent import android.os.Bundle import androidx.lifecycle.Observer -import com.stripe.android.PaymentIntentResult import com.stripe.android.model.Address import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.example.databinding.UpiPaymentActivityBinding class UpiPaymentActivity : StripeIntentActivity() { @@ -14,6 +14,8 @@ class UpiPaymentActivity : StripeIntentActivity() { UpiPaymentActivityBinding.inflate(layoutInflater) } + private lateinit var paymentIntentSecret: String + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(viewBinding.root) @@ -39,20 +41,22 @@ class UpiPaymentActivity : StripeIntentActivity() { ) ) - createAndConfirmPaymentIntent("in", params) + createAndConfirmPaymentIntent("in", params) { secret -> + paymentIntentSecret = secret + } } } - override fun onConfirmSuccess(result: PaymentIntentResult) { - val paymentIntent = result.intent + override fun onConfirmSuccess() { startActivity( Intent(this@UpiPaymentActivity, UpiWaitingActivity::class.java) - .putExtra(EXTRA_CLIENT_SECRET, paymentIntent.clientSecret) + .putExtra(EXTRA_CLIENT_SECRET, paymentIntentSecret) ) } - override fun onConfirmError(throwable: Throwable) { - viewModel.status.value += "\n\nException: " + throwable.message + override fun onConfirmError(failedResult: PaymentResult.Failed) { + viewModel.status.value += "\n\nPaymentIntent confirmation failed with throwable " + + "${failedResult.throwable} \n\n" } internal companion object { diff --git a/example/src/main/java/com/stripe/example/module/StripeIntentViewModel.kt b/example/src/main/java/com/stripe/example/module/StripeIntentViewModel.kt index 3cbf233e8ac..3fcbe0c436d 100644 --- a/example/src/main/java/com/stripe/example/module/StripeIntentViewModel.kt +++ b/example/src/main/java/com/stripe/example/module/StripeIntentViewModel.kt @@ -4,8 +4,7 @@ import android.app.Application import androidx.annotation.StringRes import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData -import com.stripe.android.PaymentIntentResult -import com.stripe.android.SetupIntentResult +import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.example.R import com.stripe.example.activity.BaseViewModel import kotlinx.coroutines.withContext @@ -19,8 +18,7 @@ internal class StripeIntentViewModel( val inProgress = MutableLiveData() val status = MutableLiveData() - val paymentIntentResultLiveData = MutableLiveData>() - val setupIntentResultLiveData = MutableLiveData>() + val paymentResultLiveData = MutableLiveData() fun createPaymentIntent( country: String,