From 1ce8b538055ad4b3d143a31ce9e4a80434e09ab4 Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Thu, 12 Sep 2019 07:19:14 -0400 Subject: [PATCH 1/2] Support "intent://" URIs in payment auth WebView `intent://` URIs need to first be parsed into `Intent` objects, then handled. --- .travis.yml | 2 +- .../stripe/android/view/PaymentAuthWebView.kt | 47 ++++++---- .../android/view/PaymentAuthWebViewTest.java | 87 +++++++++++++------ 3 files changed, 91 insertions(+), 45 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29d567782b5..dfac7dbb707 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,4 +47,4 @@ jobs: script: - ./gradlew clean ktlint - ./gradlew checkstyle lintRelease - - ./gradlew testRelease + - ./gradlew testRelease -i diff --git a/stripe/src/main/java/com/stripe/android/view/PaymentAuthWebView.kt b/stripe/src/main/java/com/stripe/android/view/PaymentAuthWebView.kt index aad7354a6fe..4d304a15245 100644 --- a/stripe/src/main/java/com/stripe/android/view/PaymentAuthWebView.kt +++ b/stripe/src/main/java/com/stripe/android/view/PaymentAuthWebView.kt @@ -5,6 +5,7 @@ import android.annotation.TargetApi import android.app.Activity import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.util.AttributeSet @@ -39,7 +40,8 @@ internal class PaymentAuthWebView : WebView { clientSecret: String, returnUrl: String? ) { - webViewClient = PaymentAuthWebViewClient(activity, progressBar, clientSecret, returnUrl) + webViewClient = PaymentAuthWebViewClient(activity, activity.packageManager, progressBar, + clientSecret, returnUrl) } @SuppressLint("SetJavaScriptEnabled") @@ -50,11 +52,12 @@ internal class PaymentAuthWebView : WebView { internal class PaymentAuthWebViewClient( private val activity: Activity, + private val packageManager: PackageManager, private val progressBar: ProgressBar, private val clientSecret: String, returnUrl: String? ) : WebViewClient() { - private val returnUrl: Uri? = if (returnUrl != null) Uri.parse(returnUrl) else null + private val returnUri: Uri? = if (returnUrl != null) Uri.parse(returnUrl) else null override fun onPageCommitVisible(view: WebView, url: String) { super.onPageCommitVisible(view, url) @@ -83,21 +86,29 @@ internal class PaymentAuthWebView : WebView { return if (isReturnUrl(uri)) { onAuthCompleted() true - } else if (!URLUtil.isNetworkUrl(urlString)) { - openNonNetworkUrlDeeplink(uri) + } else if ("intent".equals(uri.scheme, ignoreCase = true)) { + openIntentScheme(uri) + true + } else if (!URLUtil.isNetworkUrl(uri.toString())) { + // Non-network URLs are likely deep-links into banking apps. If the deep-link can be + // opened via an Intent, start it. Otherwise, stop the authentication attempt. + openIntent(Intent(Intent.ACTION_VIEW, uri)) true } else { super.shouldOverrideUrlLoading(view, urlString) } } - /** - * Non-network URLs are likely deep-links into banking apps. If the deep-link can be opened - * via an Intent, start it. Otherwise, stop the authentication attempt. - */ - private fun openNonNetworkUrlDeeplink(uri: Uri) { - val intent = Intent(Intent.ACTION_VIEW, uri) - if (intent.resolveActivity(activity.packageManager) != null) { + private fun openIntentScheme(uri: Uri) { + try { + openIntent(Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME)) + } catch (e: Exception) { + onAuthCompleted() + } + } + + private fun openIntent(intent: Intent) { + if (intent.resolveActivity(packageManager) != null) { activity.startActivity(intent) } else { // complete auth if the deep-link can't be opened @@ -117,19 +128,19 @@ internal class PaymentAuthWebView : WebView { when { isPredefinedReturnUrl(uri) -> return true - // If the `returnUrl` is known, look for URIs that match it. - returnUrl != null -> - return returnUrl.scheme != null && - returnUrl.scheme == uri.scheme && - returnUrl.host != null && - returnUrl.host == uri.host + // If the `returnUri` is known, look for URIs that match it. + returnUri != null -> + return returnUri.scheme != null && + returnUri.scheme == uri.scheme && + returnUri.host != null && + returnUri.host == uri.host else -> { // Skip opaque (i.e. non-hierarchical) URIs if (uri.isOpaque) { return false } - // If the `returnUrl` is unknown, look for URIs that contain a + // If the `returnUri` is unknown, look for URIs that contain a // `payment_intent_client_secret` or `setup_intent_client_secret` // query parameter, and check if its values matches the given `clientSecret` // as a query parameter. diff --git a/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java b/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java index 8eafef8db3b..b346e41c09f 100644 --- a/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java +++ b/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java @@ -1,21 +1,27 @@ package com.stripe.android.view; import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; import android.webkit.WebView; import android.widget.ProgressBar; -import androidx.test.core.app.ApplicationProvider; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) public class PaymentAuthWebViewTest { @@ -23,12 +29,13 @@ public class PaymentAuthWebViewTest { @Mock private Activity mActivity; @Mock private ProgressBar mProgressBar; @Mock private WebView mWebView; + @Mock private PackageManager mPackageManager; + + @Captor private ArgumentCaptor mIntentArgumentCaptor; @Before public void setup() { MockitoAnnotations.initMocks(this); - when(mActivity.getPackageManager()) - .thenReturn(ApplicationProvider.getApplicationContext().getPackageManager()); } @Test @@ -36,9 +43,10 @@ public void shouldOverrideUrlLoading_withPaymentIntent_shouldSetResult() { final String deepLink = "stripe://payment_intent_return?payment_intent=pi_123&" + "payment_intent_client_secret=pi_123_secret_456&source_type=card"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, + createWebViewClient( "pi_123_secret_456", - "stripe://payment_intent_return"); + "stripe://payment_intent_return" + ); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); verify(mActivity).finish(); } @@ -49,9 +57,7 @@ public void shouldOverrideUrlLoading_withSetupIntent_shouldSetResult() { "&setup_intent_client_secret=seti_1234_secret_5678&source_type=card"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "seti_1234_secret_5678", - "stripe://payment_auth"); + createWebViewClient("seti_1234_secret_5678", "stripe://payment_auth"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); verify(mActivity).finish(); } @@ -61,8 +67,7 @@ public void shouldOverrideUrlLoading_withoutReturnUrl_onPaymentIntentImplicitRet final String deepLink = "stripe://payment_intent_return?payment_intent=pi_123&" + "payment_intent_client_secret=pi_123_secret_456&source_type=card"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); verify(mActivity).finish(); } @@ -72,8 +77,7 @@ public void shouldOverrideUrlLoading_withoutReturnUrl_onSetupIntentImplicitRetur final String deepLink = "stripe://payment_auth?setup_intent=seti_1234" + "&setup_intent_client_secret=seti_1234_secret_5678&source_type=card"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "seti_1234_secret_5678", null); + createWebViewClient("seti_1234_secret_5678"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); verify(mActivity).finish(); } @@ -81,18 +85,16 @@ public void shouldOverrideUrlLoading_withoutReturnUrl_onSetupIntentImplicitRetur @Test public void shouldOverrideUrlLoading_withoutReturnUrl_shouldNotAutoFinishActivity() { final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, "https://example.com"); verify(mActivity, never()).finish(); } @Test - public void shouldOverrideUrlLoading_witKnownReturnUrl_shouldFinish() { + public void shouldOverrideUclientSecretrlLoading_witKnownReturnUrl_shouldFinish() { final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, "stripejs://use_stripe_sdk/return_url"); verify(mActivity).finish(); @@ -101,8 +103,7 @@ public void shouldOverrideUrlLoading_witKnownReturnUrl_shouldFinish() { @Test public void onPageFinished_wit3DSecureCompleteUrl_shouldFinish() { final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.onPageFinished(mWebView, "https://hooks.stripe.com/3d_secure/complete/tdsrc_1ExLWoCRMbs6FrXfjPJRYtng"); verify(mActivity).finish(); @@ -111,8 +112,7 @@ public void onPageFinished_wit3DSecureCompleteUrl_shouldFinish() { @Test public void onPageFinished_witRedirectCompleteUrl_shouldFinish() { final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.onPageFinished(mWebView, "https://hooks.stripe.com/redirect/complete/src_1ExLWoCRMbs6FrXfjPJRYtng"); verify(mActivity).finish(); @@ -123,8 +123,7 @@ public void shouldOverrideUrlLoading_withOpaqueUri_shouldNotCrash() { final String deepLink = "mailto:patrick@example.com?payment_intent=pi_123&" + "payment_intent_client_secret=pi_123_secret_456&source_type=card"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); } @@ -132,9 +131,45 @@ public void shouldOverrideUrlLoading_withOpaqueUri_shouldNotCrash() { public void shouldOverrideUrlLoading_withUnsupportedDeeplink_shouldFinish() { final String deepLink = "deep://link"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = - new PaymentAuthWebView.PaymentAuthWebViewClient(mActivity, mProgressBar, - "pi_123_secret_456", null); + createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); verify(mActivity).finish(); } + + @Test + public void shouldOverrideUlLoading_withIntentUri_shouldParseUri() { + final String deepLink = + "intent://example.com/#Intent;scheme=https;action=android.intent.action.VIEW;end"; + final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = + createWebViewClient("pi_123_secret_456"); + paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, deepLink); + verify(mPackageManager).resolveActivity( + mIntentArgumentCaptor.capture(), + eq(PackageManager.MATCH_DEFAULT_ONLY) + ); + final Intent intent = mIntentArgumentCaptor.getValue(); + assertEquals("https://example.com/", intent.getDataString()); + verify(mActivity).finish(); + } + + @NonNull + private PaymentAuthWebView.PaymentAuthWebViewClient createWebViewClient( + @NonNull String clientSecret + ) { + return createWebViewClient(clientSecret, null); + } + + @NonNull + private PaymentAuthWebView.PaymentAuthWebViewClient createWebViewClient( + @NonNull String clientSecret, + @Nullable String returnUrl + ) { + return new PaymentAuthWebView.PaymentAuthWebViewClient( + mActivity, + mPackageManager, + mProgressBar, + clientSecret, + returnUrl + ); + } } From 168ca95bb9d9d85059a9347641f490ff56bd6be4 Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Thu, 12 Sep 2019 10:38:39 -0400 Subject: [PATCH 2/2] Respond to comments --- .../java/com/stripe/android/view/PaymentAuthWebViewTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java b/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java index b346e41c09f..a2d0ad2d314 100644 --- a/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java +++ b/stripe/src/test/java/com/stripe/android/view/PaymentAuthWebViewTest.java @@ -92,7 +92,7 @@ public void shouldOverrideUrlLoading_withoutReturnUrl_shouldNotAutoFinishActivit } @Test - public void shouldOverrideUclientSecretrlLoading_witKnownReturnUrl_shouldFinish() { + public void shouldOverrideUrlLoading_withKnownReturnUrl_shouldFinish() { final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient = createWebViewClient("pi_123_secret_456"); paymentAuthWebViewClient.shouldOverrideUrlLoading(mWebView, @@ -137,7 +137,7 @@ public void shouldOverrideUrlLoading_withUnsupportedDeeplink_shouldFinish() { } @Test - public void shouldOverrideUlLoading_withIntentUri_shouldParseUri() { + public void shouldOverrideUrlLoading_withIntentUri_shouldParseUri() { final String deepLink = "intent://example.com/#Intent;scheme=https;action=android.intent.action.VIEW;end"; final PaymentAuthWebView.PaymentAuthWebViewClient paymentAuthWebViewClient =