braintree / popup-bridge-android

PopupBridge allows WebViews to open popup windows in a browser and send data back to the WebView
MIT License
53 stars 28 forks source link

Paypal smart buttons callbacks are not called if popupbridge is used outside an activity #24

Closed molundb closed 4 years ago

molundb commented 4 years ago

General information

Issue description

First of all thank you so much for this wonderful plugin.

What I am trying to achieve is this: I want put the handling of the webView used for the popupBridge in a separate class (PaypalPopup.kt), then I want to simulate a click of the paypal pay button on the button click of another button (in MainActivity.kt) (solution 1). This is for a smooth user experience. I have a working solution (solution 2) using a separate activity but that causes a second screen with the paypal button to show after the initial button click.

Everything seems to be working fine with solution 1, but for some reason the paypal callback onApprove is not called after this line in PopupBridge.java is called. runJavaScriptInWebView(String.format("window.popupBridge.onComplete(%s, %s);", error, payload));. I've been stuck on this issue for days and I'm clueless. Why is onApprove not getting called in this scenario?

Thank you once again.

Code:

MainActivity.kt

package com.example.popupbridgeexample

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import butterknife.OnClick
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    lateinit var service: PaypalPopup

    private val useActivity = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (!useActivity) {
            service = PaypalPopup(this as AppCompatActivity)
        }

        payButton.setOnClickListener {
            payClicked()
        }
    }

    fun payClicked() {
        if (useActivity) {
            val paypalPopupActivity = Intent(this, PaypalPopupActivity::class.java)
            startActivity(paypalPopupActivity)
        } else {
            service.pay()
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/payButton"
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:text="Paypal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

PaypalPopupActivity.kt (this is working)

package com.example.popupbridgeexample

import android.os.Bundle
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import com.braintreepayments.popupbridge.PopupBridge

class PaypalPopupActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.fragment_paypal_web_view)
        val mWebView: WebView = findViewById(R.id.web_view)

        PopupBridge.newInstance(this, mWebView)

        WebView.setWebContentsDebuggingEnabled(true)

        val popupHelper = PaypalPopupHelper(this, mWebView)
        popupHelper.setupWebView()
    }
}

fragment_paypal_web_view.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/cart_item_ready_to_buy"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:breakStrategy="simple"
        android:text="lorem Ipsum dolarius ameno lerum"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/cart_item_ready_to_buy"
        tools:layout_editor_absoluteX="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

PaypalPopup.kt (this is not working)

package com.example.popupbridgeexample

import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import com.braintreepayments.popupbridge.PopupBridge

class PaypalPopup(activity: AppCompatActivity) {
    private val mWebView = WebView(activity)

    init {
        PopupBridge.newInstance(activity, mWebView)

        WebView.setWebContentsDebuggingEnabled(true)

        val popupHelper = PaypalPopupHelper(activity, mWebView)
        popupHelper.setupWebView()
    }

    fun pay() {
        mWebView.evaluateJavascript(
            "javascript: " +
                    "pay()", null
        )
    }
}

PaypalPopupHelper.kt

package com.example.popupbridgeexample

import android.content.Context
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast

open class PaypalPopupHelper(val ctx: Context, val mWebView: WebView) {

    fun load() {
        val clientId = // paypalSandboxId
        mWebView.evaluateJavascript(
            "javascript: " +
                    "load(\"" + clientId + "\")", null
        )
    }

    fun setupWebView() {
        mWebView.settings?.javaScriptEnabled = true
        mWebView.settings?.javaScriptCanOpenWindowsAutomatically = true
        mWebView.settings?.allowUniversalAccessFromFileURLs = true
        mWebView.addJavascriptInterface(JavaScriptInterface(), JAVASCRIPT_OBJ)

        mWebView.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {
                load()
            }
        }

        mWebView.loadUrl(PAYPAL_SDK)
    }

    companion object {
        private val PAYPAL_SDK = "file:///android_asset/paypal.html"
        private val JAVASCRIPT_OBJ = "JAVASCRIPT_OBJ"
    }

    private inner class JavaScriptInterface {
        @JavascriptInterface
        fun onApprove(fromWeb: String) {
            Toast.makeText(ctx, "Approved!", Toast.LENGTH_LONG).show()
        }

        @JavascriptInterface
        fun onCancel(fromWeb: String) {
            Toast.makeText(ctx, fromWeb, Toast.LENGTH_LONG).show()
        }

        @JavascriptInterface
        fun onError(fromWeb: String) {
            Toast.makeText(ctx, fromWeb, Toast.LENGTH_LONG).show()
        }
    }
}

paypal.html

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>PayPal</title>
    <script language="JavaScript" type="text/javascript" src="paypalJavascript.js"></script>
</head>
<body>
<div id="paypal-button-container"></div>
</body>
</html>

paypalJavascript.js

var waitTillLoaded;
var paypalOrderId;

function load(clientId) {
    console.log("load");
    var script = document.createElement('script');
    script.setAttribute('src', 'https://www.paypal.com/sdk/js?client-id=' + clientId + '&disable-funding=credit,card,sepa,giropay,sofort&intent=authorize&currency=EUR');
    document.head.appendChild(script);
    script.onload = render;
}

function render() {
    console.log("render");
    paypal.Buttons({
        createOrder: function(data, actions) {
            console.log("createOrder")
            return "8XG90531F0790483Y"
        },
        onApprove: (data, actions) => {
            // OBS! This is the problem. This doesn't get called when use useActivity
            // is set to true in MainActivity.kt
            console.log("onApprove") 
            console.log(data)
            return onApprove(data)
        },
        onCancel: () => onCancel,
        onError: err => {
            console.log("error")
            onError(err)
        }
    }).render('#paypal-button-container');
}

function onApprove(data) {
    console.log("onApprove");
    console.log("data: " + data);
    JAVASCRIPT_OBJ.onApprove("onApprove");
}

function onCancel() {
    console.log("onCancel");
    JAVASCRIPT_OBJ.onCancel("session canceled");
}

function onError(error) {
    console.log("onError");
    JAVASCRIPT_OBJ.onError(error);
}

function pay() {
    console.log("pay")
    waitTillLoaded = setInterval(function() {
        if (isPayPalLoaded()) {
            clearInterval(waitTillLoaded)
            var button = document.getElementById('paypal-button-container').childNodes[0].childNodes[1].contentWindow.document.getElementsByClassName('paypal-button')[0]
            button.click()
        }
    }, 500)
}

function isPayPalLoaded() {
    try {
        return document.getElementById('paypal-button-container').childNodes[0].childNodes[1].contentWindow.document.getElementsByClassName('paypal-button')[0] != null
    } catch(err) {
        return false
    }
}
sshropshire commented 4 years ago

Hi @molundb looking into this. I noticed in solution 1 the WebView is programmatically instantiated. Where does it get added to the view hierarchy in this case?

Also if you could provide screenshots and/or logs from a web inspector console if there are any, that would help us diagnose the issue.

molundb commented 4 years ago

@sshropshire Thank you. It does not get added to the view hierarchy I suppose; that's the idea to make it not show up and instead just show the paypal webview.

I'll look into the web inspector console thing and get back to you.

molundb commented 4 years ago

@sshropshire Here are gifs and web console logs of the two different solutions. I hope it helps, and if you need any further information I will happily provide it.

Solution 1, without activity (not working)

Gif. Notice that there's no popup saying "Approved!" as in solution 2. popup_bridge_not_working

Logs. The problem compared to the working solution 2 seems to be that the Paypal.buttons onApprove() callback doesn't get called. popup_bridge_not_working_web_console_log

Solution 2, with activity (working)

Gif popup_bridge_activity_working

Logs popup_bridge_activity_working_web_console_log

molundb commented 4 years ago

I solved it by adding the webview to the fragment_main.xml with visibility="gone" and passing it to PaypalPopup.kt. I guess it needs to be in the view hierarchy. Thanks a lot for the help!