Skip to content

Commit

Permalink
Support "intent://" URIs in payment auth WebView
Browse files Browse the repository at this point in the history
`intent://` URIs need to first be parsed into `Intent`
objects, then handled.
  • Loading branch information
mshafrir-stripe committed Sep 12, 2019
1 parent a24ee43 commit 1681325
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 46 deletions.
4 changes: 2 additions & 2 deletions example/src/main/java/com/stripe/example/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ internal object Settings {
* [example-ios-backend](https://github.com/stripe/example-ios-backend),
* the URL will be something like `https://hidden-beach-12345.herokuapp.com/`.
*/
const val BASE_URL = "put your base url here"
const val BASE_URL = "https://mshafrir-mobile-backend.herokuapp.com/"

/**
* Set to publishable key from https://dashboard.stripe.com/test/apikeys
*/
const val PUBLISHABLE_KEY = "pk_test_your_key_goes_here"
const val PUBLISHABLE_KEY = "pk_test_brpLutVVvqMRjOedg8SUJr9O"

/**
* Optionally, set to a Connect Account id to use for API requests to test Connect
Expand Down
47 changes: 29 additions & 18 deletions stripe/src/main/java/com/stripe/android/view/PaymentAuthWebView.kt
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 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();
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 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
);
}
}

0 comments on commit 1681325

Please sign in to comment.