Search code examples
flutterpaypalflutter-webdart-js-interop

Using HtmlElementView With PayPal


I have a Flutter Web project. I have the following HTML script that I need to put into HtmlElementView:

<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID"></script>
<script>paypal.Buttons().render('body');</script>

from here. I have tried this:

Widget build(BuildContext context) {
    // ignore: undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(
      'paypal-button',
        (int viewId) => IFrameElement()
        ..width = '500'
        ..height = '500'
        ..src = '''
        <div id="paypal-button-container"></div>
        <script src="https://paypal.com/sdk/js?client-id=MY_CLIENT_ID"></script>
        <script>
        paypal.Buttons({

            // Set up the transaction
            createOrder: function(data, actions) {
                return actions.order.create({
                    purchase_units: [{
                        amount: {
                            value: '0.01'
                        }
                    }]
                });
            },

            // Finalize the transaction
            onApprove: function(data, actions) {
                return actions.order.capture().then(function(details) {
                    // Show a success message to the buyer
                    alert('Transaction completed by ' + details.payer.name.given_name + '!');
                });
            }


        }).render('#paypal-button-container');
    </script>
        '''
    );
    return SizedBox(
      height: 300,
      width: 500,
      child: Center(
        child: HtmlElementView(
          viewType: 'paypal-button',
        ),
      ),
    );
  }

Note: This code is used from https://developer.paypal.com/demo/checkout/#/pattern/client

This code doesn't show any buttons and gives the following errors:

Bad state: Future already completed

Resource requests whose URLs contained both removed whitespace (\n, \r, \t) characters and less-than characters (<) are blocked. Please remove newlines and encode less-than characters from places like element attribute values in order to load these resources. See https://www.chromestatus.com/feature/5735596811091968 for more details.

What I need to know is how can I display this code snippet as a Widget in order for my Flutter Web project to be able to accept Paypal payments. Thanks for any help!


Solution

  • Currently, the only way to integrate PayPal buttons into flutter web is to wrap them in an IFrame container:

    class PayPalWidget extends StatefulWidget {
      @override
      _PayPalState createState() => _PayPalState();
    }
    
    class _PayPalState extends State<PayPalWidget> {
      html.IFrameElement _element;
    
      @override
      void initState() {
        _element = html.IFrameElement()
          ..width = "200px"
          ..height = "200px"
          ..style.border = 'none'
          ..srcdoc = """
            <!DOCTYPE html>
            <html>
              <body>
                <script src="https://www.paypal.com/sdk/js?client-id=sb"></script>
                <script>
                  paypal.Buttons(
                    {
                      createOrder: function(data, actions) {
                        return actions.order.create({
                          purchase_units: parent.purchase_units
                        });
                      },
                      onApprove: function(data, actions) {
                        return actions.order.capture().then(function(details) {
                          parent.flutter_feedback('Transaction completed by ' + details.payer.name.given_name);
                        });
                      }
                    }
    
                  ).render('body');
                </script>
              </body>
            </html>
            """;
    
        js.context["purchase_units"] = js.JsObject.jsify([
          {
            'amount': {'value': '0.02'}
          }
        ]);
        js.context["flutter_feedback"] = (msg) {
          Scaffold.of(context).showSnackBar(SnackBar(content: Text(msg)));
        };
    
        // ignore:undefined_prefixed_name
        ui.platformViewRegistry.registerViewFactory(
          'PayPalButtons',
          (int viewId) => _element,
        );
    
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
          width: 220,
          height: 220,
          child: HtmlElementView(viewType: 'PayPalButtons'),
        );
      }
    }
    

    But keep in mind that this method is far from ideal, since the IFrame is re-created every time the widget is updated. I added code to demonstrate this downside effect:

    import 'dart:ui' as ui;
    import 'dart:js' as js;
    import 'package:universal_html/html.dart' as html;
    import 'package:flutter/material.dart';
    
    class NextLabPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              leading: BackButton(),
              title: Text('PayPal integration'),
              centerTitle: true,
            ),
            body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Noise(),
                    PayPalWidget(),
                  ]),
            ),
          ),
        );
      }
    }
    
    class Noise extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: RaisedButton(
            child: Text('Make Noise'),
            onPressed: () {
              Scaffold.of(context).showSnackBar(
                SnackBar(
                  content:
                      Text("PayPal buttons will be re-created when I disappear"),
                ),
              );
            },
          ),
        );
      }
    }
    
    // PayPalWidget code
    // ...
    
    

    Push 'Make sound' button to show snackbar - when message disappears, the buttons disappear too. Or hover your mouse over the "back arrow" in the AppBar and move away to show the same effect.