diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d18a3a2ad..dc374e0c1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Payments * [CHANGED] [5552](https://github.com/stripe/stripe-android/pull/5552) Make `PaymentMethod.Card.networks` field public. +* [FIXED][5554](https://github.com/stripe/stripe-android/pull/5554) Fix Alipay integration when using the Alipay SDK. ## 20.12.0 - 2022-09-13 This release upgrades `compileSdkVersion` to 33, updates Google Pay button to match the new brand diff --git a/example/AndroidManifest.xml b/example/AndroidManifest.xml index 4fff83498f6..c5ad89b71e1 100644 --- a/example/AndroidManifest.xml +++ b/example/AndroidManifest.xml @@ -72,6 +72,8 @@ + + diff --git a/example/build.gradle b/example/build.gradle index 2a187a96ff4..cb0b28dfa2d 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -32,6 +32,9 @@ def getAccountId() { dependencies { implementation project(':payments') implementation project(':financial-connections') + + implementation("com.alipay.sdk:alipaysdk-android:15.8.11") + implementation "androidx.appcompat:appcompat:$androidxAppcompatVersion" implementation "androidx.recyclerview:recyclerview:$androidxRecyclerviewVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion" diff --git a/example/res/values/strings.xml b/example/res/values/strings.xml index 8a0b3eb706c..78d961505ca 100644 --- a/example/res/values/strings.xml +++ b/example/res/values/strings.xml @@ -87,6 +87,11 @@ Confirm with Affirm Affirm Payment Intent Example + Tapping the button below will create a PaymentIntent and then use Alipay to confirm it + Confirm with Alipay + Alipay Payment Intent Native Example + Alipay Payment Intent Web Example + By providing your IBAN and confirming this payment, you are authorizing EXAMPLE COMPANY NAME and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited. IBAN diff --git a/example/src/main/java/com/stripe/example/activity/AlipayPaymentNativeActivity.kt b/example/src/main/java/com/stripe/example/activity/AlipayPaymentNativeActivity.kt new file mode 100644 index 00000000000..98bc189b821 --- /dev/null +++ b/example/src/main/java/com/stripe/example/activity/AlipayPaymentNativeActivity.kt @@ -0,0 +1,126 @@ +package com.stripe.example.activity + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import com.alipay.sdk.app.PayTask +import com.stripe.android.ApiResultCallback +import com.stripe.android.PaymentConfiguration +import com.stripe.android.PaymentIntentResult +import com.stripe.android.Stripe +import com.stripe.android.model.ConfirmPaymentIntentParams +import com.stripe.android.model.MandateDataParams +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.StripeIntent +import com.stripe.example.R +import com.stripe.example.databinding.PaymentExampleActivityBinding +import org.json.JSONObject + +class AlipayPaymentNativeActivity : StripeIntentActivity() { + + private val viewBinding: PaymentExampleActivityBinding by lazy { + PaymentExampleActivityBinding.inflate(layoutInflater) + } + + private val stripe: Stripe by lazy { + Stripe( + applicationContext, + PaymentConfiguration.getInstance(applicationContext).publishableKey + ) + } + + private var clientSecret: String? = null + private var confirmed = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + + viewBinding.confirmWithPaymentButton.text = + resources.getString(R.string.confirm_alipay_button) + viewBinding.paymentExampleIntro.text = + resources.getString(R.string.alipay_example_intro) + + viewModel.inProgress.observe(this) { enableUi(!it) } + viewModel.status.observe(this, Observer(viewBinding.status::setText)) + + viewBinding.confirmWithPaymentButton.setOnClickListener { + clientSecret?.let { + // If we already loaded the Payment Intent and haven't confirmed, try again + if (!confirmed) { + updateStatus("\n\nPayment Intent already created, trying to confirm") + confirmPayment(it) + } + } ?: run { + createAndConfirmPaymentIntent( + country = "US", + paymentMethodCreateParams = PaymentMethodCreateParams.createAlipay(), + supportedPaymentMethods = "alipay" + ) + } + } + } + + override fun handleCreatePaymentIntentResponse( + responseData: JSONObject, + params: PaymentMethodCreateParams?, + shippingDetails: ConfirmPaymentIntentParams.Shipping?, + stripeAccountId: String?, + existingPaymentMethodId: String?, + mandateDataParams: MandateDataParams?, + onPaymentIntentCreated: (String) -> Unit + ) { + viewModel.status.value += + "\n\nStarting PaymentIntent confirmation" + + ( + stripeAccountId?.let { + " for $it" + } ?: "" + ) + + clientSecret = responseData.getString("secret").also { + confirmPayment(it) + } + } + + private fun confirmPayment(clientSecret: String) { + stripe.confirmAlipayPayment( + confirmPaymentIntentParams = ConfirmPaymentIntentParams.createAlipay(clientSecret), + authenticator = { data -> + PayTask(this).payV2(data, true) + }, + callback = object : ApiResultCallback { + override fun onSuccess(result: PaymentIntentResult) { + val paymentIntent = result.intent + when (paymentIntent.status) { + StripeIntent.Status.Succeeded -> { + confirmed = true + updateStatus("\n\nPayment succeeded") + } + StripeIntent.Status.RequiresAction -> + updateStatus("\n\nUser canceled confirmation") + else -> + updateStatus( + "\n\nPayment failed or canceled." + + "\nStatus: ${paymentIntent.status}" + ) + } + } + + override fun onError(e: Exception) { + updateStatus("\n\nError: ${e.message}") + } + } + ) + } + + private fun updateStatus(appendMessage: String) { + viewModel.status.value += appendMessage + viewModel.inProgress.postValue(false) + } + + private fun enableUi(enable: Boolean) { + viewBinding.progressBar.visibility = if (enable) View.INVISIBLE else View.VISIBLE + viewBinding.confirmWithPaymentButton.isEnabled = enable + } +} diff --git a/example/src/main/java/com/stripe/example/activity/AlipayPaymentWebActivity.kt b/example/src/main/java/com/stripe/example/activity/AlipayPaymentWebActivity.kt new file mode 100644 index 00000000000..edfefbd3426 --- /dev/null +++ b/example/src/main/java/com/stripe/example/activity/AlipayPaymentWebActivity.kt @@ -0,0 +1,130 @@ +package com.stripe.example.activity + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import com.stripe.android.ApiResultCallback +import com.stripe.android.PaymentConfiguration +import com.stripe.android.PaymentIntentResult +import com.stripe.android.Stripe +import com.stripe.android.model.ConfirmPaymentIntentParams +import com.stripe.android.model.MandateDataParams +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.StripeIntent +import com.stripe.example.R +import com.stripe.example.databinding.PaymentExampleActivityBinding +import org.json.JSONObject + +class AlipayPaymentWebActivity : StripeIntentActivity() { + + private val viewBinding: PaymentExampleActivityBinding by lazy { + PaymentExampleActivityBinding.inflate(layoutInflater) + } + + private val stripe: Stripe by lazy { + Stripe( + applicationContext, + PaymentConfiguration.getInstance(applicationContext).publishableKey + ) + } + + private var clientSecret: String? = null + private var confirmed = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + + viewBinding.confirmWithPaymentButton.text = + resources.getString(R.string.confirm_alipay_button) + viewBinding.paymentExampleIntro.text = + resources.getString(R.string.alipay_example_intro) + + viewModel.inProgress.observe(this) { enableUi(!it) } + viewModel.status.observe(this, Observer(viewBinding.status::setText)) + + viewBinding.confirmWithPaymentButton.setOnClickListener { + clientSecret?.let { + // If we already loaded the Payment Intent and haven't confirmed, try again + if (!confirmed) { + updateStatus("\n\nPayment Intent already created, trying to confirm") + confirmPayment(it) + } + } ?: run { + createAndConfirmPaymentIntent( + country = "US", + paymentMethodCreateParams = PaymentMethodCreateParams.createAlipay(), + supportedPaymentMethods = "alipay" + ) + } + } + } + + override fun handleCreatePaymentIntentResponse( + responseData: JSONObject, + params: PaymentMethodCreateParams?, + shippingDetails: ConfirmPaymentIntentParams.Shipping?, + stripeAccountId: String?, + existingPaymentMethodId: String?, + mandateDataParams: MandateDataParams?, + onPaymentIntentCreated: (String) -> Unit + ) { + viewModel.status.value += + "\n\nStarting PaymentIntent confirmation" + + ( + stripeAccountId?.let { + " for $it" + } ?: "" + ) + + clientSecret = responseData.getString("secret").also { + confirmPayment(it) + } + } + + private fun confirmPayment(clientSecret: String) { + stripe.confirmPayment(this, ConfirmPaymentIntentParams.createAlipay(clientSecret)) + } + + private fun updateStatus(appendMessage: String) { + viewModel.status.value += appendMessage + viewModel.inProgress.postValue(false) + } + + private fun enableUi(enable: Boolean) { + viewBinding.progressBar.visibility = if (enable) View.INVISIBLE else View.VISIBLE + viewBinding.confirmWithPaymentButton.isEnabled = enable + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + // Handle the result of stripe.confirmPayment + stripe.onPaymentResult( + requestCode, + data, + object : ApiResultCallback { + override fun onSuccess(result: PaymentIntentResult) { + val paymentIntent = result.intent + val status = paymentIntent.status + when (status) { + StripeIntent.Status.Succeeded -> + updateStatus("\n\nPayment succeeded") + StripeIntent.Status.RequiresAction -> + updateStatus("\n\nUser canceled confirmation") + else -> + updateStatus( + "\n\nPayment failed or canceled." + + "\nStatus: ${paymentIntent.status}" + ) + } + } + + override fun onError(e: Exception) { + updateStatus("\n\nError: ${e.message}") + } + } + ) + } +} 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 6e7e1db3c52..ee2b64cca34 100644 --- a/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/LauncherActivity.kt @@ -109,6 +109,14 @@ class LauncherActivity : AppCompatActivity() { activity.getString(R.string.confirm_with_affirm), AffirmPaymentActivity::class.java ), + Item( + activity.getString(R.string.confirm_with_alipay_native), + AlipayPaymentNativeActivity::class.java + ), + Item( + activity.getString(R.string.confirm_with_alipay_web), + AlipayPaymentWebActivity::class.java + ), Item( activity.getString(R.string.becs_debit_example), BecsDebitPaymentMethodActivity::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 c15f1397794..607732b0e84 100644 --- a/example/src/main/java/com/stripe/example/activity/StripeIntentActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/StripeIntentActivity.kt @@ -127,7 +127,7 @@ abstract class StripeIntentActivity : AppCompatActivity() { } } - private fun handleCreatePaymentIntentResponse( + open fun handleCreatePaymentIntentResponse( responseData: JSONObject, params: PaymentMethodCreateParams?, shippingDetails: ConfirmPaymentIntentParams.Shipping?, diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt index 6b8423110a1..8749a60bad0 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt @@ -251,7 +251,9 @@ internal abstract class IdentityDocumentScanFragment( notSubmitBlock = if (verificationPage.requireSelfie()) { ({ findNavController().navigate(R.id.action_global_selfieFragment) }) - } else null + } else { + null + } ) }.onFailure { throwable -> Log.e( diff --git a/payments-core/src/main/java/com/stripe/android/networking/StripeApiRepository.kt b/payments-core/src/main/java/com/stripe/android/networking/StripeApiRepository.kt index 4013097331a..f50d2fb98f5 100644 --- a/payments-core/src/main/java/com/stripe/android/networking/StripeApiRepository.kt +++ b/payments-core/src/main/java/com/stripe/android/networking/StripeApiRepository.kt @@ -92,7 +92,6 @@ import com.stripe.android.payments.core.injection.PRODUCT_USAGE import com.stripe.android.utils.StripeUrlUtils import kotlinx.coroutines.Dispatchers import org.json.JSONException -import org.json.JSONObject import java.io.IOException import java.net.HttpURLConnection import java.security.Security @@ -1112,7 +1111,7 @@ class StripeApiRepository @JvmOverloads internal constructor( override suspend fun retrieveObject( url: String, requestOptions: ApiRequest.Options - ): JSONObject { + ): StripeResponse { if (!StripeUrlUtils.isStripeUrl(url)) { throw IllegalArgumentException("Unrecognized domain: $url") } @@ -1125,7 +1124,7 @@ class StripeApiRepository @JvmOverloads internal constructor( fireAnalyticsRequest(PaymentAnalyticsEvent.StripeUrlRetrieve) } - return response.responseJson() + return response } /** diff --git a/payments-core/src/main/java/com/stripe/android/networking/StripeRepository.kt b/payments-core/src/main/java/com/stripe/android/networking/StripeRepository.kt index 6185f4da2ba..2a443ba0395 100644 --- a/payments-core/src/main/java/com/stripe/android/networking/StripeRepository.kt +++ b/payments-core/src/main/java/com/stripe/android/networking/StripeRepository.kt @@ -10,6 +10,7 @@ import com.stripe.android.core.exception.InvalidRequestException import com.stripe.android.core.model.StripeFile import com.stripe.android.core.model.StripeFileParams import com.stripe.android.core.networking.ApiRequest +import com.stripe.android.core.networking.StripeResponse import com.stripe.android.exception.CardException import com.stripe.android.model.BankStatuses import com.stripe.android.model.CardMetadata @@ -40,7 +41,6 @@ import com.stripe.android.model.StripeIntent import com.stripe.android.model.Token import com.stripe.android.model.TokenParams import org.json.JSONException -import org.json.JSONObject import java.util.Locale /** @@ -391,7 +391,7 @@ abstract class StripeRepository { internal abstract suspend fun retrieveObject( url: String, requestOptions: ApiRequest.Options - ): JSONObject + ): StripeResponse internal abstract suspend fun createRadarSession( requestOptions: ApiRequest.Options diff --git a/payments-core/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt b/payments-core/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt index 790c1dc8b02..bb8322db052 100644 --- a/payments-core/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt +++ b/payments-core/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt @@ -5,6 +5,7 @@ import com.stripe.android.core.exception.APIException import com.stripe.android.core.model.StripeFile import com.stripe.android.core.model.StripeFileParams import com.stripe.android.core.networking.ApiRequest +import com.stripe.android.core.networking.StripeResponse import com.stripe.android.model.BankStatuses import com.stripe.android.model.BinFixtures import com.stripe.android.model.CardMetadata @@ -34,7 +35,6 @@ import com.stripe.android.model.Stripe3ds2AuthResultFixtures import com.stripe.android.model.StripeIntent import com.stripe.android.model.Token import com.stripe.android.model.TokenParams -import org.json.JSONObject import java.util.Locale internal abstract class AbsFakeStripeRepository : StripeRepository() { @@ -279,7 +279,7 @@ internal abstract class AbsFakeStripeRepository : StripeRepository() { override suspend fun retrieveObject( url: String, requestOptions: ApiRequest.Options - ) = JSONObject() + ) = StripeResponse(1, "response") override suspend fun createRadarSession( requestOptions: ApiRequest.Options diff --git a/payments-core/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt b/payments-core/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt index adf0e33aedd..b4ad5754808 100644 --- a/payments-core/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt +++ b/payments-core/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt @@ -1223,6 +1223,28 @@ internal class StripeApiRepositoryTest { verifyAnalyticsRequest(PaymentAnalyticsEvent.FileCreate) } + @Test + fun retrieveObject_shouldFireExpectedRequestsAndNotParseResult() = runTest { + val responseBody = "not a valid json" + whenever(stripeNetworkClient.executeRequest(any())) + .thenReturn( + StripeResponse( + 200, + responseBody, + emptyMap() + ) + ) + + val response = create().retrieveObject( + StripeApiRepository.paymentMethodsUrl, + DEFAULT_OPTIONS + ) + + verify(stripeNetworkClient).executeRequest(any()) + assertThat(response.body).isEqualTo(responseBody) + verifyAnalyticsRequest(PaymentAnalyticsEvent.StripeUrlRetrieve) + } + @Test fun apiRequest_withErrorResponse_onUnsupportedSdkVersion_shouldNotBeTranslated() = runTest {