Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PopupBridge Android Release Notes

## unreleased

* Fix an issue where the WebViewClient is being silently overridden. [#95](https://github.com/braintree/popup-bridge-android/issues/95)

## 5.0.0

* Android 13 Support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,33 @@
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.braintreepayments.api.PopupBridgeClient;
import com.braintreepayments.api.PopupBridgeWebViewClient;

public class PopupActivity extends AppCompatActivity {

private static final String RETURN_URL_SCHEME = "com.braintreepayments.popupbridgeexample";

private WebView webView;
private PopupBridgeClient popupBridgeClient;
private PopupBridgeWebViewClient popupBridgeWebViewClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_popup);
webView = findViewById(R.id.web_view);

popupBridgeClient = new PopupBridgeClient(this, webView, RETURN_URL_SCHEME);
WebViewClient webViewClient = demoWebViewClient();

popupBridgeWebViewClient = new PopupBridgeWebViewClient(webViewClient);

popupBridgeClient = new PopupBridgeClient(this, webView, RETURN_URL_SCHEME, popupBridgeWebViewClient);
popupBridgeClient.setErrorListener(error -> showDialog(error.getMessage()));

webView.loadUrl(getIntent().getStringExtra("url"));
Expand All @@ -46,4 +54,20 @@ public void showDialog(String message) {
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}

private WebViewClient demoWebViewClient() {
return new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Toast.makeText(PopupActivity.this, "Page Finished", Toast.LENGTH_SHORT).show();
}

@Override
public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Toast.makeText(PopupActivity.this, "Page Started", Toast.LENGTH_SHORT).show();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
Expand All @@ -16,17 +15,17 @@ import com.braintreepayments.api.internal.AnalyticsParamRepository
import com.braintreepayments.api.internal.PendingRequestRepository
import com.braintreepayments.api.internal.PopupBridgeJavascriptInterface
import com.braintreepayments.api.internal.PopupBridgeJavascriptInterface.Companion.POPUP_BRIDGE_URL_HOST
import com.braintreepayments.api.internal.isVenmoInstalled
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.json.JSONException
import org.json.JSONObject
import java.lang.ref.WeakReference

class PopupBridgeClient @SuppressLint("SetJavaScriptEnabled") internal constructor(
activity: ComponentActivity,
webView: WebView,
private val returnUrlScheme: String,
private val popupBridgeWebViewClient: PopupBridgeWebViewClient,
private val browserSwitchClient: BrowserSwitchClient,
private val pendingRequestRepository: PendingRequestRepository = PendingRequestRepository(activity.applicationContext),
private val coroutineScope: CoroutineScope = activity.lifecycleScope,
Expand Down Expand Up @@ -64,16 +63,20 @@ class PopupBridgeClient @SuppressLint("SetJavaScriptEnabled") internal construct
* @param activity The [ComponentActivity] that contains the [WebView].
* @param webView The [WebView] to enable for PopupBridge.
* @param returnUrlScheme The return url scheme to use for deep linking back into the application.
* @param popupBridgeWebViewClient The [PopupBridgeWebViewClient] to use for handling web view events.
* @throws IllegalArgumentException If the activity is not valid or the fragment cannot be added.
*/
@JvmOverloads
constructor(
activity: ComponentActivity,
webView: WebView,
returnUrlScheme: String
returnUrlScheme: String,
popupBridgeWebViewClient: PopupBridgeWebViewClient = PopupBridgeWebViewClient()
) : this(
activity = activity,
webView = webView,
returnUrlScheme = returnUrlScheme,
popupBridgeWebViewClient = popupBridgeWebViewClient,
browserSwitchClient = BrowserSwitchClient()
)

Expand All @@ -88,12 +91,7 @@ class PopupBridgeClient @SuppressLint("SetJavaScriptEnabled") internal construct

webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(popupBridgeJavascriptInterface, POPUP_BRIDGE_NAME)
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
setVenmoInstalled(activity.isVenmoInstalled())
}
}
webView.webViewClient = popupBridgeWebViewClient

with(popupBridgeJavascriptInterface) {
onOpen = { url -> openUrl(url) }
Expand Down Expand Up @@ -222,23 +220,6 @@ class PopupBridgeClient @SuppressLint("SetJavaScriptEnabled") internal construct
)
}

private fun setVenmoInstalled(isVenmoInstalled: Boolean) {
runJavaScriptInWebView(
""
+ "function setVenmoInstalled() {"
+ " window.popupBridge.isVenmoInstalled = ${isVenmoInstalled};"
+ "}"
+ ""
+ "if (document.readyState === 'complete') {"
+ " setVenmoInstalled();"
+ "} else {"
+ " window.addEventListener('load', function () {"
+ " setVenmoInstalled();"
+ " });"
+ "}"
)
}

private fun runJavaScriptInWebView(script: String) {
webViewRef.get()?.post(
Runnable { webViewRef.get()?.evaluateJavascript(script, null) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.braintreepayments.api

import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.os.Message
import android.view.KeyEvent
import android.webkit.ClientCertRequest
import android.webkit.HttpAuthHandler
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import com.braintreepayments.api.internal.isVenmoInstalled

class PopupBridgeWebViewClient(
private val delegate: WebViewClient? = null
) : WebViewClient() {

override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
setVenmoInstalled(view, view?.context?.isVenmoInstalled() == true)
delegate?.onPageFinished(view, url)
}

@Deprecated("Deprecated in [android.webkit.WebViewClient]")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return delegate?.shouldOverrideUrlLoading(view, url) ?: super.shouldOverrideUrlLoading(view, url)
}

@RequiresApi(Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return delegate?.shouldOverrideUrlLoading(view, request) ?: super.shouldOverrideUrlLoading(view, request)
}

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
delegate?.onPageStarted(view, url, favicon) ?: super.onPageStarted(view, url, favicon)
}

override fun onLoadResource(view: WebView?, url: String?) {
delegate?.onLoadResource(view, url) ?: super.onLoadResource(view, url)
}

override fun onPageCommitVisible(view: WebView?, url: String?) {
delegate?.onPageCommitVisible(view, url) ?: super.onPageCommitVisible(view, url)
}

override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: android.webkit.WebResourceError?
) {
delegate?.onReceivedError(view, request, error) ?: super.onReceivedError(view, request, error)
}

override fun onReceivedHttpError(
view: WebView?,
request: WebResourceRequest?,
errorResponse: WebResourceResponse?
) {
delegate?.onReceivedHttpError(view, request, errorResponse)
?: super.onReceivedHttpError(view, request, errorResponse)
}

override fun onFormResubmission(view: WebView?, dontResend: Message?, resend: Message?) {
delegate?.onFormResubmission(view, dontResend, resend) ?: super.onFormResubmission(view, dontResend, resend)
}

override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
delegate?.doUpdateVisitedHistory(view, url, isReload) ?: super.doUpdateVisitedHistory(view, url, isReload)
}

override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
delegate?.onReceivedSslError(view, handler, error) ?: super.onReceivedSslError(view, handler, error)
}

override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest?) {
delegate?.onReceivedClientCertRequest(view, request) ?: super.onReceivedClientCertRequest(view, request)
}

override fun onReceivedHttpAuthRequest(view: WebView?, handler: HttpAuthHandler?, host: String?, realm: String?) {
delegate?.onReceivedHttpAuthRequest(view, handler, host, realm)
?: super.onReceivedHttpAuthRequest(view, handler, host, realm)
}

@Deprecated("Deprecated in [android.webkit.WebViewClient]")
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
return delegate?.shouldInterceptRequest(view, url) ?: super.shouldInterceptRequest(view, url)
}

override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
return delegate?.shouldInterceptRequest(view, request) ?: super.shouldInterceptRequest(view, request)
}

override fun onUnhandledKeyEvent(view: WebView?, event: KeyEvent?) {
delegate?.onUnhandledKeyEvent(view, event) ?: super.onUnhandledKeyEvent(view, event)
}

override fun onScaleChanged(view: WebView?, oldScale: Float, newScale: Float) {
delegate?.onScaleChanged(view, oldScale, newScale) ?: super.onScaleChanged(view, oldScale, newScale)
}

override fun onReceivedLoginRequest(view: WebView?, realm: String?, account: String?, args: String?) {
delegate?.onReceivedLoginRequest(view, realm, account, args)
?: super.onReceivedLoginRequest(view, realm, account, args)
}

private fun setVenmoInstalled(view: WebView?, isVenmoInstalled: Boolean) {
runJavaScriptInWebView(view,
""
+ "function setVenmoInstalled() {"
+ " window.popupBridge.isVenmoInstalled = ${isVenmoInstalled};"
+ "}"
+ ""
+ "if (document.readyState === 'complete') {"
+ " setVenmoInstalled();"
+ "} else {"
+ " window.addEventListener('load', function () {"
+ " setVenmoInstalled();"
+ " });"
+ "}"
)
}

private fun runJavaScriptInWebView(webView: WebView?, script: String) {
webView?.post(
Runnable { webView.evaluateJavascript(script, null) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,12 @@ import com.braintreepayments.api.internal.AnalyticsClient
import com.braintreepayments.api.internal.AnalyticsParamRepository
import com.braintreepayments.api.internal.PendingRequestRepository
import com.braintreepayments.api.internal.PopupBridgeJavascriptInterface
import com.braintreepayments.api.internal.isVenmoInstalled
import com.braintreepayments.api.util.CoroutineTestRule
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.spyk
import io.mockk.unmockkAll
import io.mockk.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
Expand All @@ -46,6 +42,7 @@ class PopupBridgeClientUnitTest {
private val activityMock: ComponentActivity = mockk(relaxed = true)
private val webViewMock: WebView = mockk(relaxed = true)
private val browserSwitchClient: BrowserSwitchClient = mockk(relaxed = true)
private val popupBridgeWebViewClient: PopupBridgeWebViewClient = mockk(relaxed = true)
private val pendingRequestRepository: PendingRequestRepository = mockk(relaxed = true)
private val analyticsClient: AnalyticsClient = mockk(relaxed = true)
private val popupBridgeJavascriptInterface: PopupBridgeJavascriptInterface = mockk(relaxed = true)
Expand Down Expand Up @@ -77,6 +74,7 @@ class PopupBridgeClientUnitTest {
activity = activity,
webView = webView,
returnUrlScheme = returnUrlScheme,
popupBridgeWebViewClient = popupBridgeWebViewClient,
browserSwitchClient = browserSwitchClient,
pendingRequestRepository = pendingRequestRepository,
coroutineScope = TestScope(testDispatcher),
Expand Down Expand Up @@ -319,48 +317,6 @@ class PopupBridgeClientUnitTest {
verify { analyticsClient.sendEvent(POPUP_BRIDGE_CANCELED) }
}

@Test
fun `on init, when venmo installed, isVenmoInstalled is set to true on the popupBridgeJavascriptInterface`() = runTest {
val webView = spyk<WebView>(WebView(activityMock))

initializeClient(webView = webView) {
mockkStatic("com.braintreepayments.api.internal.AppInstalledChecksKt")
every { activityMock.isVenmoInstalled() } returns true
}

webView.webViewClient.onPageFinished(webView, "https://example.com")
runnableSlot.captured.run()

verify {
webView.evaluateJavascript(withArg { javascriptString ->
assertEquals(getExpectedVenmoInstalledJavascript(true), javascriptString)
}, null)
}

unmockkAll()
}

@Test
fun `on init, when venmo is not installed, isVenmoInstalled is set to false on the popupBridgeJavascriptInterface`() = runTest {
val webView = spyk<WebView>(WebView(activityMock))

initializeClient(webView = webView) {
mockkStatic("com.braintreepayments.api.internal.AppInstalledChecksKt")
every { activityMock.isVenmoInstalled() } returns false
}

webView.webViewClient.onPageFinished(webView, "https://example.com")
runnableSlot.captured.run()

verify {
webView.evaluateJavascript(withArg { javascriptString ->
assertEquals(getExpectedVenmoInstalledJavascript(false), javascriptString)
}, null)
}

unmockkAll()
}

@Test
fun `when popupBridgeJavascriptInterface onOpen is called, analyticsClient sends POPUP_BRIDGE_STARTED event`() {
initializeClient()
Expand Down Expand Up @@ -483,23 +439,6 @@ class PopupBridgeClientUnitTest {
)
}

private fun getExpectedVenmoInstalledJavascript(isVenmoInstalled: Boolean): String {
return String.format(
(""
+ "function setVenmoInstalled() {"
+ " window.popupBridge.isVenmoInstalled = %s;"
+ "}"
+ ""
+ "if (document.readyState === 'complete') {"
+ " setVenmoInstalled();"
+ "} else {"
+ " window.addEventListener('load', function () {"
+ " setVenmoInstalled();"
+ " });"
+ "}"), isVenmoInstalled
)
}

companion object {
private const val CANCELED_JAVASCRIPT = (""
+ "function notifyCanceled() {"
Expand All @@ -519,4 +458,3 @@ class PopupBridgeClientUnitTest {
+ "}")
}
}

Loading