diff --git a/CHANGELOG.md b/CHANGELOG.md index 423c135a821..8bbe1448766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,13 @@ ## X.X.X ### PaymentSheet -[FIXED][5321](https://github.com/stripe/stripe-android/pull/5321) Fixed issue with forever loading and mochi library. +* [FIXED][5321](https://github.com/stripe/stripe-android/pull/5321) Fixed issue with forever loading and mochi library. ### Payments -[FIXED][5308](https://github.com/stripe/stripe-android/pull/5308) OXXO so that processing is considered a successful terminal state, similar to Konbini and Boleto. -[FIXED][5138](https://github.com/stripe/stripe-android/pull/5138) Fixed an issue where PaymentSheet will show a failure even when 3DS2 Payment/SetupIntent is successful +* [FIXED][5308](https://github.com/stripe/stripe-android/pull/5308) OXXO so that processing is considered a successful terminal state, similar to Konbini and Boleto. +* [FIXED][5138](https://github.com/stripe/stripe-android/pull/5138) Fixed an issue where PaymentSheet will show a failure even when 3DS2 Payment/SetupIntent is successful. +* [ADDED][5274](https://github.com/stripe/stripe-android/pull/5274) Create `rememberLauncher` method enabling usage of `GooglePayLauncher`, `GooglePayPaymentMethodLauncher` and `PaymentLauncher` in Compose. +* [DEPRECATED][5274](https://github.com/stripe/stripe-android/pull/5274) Deprecate `PaymentLauncher.createForCompose` in favor of `PaymentLauncher.rememberLauncher`. ## 20.7.0 - 2022-07-06 * This release adds additional support for Afterpay/Clearpay in PaymentSheet. diff --git a/example/AndroidManifest.xml b/example/AndroidManifest.xml index 838d3b9b8ca..4fff83498f6 100644 --- a/example/AndroidManifest.xml +++ b/example/AndroidManifest.xml @@ -50,7 +50,9 @@ + + diff --git a/example/res/values/strings.xml b/example/res/values/strings.xml index ce6667e5445..b1bee1245e6 100644 --- a/example/res/values/strings.xml +++ b/example/res/values/strings.xml @@ -13,8 +13,10 @@ Customer Payment Selection Customer Session Confirm payment with GooglePayLauncher + Confirm payment in Compose with GooglePayLauncher Test GooglePayLauncher Configurations Create payment method with GooglePayPaymentMethodLauncher + Create payment method in Compose with GooglePayPaymentMethodLauncher Payment Session Fragment Examples Create Payment Method diff --git a/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt b/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt index e5b421d89e5..0fe7aae7569 100644 --- a/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/ComposeExampleActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -45,33 +46,45 @@ class ComposeExampleActivity : AppCompatActivity() { fun ComposeScreen() { val inProgress by viewModel.inProgress.observeAsState(false) val status by viewModel.status.observeAsState("") + val paymentLauncher = rememberPaymentLauncher() - 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 + ComposeScreen( + inProgress = inProgress, + status = status, + onConfirm = { paymentLauncher.confirm(it) } + ) + } + + @Composable + private fun ComposeScreen( + inProgress: Boolean, + status: String, + onConfirm: (ConfirmPaymentIntentParams) -> Unit + ) { + Column(modifier = Modifier.padding(horizontal = 10.dp)) { + if (inProgress) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth() ) - Divider(modifier = Modifier.padding(vertical = 5.dp)) - Text(text = status) } + Text( + stringResource(R.string.payment_auth_intro), + modifier = Modifier.padding(vertical = 5.dp) + ) + ConfirmButton( + params = confirmParams3ds1, + buttonName = R.string.confirm_with_3ds1_button, + onConfirm = onConfirm, + inProgress = inProgress + ) + ConfirmButton( + params = confirmParams3ds2, + buttonName = R.string.confirm_with_3ds2_button, + onConfirm = onConfirm, + inProgress = inProgress + ) + Divider(modifier = Modifier.padding(vertical = 5.dp)) + Text(text = status) } } @@ -79,39 +92,43 @@ class ComposeExampleActivity : AppCompatActivity() { * Create [PaymentLauncher] in a [Composable] */ @Composable - fun createPaymentLauncher(): PaymentLauncher { - val settings = Settings(LocalContext.current) - return PaymentLauncher.createForCompose( + private fun rememberPaymentLauncher(): PaymentLauncher { + val context = LocalContext.current + val settings = remember { Settings(context) } + return PaymentLauncher.rememberLauncher( 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 - } + stripeAccountId = settings.stripeAccountId, + callback = ::onPaymentResult + ) + } + + private fun onPaymentResult(paymentResult: PaymentResult) { + when (paymentResult) { + 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 ${paymentResult.throwable} \n\n" + viewModel.inProgress.value = false } } } @Composable - fun ConfirmButton( + private fun ConfirmButton( params: PaymentMethodCreateParams, @StringRes buttonName: Int, - paymentLauncher: PaymentLauncher, - inProgress: Boolean + inProgress: Boolean, + onConfirm: (ConfirmPaymentIntentParams) -> Unit ) { Button( - onClick = { createAndConfirmPaymentIntent(params, paymentLauncher) }, + onClick = { createAndConfirmPaymentIntent(params, onConfirm) }, modifier = Modifier .fillMaxWidth() .padding(vertical = 5.dp), @@ -123,7 +140,7 @@ class ComposeExampleActivity : AppCompatActivity() { private fun createAndConfirmPaymentIntent( params: PaymentMethodCreateParams, - paymentLauncher: PaymentLauncher + onConfirm: (ConfirmPaymentIntentParams) -> Unit ) { viewModel.createPaymentIntent("us").observe( this @@ -135,7 +152,7 @@ class ComposeExampleActivity : AppCompatActivity() { clientSecret = responseData.getString("secret"), shipping = SHIPPING ) - paymentLauncher.confirm(confirmPaymentIntentParams) + onConfirm(confirmPaymentIntentParams) } } } diff --git a/example/src/main/java/com/stripe/example/activity/GooglePayLauncherComposeActivity.kt b/example/src/main/java/com/stripe/example/activity/GooglePayLauncherComposeActivity.kt new file mode 100644 index 00000000000..0dfe754a968 --- /dev/null +++ b/example/src/main/java/com/stripe/example/activity/GooglePayLauncherComposeActivity.kt @@ -0,0 +1,166 @@ +package com.stripe.example.activity + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.stripe.android.googlepaylauncher.GooglePayEnvironment +import com.stripe.android.googlepaylauncher.GooglePayLauncher +import kotlinx.coroutines.launch + +class GooglePayLauncherComposeActivity : StripeIntentActivity() { + private val googlePayConfig = GooglePayLauncher.Config( + environment = GooglePayEnvironment.Test, + merchantCountryCode = COUNTRY_CODE, + merchantName = "Widget Store", + billingAddressConfig = GooglePayLauncher.BillingAddressConfig( + isRequired = true, + format = GooglePayLauncher.BillingAddressConfig.Format.Full, + isPhoneNumberRequired = false + ), + existingPaymentMethodRequired = false + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + GooglePayLauncherScreen() + } + } + + @Composable + private fun GooglePayLauncherScreen() { + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + + var clientSecret by rememberSaveable { mutableStateOf("") } + var googlePayReady by rememberSaveable { mutableStateOf(null) } + var completed by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if (clientSecret.isBlank()) { + viewModel.createPaymentIntent(COUNTRY_CODE).observe( + this@GooglePayLauncherComposeActivity + ) { result -> + result.fold( + onSuccess = { json -> + clientSecret = json.getString("secret") + }, + onFailure = { error -> + scope.launch { + scaffoldState.snackbarHostState.showSnackbar( + "Could not create PaymentIntent. ${error.message}" + ) + } + completed = true + } + ) + } + } + } + + val googlePayLauncher = GooglePayLauncher.rememberLauncher( + config = googlePayConfig, + readyCallback = { ready -> + if (googlePayReady == null) { + googlePayReady = ready + + if (!ready) { + completed = true + } + + scope.launch { + scaffoldState.snackbarHostState.showSnackbar("Google Pay ready? $ready") + } + } + }, + resultCallback = { result -> + when (result) { + GooglePayLauncher.Result.Completed -> { + "Successfully collected payment." + } + GooglePayLauncher.Result.Canceled -> { + "Customer cancelled Google Pay." + } + is GooglePayLauncher.Result.Failed -> { + "Google Pay failed. ${result.error.message}" + } + }.let { + scope.launch { + scaffoldState.snackbarHostState.showSnackbar(it) + completed = true + } + } + } + ) + + GooglePayLauncherScreen( + scaffoldState = scaffoldState, + clientSecret = clientSecret, + googlePayReady = googlePayReady, + completed = completed, + onLaunchGooglePay = { googlePayLauncher.presentForPaymentIntent(it) } + ) + } + + @Composable + private fun GooglePayLauncherScreen( + scaffoldState: ScaffoldState, + clientSecret: String, + googlePayReady: Boolean?, + completed: Boolean, + onLaunchGooglePay: (String) -> Unit + ) { + Scaffold(scaffoldState = scaffoldState) { + Column(Modifier.fillMaxWidth()) { + if (googlePayReady == null || clientSecret.isBlank()) { + LinearProgressIndicator(Modifier.fillMaxWidth()) + } + + Spacer( + Modifier + .height(8.dp) + .fillMaxWidth() + ) + + AndroidView( + factory = { context -> + GooglePayButton(context) + }, + modifier = Modifier + .wrapContentWidth() + .clickable( + enabled = googlePayReady == true && + clientSecret.isNotBlank() && !completed, + onClick = { + onLaunchGooglePay(clientSecret) + } + ) + ) + } + } + } + + private companion object { + private const val COUNTRY_CODE = "US" + } +} diff --git a/example/src/main/java/com/stripe/example/activity/GooglePayPaymentMethodLauncherComposeActivity.kt b/example/src/main/java/com/stripe/example/activity/GooglePayPaymentMethodLauncherComposeActivity.kt new file mode 100644 index 00000000000..1a58924c238 --- /dev/null +++ b/example/src/main/java/com/stripe/example/activity/GooglePayPaymentMethodLauncherComposeActivity.kt @@ -0,0 +1,112 @@ +package com.stripe.example.activity + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.stripe.android.googlepaylauncher.GooglePayEnvironment +import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher +import kotlinx.coroutines.launch + +class GooglePayPaymentMethodLauncherComposeActivity : AppCompatActivity() { + private val googlePayConfig = GooglePayPaymentMethodLauncher.Config( + environment = GooglePayEnvironment.Test, + merchantCountryCode = "US", + merchantName = "Widget Store", + billingAddressConfig = GooglePayPaymentMethodLauncher.BillingAddressConfig( + isRequired = true, + format = GooglePayPaymentMethodLauncher.BillingAddressConfig.Format.Full, + isPhoneNumberRequired = false + ), + existingPaymentMethodRequired = false + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + GooglePayPaymentMethodLauncherScreen() + } + } + + @Composable + private fun GooglePayPaymentMethodLauncherScreen() { + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + var enabled by remember { mutableStateOf(false) } + + val googlePayLauncher = GooglePayPaymentMethodLauncher.rememberLauncher( + config = googlePayConfig, + readyCallback = { ready -> + if (ready) { + enabled = true + } + scope.launch { + scaffoldState.snackbarHostState.showSnackbar("Google Pay ready? $ready") + } + }, + resultCallback = { result -> + when (result) { + is GooglePayPaymentMethodLauncher.Result.Completed -> { + "Successfully created a PaymentMethod. ${result.paymentMethod}" + } + GooglePayPaymentMethodLauncher.Result.Canceled -> { + "Customer cancelled Google Pay." + } + is GooglePayPaymentMethodLauncher.Result.Failed -> { + "Google Pay failed: ${result.errorCode}: ${result.error.message}" + } + }.let { + scope.launch { + scaffoldState.snackbarHostState.showSnackbar(it) + enabled = false + } + } + } + ) + + GooglePayPaymentMethodLauncherScreen( + scaffoldState = scaffoldState, + enabled = enabled, + onLaunchGooglePay = { + googlePayLauncher.present( + currencyCode = "EUR", + amount = 2500 + ) + } + ) + } + + @Composable + private fun GooglePayPaymentMethodLauncherScreen( + scaffoldState: ScaffoldState, + enabled: Boolean, + onLaunchGooglePay: () -> Unit + ) { + Scaffold(scaffoldState = scaffoldState) { + AndroidView( + factory = { context -> + GooglePayButton(context) + }, + modifier = Modifier + .wrapContentWidth() + .clickable( + enabled = enabled, + onClick = onLaunchGooglePay + ) + ) + } + } +} 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 058ad11c505..6e7e1db3c52 100644 --- a/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt @@ -68,6 +68,10 @@ class LauncherActivity : AppCompatActivity() { activity.getString(R.string.googlepaylauncher_example), GooglePayLauncherIntegrationActivity::class.java ), + Item( + activity.getString(R.string.googlepaycomposelauncher_example), + GooglePayLauncherComposeActivity::class.java + ), // This is for internal use so as not to confuse the user. // Item( // activity.getString(R.string.googlepayplayground_example), @@ -77,6 +81,10 @@ class LauncherActivity : AppCompatActivity() { activity.getString(R.string.googlepaypaymentmethodlauncher_example), GooglePayPaymentMethodLauncherIntegrationActivity::class.java ), + Item( + activity.getString(R.string.googlepaypaymentmethodcomposelauncher_example), + GooglePayPaymentMethodLauncherComposeActivity::class.java + ), Item( activity.getString(R.string.launch_confirm_pm_sepa_debit), ConfirmSepaDebitActivity::class.java diff --git a/payments-core/api/payments-core.api b/payments-core/api/payments-core.api index 4c541a42d8c..49bad9fa80a 100644 --- a/payments-core/api/payments-core.api +++ b/payments-core/api/payments-core.api @@ -1106,6 +1106,7 @@ public final class com/stripe/android/googlepaylauncher/GooglePayEnvironment : j public final class com/stripe/android/googlepaylauncher/GooglePayLauncher { public static final field $stable I + public static final field Companion Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$Companion; public fun (Landroidx/activity/ComponentActivity;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$ReadyCallback;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$ResultCallback;)V public fun (Landroidx/fragment/app/Fragment;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$ReadyCallback;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$ResultCallback;)V public final fun presentForPaymentIntent (Ljava/lang/String;)V @@ -1144,6 +1145,10 @@ public final class com/stripe/android/googlepaylauncher/GooglePayLauncher$Billin public static fun values ()[Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$BillingAddressConfig$Format; } +public final class com/stripe/android/googlepaylauncher/GooglePayLauncher$Companion { + public final fun rememberLauncher (Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$ReadyCallback;Lcom/stripe/android/googlepaylauncher/GooglePayLauncher$ResultCallback;Landroidx/compose/runtime/Composer;I)Lcom/stripe/android/googlepaylauncher/GooglePayLauncher; +} + public final class com/stripe/android/googlepaylauncher/GooglePayLauncher$Config : android/os/Parcelable { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; @@ -1348,6 +1353,7 @@ public final class com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLa public static final field DEVELOPER_ERROR I public static final field INTERNAL_ERROR I public static final field NETWORK_ERROR I + public synthetic fun (Landroid/content/Context;Lkotlinx/coroutines/CoroutineScope;Landroidx/activity/result/ActivityResultLauncher;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ReadyCallback;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Landroidx/activity/ComponentActivity;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ReadyCallback;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ResultCallback;)V public fun (Landroidx/fragment/app/Fragment;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ReadyCallback;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ResultCallback;)V public final fun present (Ljava/lang/String;)V @@ -1395,6 +1401,7 @@ public final class com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLa } public final class com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$Companion { + public final fun rememberLauncher (Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$Config;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ReadyCallback;Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$ResultCallback;Landroidx/compose/runtime/Composer;I)Lcom/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher; } public final class com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher$Config : android/os/Parcelable { @@ -6898,6 +6905,7 @@ public final class com/stripe/android/payments/paymentlauncher/PaymentLauncher$C public static synthetic fun create$default (Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher$Companion;Landroidx/activity/ComponentActivity;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher$PaymentResultCallback;ILjava/lang/Object;)Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher; public static synthetic fun create$default (Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher$Companion;Landroidx/fragment/app/Fragment;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher$PaymentResultCallback;ILjava/lang/Object;)Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher; public final fun createForCompose (Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher$PaymentResultCallback;Landroidx/compose/runtime/Composer;II)Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher; + public final fun rememberLauncher (Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher$PaymentResultCallback;Landroidx/compose/runtime/Composer;II)Lcom/stripe/android/payments/paymentlauncher/PaymentLauncher; } public abstract interface class com/stripe/android/payments/paymentlauncher/PaymentLauncher$PaymentResultCallback { diff --git a/payments-core/build.gradle b/payments-core/build.gradle index 3b3dd5f2d8b..a6f59f50b68 100644 --- a/payments-core/build.gradle +++ b/payments-core/build.gradle @@ -19,26 +19,24 @@ dependencies { implementation "androidx.fragment:fragment-ktx:$androidxFragmentVersion" implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintlayoutVersion" implementation "androidx.activity:activity-ktx:$androidxActivityVersion" + implementation "androidx.activity:activity-compose:$androidxActivityVersion" implementation "com.google.android.gms:play-services-wallet:$playServicesWalletVersion" implementation "com.google.android.material:material:$materialVersion" implementation "com.google.dagger:dagger:$daggerVersion" - kapt "com.google.dagger:dagger-compiler:$daggerVersion" - - implementation "androidx.activity:activity-compose:$androidxActivityVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion" implementation 'com.google.android.instantapps:instantapps:1.1.0' - // For instructions on replacing the BouncyCastle dependency used by the 3DS2 SDK, see // https://github.com/stripe/stripe-android/issues/3173#issuecomment-785176608 implementation "com.stripe:stripe-3ds2-android:6.1.5" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion" + kapt "com.google.dagger:dagger-compiler:$daggerVersion" javadocDeps "androidx.annotation:annotation:$androidxAnnotationVersion" javadocDeps "androidx.appcompat:appcompat:$androidxAppcompatVersion" javadocDeps "com.google.android.material:material:$materialVersion" - testImplementation project(':financial-connections') + testImplementation project(':financial-connections') testImplementation "junit:junit:$junitVersion" testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" @@ -56,6 +54,7 @@ dependencies { androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test:rules:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion" + androidTestUtil "androidx.test:orchestrator:$androidTestVersion" ktlint "com.pinterest:ktlint:$ktlintVersion" diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncher.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncher.kt index 5123bae901c..6ba57496a4e 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncher.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncher.kt @@ -2,7 +2,14 @@ package com.stripe.android.googlepaylauncher import android.os.Parcelable import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.stripe.android.PaymentConfiguration @@ -276,7 +283,58 @@ class GooglePayLauncher internal constructor( fun onResult(result: Result) } - internal companion object { + companion object { internal const val PRODUCT_USAGE = "GooglePayLauncher" + + /** + * Create a [GooglePayLauncher] used for Jetpack Compose. + * + * This API uses Compose specific API [rememberLauncherForActivityResult] to register a + * [ActivityResultLauncher] into current activity, it should be called as part of Compose + * initialization path. + * The GooglePayLauncher created is remembered across recompositions. Recomposition will + * always return the value produced by composition. + */ + @Composable + fun rememberLauncher( + config: Config, + readyCallback: ReadyCallback, + resultCallback: ResultCallback + ): GooglePayLauncher { + val currentReadyCallback by rememberUpdatedState(readyCallback) + + val context = LocalContext.current + val lifecycleScope = LocalLifecycleOwner.current.lifecycleScope + val activityResultLauncher = rememberLauncherForActivityResult( + GooglePayLauncherContract(), + resultCallback::onResult + ) + + return remember(config) { + GooglePayLauncher( + lifecycleScope = lifecycleScope, + config = config, + readyCallback = { + currentReadyCallback.onReady(it) + }, + activityResultLauncher = activityResultLauncher, + googlePayRepositoryFactory = { + DefaultGooglePayRepository( + context = context, + environment = config.environment, + billingAddressParameters = config.billingAddressConfig.convert(), + existingPaymentMethodRequired = config.existingPaymentMethodRequired, + allowCreditCards = config.allowCreditCards + ) + }, + PaymentAnalyticsRequestFactory( + context, + PaymentConfiguration.getInstance(context).publishableKey, + setOf(PRODUCT_USAGE) + ), + DefaultAnalyticsRequestExecutor() + ) + } + } } } diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher.kt index 8b0cf1575c0..bfe201cb43e 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncher.kt @@ -3,9 +3,15 @@ package com.stripe.android.googlepaylauncher import android.content.Context import android.os.Parcelable import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultCaller +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IntDef +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.stripe.android.BuildConfig @@ -129,10 +135,13 @@ class GooglePayPaymentMethodLauncher @AssistedInject internal constructor( ) : this( activity, activity.lifecycleScope, - activity, + activity.registerForActivityResult( + GooglePayPaymentMethodLauncherContract() + ) { + resultCallback.onResult(it) + }, config, - readyCallback, - resultCallback + readyCallback ) /** @@ -154,28 +163,26 @@ class GooglePayPaymentMethodLauncher @AssistedInject internal constructor( ) : this( fragment.requireContext(), fragment.viewLifecycleOwner.lifecycleScope, - fragment, + fragment.registerForActivityResult( + GooglePayPaymentMethodLauncherContract() + ) { + resultCallback.onResult(it) + }, config, - readyCallback, - resultCallback + readyCallback ) private constructor ( context: Context, lifecycleScope: CoroutineScope, - activityResultCaller: ActivityResultCaller, + activityResultLauncher: ActivityResultLauncher, config: Config, - readyCallback: ReadyCallback, - resultCallback: ResultCallback + readyCallback: ReadyCallback ) : this( lifecycleScope, config, readyCallback, - activityResultCaller.registerForActivityResult( - GooglePayPaymentMethodLauncherContract() - ) { - resultCallback.onResult(it) - }, + activityResultLauncher, false, context, googlePayRepositoryFactory = { @@ -376,5 +383,42 @@ class GooglePayPaymentMethodLauncher @AssistedInject internal constructor( // Error executing a network call const val NETWORK_ERROR = 3 + + /** + * Create a [GooglePayPaymentMethodLauncher] used for Jetpack Compose. + * + * This API uses Compose specific API [rememberLauncherForActivityResult] to register a + * [ActivityResultLauncher] into current activity, it should be called as part of Compose + * initialization path. + * The GooglePayPaymentMethodLauncher created is remembered across recompositions. + * Recomposition will always return the value produced by composition. + */ + @Composable + fun rememberLauncher( + config: Config, + readyCallback: ReadyCallback, + resultCallback: ResultCallback + ): GooglePayPaymentMethodLauncher { + val currentReadyCallback by rememberUpdatedState(readyCallback) + + val context = LocalContext.current + val lifecycleScope = LocalLifecycleOwner.current.lifecycleScope + val activityResultLauncher = rememberLauncherForActivityResult( + GooglePayPaymentMethodLauncherContract(), + resultCallback::onResult + ) + + return remember(config) { + GooglePayPaymentMethodLauncher( + context = context, + lifecycleScope = lifecycleScope, + activityResultLauncher = activityResultLauncher, + config = config, + readyCallback = { + currentReadyCallback.onReady(it) + } + ) + } + } } } diff --git a/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncher.kt b/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncher.kt index ecaf4f307c7..9c6775864fa 100644 --- a/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncher.kt +++ b/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncher.kt @@ -4,6 +4,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.fragment.app.Fragment import com.stripe.android.model.ConfirmPaymentIntentParams @@ -75,7 +76,14 @@ interface PaymentLauncher { * This API uses Compose specific API [rememberLauncherForActivityResult] to register a * [ActivityResultLauncher] into current activity, it should be called as part of Compose * initialization path. + * This method creates a new PaymentLauncher object every time it is called, even during + * recompositions. */ + @Deprecated( + "This method creates a new PaymentLauncher object every time it is called, even" + + "during recompositions.", + replaceWith = ReplaceWith("PaymentLauncher.rememberLauncher(publishableKey, stripeAccountId, callback)") + ) @Composable fun createForCompose( publishableKey: String, @@ -88,5 +96,34 @@ interface PaymentLauncher { callback::onPaymentResult ) ).create(publishableKey, stripeAccountId) + + /** + * Create a [PaymentLauncher] used for Jetpack Compose. + * + * This API uses Compose specific API [rememberLauncherForActivityResult] to register a + * [ActivityResultLauncher] into current activity, it should be called as part of Compose + * initialization path. + * The PaymentLauncher created is remembered across recompositions. Recomposition will + * always return the value produced by composition. + */ + @Composable + fun rememberLauncher( + publishableKey: String, + stripeAccountId: String? = null, + callback: PaymentResultCallback + ): PaymentLauncher { + val context = LocalContext.current + val activityResultLauncher = rememberLauncherForActivityResult( + PaymentLauncherContract(), + callback::onPaymentResult + ) + + return remember(publishableKey, stripeAccountId) { + PaymentLauncherFactory( + context, + activityResultLauncher + ).create(publishableKey, stripeAccountId) + } + } } }