diff --git a/CHANGELOG.md b/CHANGELOG.md index 6980096c19..cdba5e1dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Remove requirement for `GooglePayActivity` to be declared in the Android manifest (fixes #1572) * Note: If upgrading from v4, any manual `GooglePayActivity` declaration in your app's manifest should be removed * Deprecate unused `GooglePayClient.EXTRA_ENVIRONMENT` and `GooglePayClient.EXTRA_PAYMENT_DATA_REQUEST` constants + * Update Google Pay dependency (play-services-wallet) to version 19.5.0 ## 5.28.1 (2026-06-01) diff --git a/Demo/build.gradle b/Demo/build.gradle index e281c097c2..4527e0d913 100644 --- a/Demo/build.gradle +++ b/Demo/build.gradle @@ -100,6 +100,7 @@ dependencies { def composeBom = platform(libs.androidx.compose.bom) implementation composeBom implementation libs.androidx.material3 + implementation libs.google.pay.button debugImplementation libs.leakcanary diff --git a/Demo/src/main/java/com/braintreepayments/demo/GooglePayFragment.java b/Demo/src/main/java/com/braintreepayments/demo/GooglePayFragment.java deleted file mode 100644 index 1118b9c1d2..0000000000 --- a/Demo/src/main/java/com/braintreepayments/demo/GooglePayFragment.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.braintreepayments.demo; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; -import androidx.navigation.NavDirections; -import androidx.navigation.fragment.NavHostFragment; - -import com.braintreepayments.api.core.PaymentMethodNonce; -import com.braintreepayments.api.core.UserCanceledException; -import com.braintreepayments.api.googlepay.GooglePayBillingAddressFormat; -import com.braintreepayments.api.googlepay.GooglePayCheckoutOption; -import com.braintreepayments.api.googlepay.GooglePayClient; -import com.braintreepayments.api.googlepay.GooglePayLauncher; -import com.braintreepayments.api.googlepay.GooglePayPaymentAuthRequest; -import com.braintreepayments.api.googlepay.GooglePayReadinessResult; -import com.braintreepayments.api.googlepay.GooglePayRequest; -import com.braintreepayments.api.googlepay.GooglePayResult; -import com.braintreepayments.api.googlepay.GooglePayShippingAddressParameters; -import com.braintreepayments.api.googlepay.GooglePayTotalPriceStatus; - -public class GooglePayFragment extends BaseFragment { - - private ImageButton googlePayButton; - private GooglePayClient googlePayClient; - private GooglePayLauncher googlePayLauncher; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_google_pay, container, false); - googlePayButton = view.findViewById(R.id.google_pay_button); - googlePayButton.setOnClickListener(this::launchGooglePay); - - googlePayClient = new GooglePayClient(requireContext(), super.getAuthStringArg()); - googlePayLauncher = new GooglePayLauncher(this, - paymentAuthResult -> googlePayClient.tokenize(paymentAuthResult, - (googlePayResult) -> { - if (googlePayResult instanceof GooglePayResult.Failure) { - handleError(((GooglePayResult.Failure) googlePayResult).getError()); - } else if (googlePayResult instanceof GooglePayResult.Success){ - handleGooglePayActivityResult(((GooglePayResult.Success) googlePayResult).getNonce()); - } else if (googlePayResult instanceof GooglePayResult.Cancel) { - handleError(new UserCanceledException("User canceled Google Pay")); - } - })); - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - googlePayClient.isReadyToPay(requireActivity(), (googlePayReadinessResult) -> { - if (googlePayReadinessResult instanceof GooglePayReadinessResult.ReadyToPay) { - googlePayButton.setVisibility(View.VISIBLE); - } else { - showDialog( - "Google Payments are not available. The following issues could be the cause:\n\n" + - "No user is logged in to the device.\n\n" + - "Google Play Services is missing or out of date."); - } - }); - } - - @Override - public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) { - super.onPaymentMethodNonceCreated(paymentMethodNonce); - - GooglePayFragmentDirections.ActionGooglePayFragmentToDisplayNonceFragment action = - GooglePayFragmentDirections.actionGooglePayFragmentToDisplayNonceFragment( - paymentMethodNonce); - - NavHostFragment.findNavController(this).navigate(action); - } - - public void launchGooglePay(View v) { - FragmentActivity activity = getActivity(); - activity.setProgressBarIndeterminateVisibility(true); - - GooglePayRequest googlePayRequest = new GooglePayRequest(Settings.getGooglePayCurrency(activity), "1.00", GooglePayTotalPriceStatus.TOTAL_PRICE_STATUS_FINAL); - googlePayRequest.setTotalPriceLabel("Braintree Demo Payment"); - googlePayRequest.setAllowPrepaidCards(Settings.areGooglePayPrepaidCardsAllowed(activity)); - googlePayRequest.setBillingAddressFormat(GooglePayBillingAddressFormat.FULL); - googlePayRequest.setBillingAddressRequired( - Settings.isGooglePayBillingAddressRequired(activity)); - googlePayRequest.setEmailRequired(Settings.isGooglePayEmailRequired(activity)); - googlePayRequest.setPhoneNumberRequired(Settings.isGooglePayPhoneNumberRequired(activity)); - googlePayRequest.setShippingAddressRequired( - Settings.isGooglePayShippingAddressRequired(activity)); - googlePayRequest.setShippingAddressParameters(new GooglePayShippingAddressParameters(Settings.getGooglePayAllowedCountriesForShipping(requireContext()))); - - googlePayRequest.setCheckoutOption(GooglePayCheckoutOption.COMPLETE_IMMEDIATE_PURCHASE); - googlePayClient.createPaymentAuthRequest(googlePayRequest, (paymentAuthRequest) -> { - if (paymentAuthRequest instanceof GooglePayPaymentAuthRequest.ReadyToLaunch) { - googlePayLauncher.launch( - ((GooglePayPaymentAuthRequest.ReadyToLaunch) paymentAuthRequest)); - } else if (paymentAuthRequest instanceof GooglePayPaymentAuthRequest.Failure) { - handleError(((GooglePayPaymentAuthRequest.Failure) paymentAuthRequest).getError()); - } - }); - } - - private void handleGooglePayActivityResult(PaymentMethodNonce paymentMethodNonce) { - super.onPaymentMethodNonceCreated(paymentMethodNonce); - - NavDirections action = - GooglePayFragmentDirections.actionGooglePayFragmentToDisplayNonceFragment( - paymentMethodNonce); - NavHostFragment.findNavController(this).navigate(action); - } -} diff --git a/Demo/src/main/java/com/braintreepayments/demo/GooglePayFragment.kt b/Demo/src/main/java/com/braintreepayments/demo/GooglePayFragment.kt new file mode 100644 index 0000000000..e6bba2ab8d --- /dev/null +++ b/Demo/src/main/java/com/braintreepayments/demo/GooglePayFragment.kt @@ -0,0 +1,176 @@ +package com.braintreepayments.demo + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.braintreepayments.api.core.PaymentMethodNonce +import com.braintreepayments.api.core.UserCanceledException +import com.braintreepayments.api.googlepay.GooglePayBillingAddressFormat +import com.braintreepayments.api.googlepay.GooglePayCheckoutOption +import com.braintreepayments.api.googlepay.GooglePayClient +import com.braintreepayments.api.googlepay.GooglePayLauncher +import com.braintreepayments.api.googlepay.GooglePayPaymentAuthRequest +import com.braintreepayments.api.googlepay.GooglePayReadinessResult +import com.braintreepayments.api.googlepay.GooglePayRequest +import com.braintreepayments.api.googlepay.GooglePayResult +import com.braintreepayments.api.googlepay.GooglePayShippingAddressParameters +import com.braintreepayments.api.googlepay.GooglePayTotalPriceStatus +import com.google.pay.button.ButtonType +import com.google.pay.button.PayButton +import androidx.compose.foundation.layout.Column +import org.json.JSONArray +import org.json.JSONObject + +class GooglePayFragment : BaseFragment() { + + private lateinit var googlePayClient: GooglePayClient + private lateinit var googlePayLauncher: GooglePayLauncher + + private val args: GooglePayFragmentArgs by navArgs() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + googlePayClient = GooglePayClient(requireContext(), args.authString) + googlePayLauncher = GooglePayLauncher(this) { paymentAuthResult -> + googlePayClient.tokenize(paymentAuthResult) { googlePayResult -> + when (googlePayResult) { + is GooglePayResult.Failure -> handleError(googlePayResult.error) + is GooglePayResult.Success -> handleGooglePayActivityResult(googlePayResult.nonce) + is GooglePayResult.Cancel -> handleError(UserCanceledException("User canceled Google Pay")) + } + } + } + + return ComposeView(requireContext()).apply { + setContent { + GooglePayScreen() + } + } + } + + @Composable + fun GooglePayScreen() { + var isReadyToPay by remember { mutableStateOf(false) } + var allowedPaymentMethods by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + googlePayClient.isReadyToPay(requireActivity()) { result -> + if (result is GooglePayReadinessResult.ReadyToPay) { + isReadyToPay = true + // Note: We avoid createPaymentAuthRequest here to prevent unnecessary analytics noise. + // Instead, we assume common card networks are supported for the demo. + val cardParams = JSONObject() + .put( + "allowedAuthMethods", + JSONArray().put("PAN_ONLY").put("CRYPTOGRAM_3DS") + ) + .put( + "allowedCardNetworks", + JSONArray().put("VISA").put("MASTERCARD").put("AMEX").put("DISCOVER") + .put("JCB") + ) + + allowedPaymentMethods = JSONArray().put( + JSONObject().put("type", "CARD").put("parameters", cardParams) + ).toString() + } else { + showDialog( + "Google Pay is not available. The following issues could be the cause:\n\n" + + "No user is logged in to the device.\n\n" + + "Google Play Services is missing or out of date." + ) + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (isReadyToPay) { + if (allowedPaymentMethods.isNotEmpty()) { + PayButton( + modifier = Modifier.width(280.dp), + onClick = { launchGooglePay() }, + allowedPaymentMethods = allowedPaymentMethods, + type = ButtonType.Buy + ) + } else { + LaunchedEffect(Unit) { + showDialog( + "allowedPaymentMethods cannot be empty.\n\n" + + "Please configure allowedPaymentMethods (e.g. CARD) " + + "to display the Google Pay button." + ) + } + } + } + } + } + } + + private fun launchGooglePay() { + val activity = requireActivity() + val googlePayRequest = GooglePayRequest( + Settings.getGooglePayCurrency(activity), + "1.00", + GooglePayTotalPriceStatus.TOTAL_PRICE_STATUS_FINAL + ).apply { + totalPriceLabel = "Braintree Demo Payment" + allowPrepaidCards = Settings.areGooglePayPrepaidCardsAllowed(activity) + billingAddressFormat = GooglePayBillingAddressFormat.FULL + isBillingAddressRequired = Settings.isGooglePayBillingAddressRequired(activity) + isEmailRequired = Settings.isGooglePayEmailRequired(activity) + isPhoneNumberRequired = Settings.isGooglePayPhoneNumberRequired(activity) + isShippingAddressRequired = Settings.isGooglePayShippingAddressRequired(activity) + shippingAddressParameters = GooglePayShippingAddressParameters( + Settings.getGooglePayAllowedCountriesForShipping(requireContext()) + ) + checkoutOption = GooglePayCheckoutOption.COMPLETE_IMMEDIATE_PURCHASE + } + + googlePayClient.createPaymentAuthRequest(googlePayRequest) { paymentAuthRequest -> + when (paymentAuthRequest) { + is GooglePayPaymentAuthRequest.ReadyToLaunch -> { + googlePayLauncher.launch(paymentAuthRequest) + } + + is GooglePayPaymentAuthRequest.Failure -> { + handleError(paymentAuthRequest.error) + } + } + } + } + + private fun handleGooglePayActivityResult(paymentMethodNonce: PaymentMethodNonce) { + super.onPaymentMethodNonceCreated(paymentMethodNonce) + + val action = GooglePayFragmentDirections.actionGooglePayFragmentToDisplayNonceFragment( + paymentMethodNonce + ) + findNavController().navigate(action) + } +} diff --git a/Demo/src/main/res/layout/fragment_google_pay.xml b/Demo/src/main/res/layout/fragment_google_pay.xml deleted file mode 100644 index 527ec3284c..0000000000 --- a/Demo/src/main/res/layout/fragment_google_pay.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc763df876..84a77cc7b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ browserSwitch = "3.5.1" cardinal = "2.2.7-7" navigationSafeArgsGradlePlugin = "2.5.0" paypalMessages = "1.1.13" -playServices = "19.4.0" +playServicesWallet = "19.5.0" junit = "4.13.2" testParameterInjector = "1.18" robolectric = "4.14-beta-1" @@ -47,6 +47,7 @@ deviceAutomator = "1.0.0" btCardForm = "5.4.0" accompanist = "0.37.3" datastore = "1.2.0" +googlePayButton = "1.1.0" [libraries] # Androidx @@ -76,7 +77,7 @@ gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } browser-switch = { group = "com.braintreepayments.api", name = "browser-switch", version.ref = "browserSwitch" } cardinal = { group = "org.jfrog.cardinalcommerce.gradle", name = "cardinalmobilesdk", version.ref = "cardinal" } paypal-messages = { module = "com.paypal.messages:paypal-messages", version.ref = "paypalMessages" } -play-services-wallet = { group = "com.google.android.gms", name = "play-services-wallet", version.ref = "playServices" } +play-services-wallet = { group = "com.google.android.gms", name = "play-services-wallet", version.ref = "playServicesWallet" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } json-assert = { group = "org.skyscreamer", name = "jsonassert", version.ref = "jsonAssert" } @@ -117,6 +118,7 @@ bt-card-form = { module = "com.braintreepayments:card-form", version.ref = "btCa androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview"} androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"} androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } +google-pay-button = { module = "com.google.pay.button:compose-pay-button", version.ref = "googlePayButton" } [plugins] android-application = { id = "com.android.application", version.ref = "gradle" }