Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "intent://" URIs in payment auth WebView #1527

Merged
merged 2 commits into from
Sep 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ jobs:
script:
- ./gradlew clean ktlint
- ./gradlew checkstyle lintRelease
- ./gradlew testRelease
- ./gradlew testRelease -i
mshafrir-stripe marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
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 {

@Mock private Activity mActivity;
@Mock private ProgressBar mProgressBar;
@Mock private WebView mWebView;
@Mock private PackageManager mPackageManager;

@Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor;

@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(mActivity.getPackageManager())
.thenReturn(ApplicationProvider.getApplicationContext().getPackageManager());
}

@Test
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();
}
Expand All @@ -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();
}
Expand All @@ -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();
}
Expand All @@ -72,27 +77,24 @@ 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();
}

@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 shouldOverrideUrlLoading_withKnownReturnUrl_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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -123,18 +123,53 @@ 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);
}

@Test
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 shouldOverrideUrlLoading_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
);
}
}