According to the documentation, I should be able to use paypal.Button.react as a React component, but doing so produces this error:
Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of...
After a quick poke around, I found that paypal.Button.react is undefined. Is there any way to use the React component currently?
Right now, the way checkout.js registers with react is to look for window.React. So window.React needs to be defined before checkout.js is loaded. I'm guessing you're bundling react as a dependency or something so the global isn't present?
I'll keep this open for now though -- need to work on a way of allowing react to be registered manually... something like paypal.register('react', window.React);
Yeah, that sounds like the problem. I'm using NPM / Webpack for React, which probably wouldn't define window.React. Thanks!
How did you solved this?
I cant make paypal button to render :(
Can you help me?
Unless you're using React via a <script> tag, you probably won't get paypal.Button.react working. At least until they add some means to inject the React object. You'll have to roll your own for now. StackOverflow is your friend.
I will be attentive to any updates.
Thank you
@ezzatron / @vagonzalez -- you shouldn't have to roll your own. Is it possible for you to just do window.React = React; before checkout.js loads? Then checkout.js will register the react component using the global object.
I have done a workaround...
Using react-async-script-loader (https://github.com/leozdgao/react-async-script-loader), I can register window.React and window.ReactDOM before checkout.js loads...
Basically:
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { connect } from 'react-redux'
import scriptLoader from 'react-async-script-loader'
import { paymentAuthorized, paymentCancelled } from '../../actions'
class PayPalButton extends Component {
constructor(props) {
super(props)
window.React = React
window.ReactDOM = ReactDOM
this.state = {
showButton: false,
}
}
componentWillReceiveProps ({ isScriptLoaded, isScriptLoadSucceed }) {
if (!this.state.show) {
if (isScriptLoaded && !this.props.isScriptLoaded) { // load finished
if (isScriptLoadSucceed) {
this.setState({ showButton: true })
console.log('alehop!!', window.paypal.Button.react)
}
else this.props.onError()
}
}
}
componentDidMount () {
const { isScriptLoaded, isScriptLoadSucceed } = this.props
if (isScriptLoaded && isScriptLoadSucceed) {
this.setState({ showButton: true })
}
}
componentWillUnmount() {
delete window.React
delete window.ReactDOM
}
render() {
const client = {
sandbox: 'xxxxxx',
production: 'xxxxxx',
}
const payment = () => {
return paypal.rest.payment.create('sandbox', client,
{
transactions: [
{ amount: { total: '1.00', currency: 'USD' } },
],
},
)
}
const onAuthorize = (data, actions) => {
return actions.payment.execute().then(() => {
console.log('The payment was completed!', this)
this.setState({ showButton: false })
const payment = Object.assign({}, this.props.payment)
payment.paid = true
payment.cancelled = false
payment.payerID = data.payerID
payment.paymentID = data.paymentID
payment.paymentToken = data.paymentToken
payment.returnUrl = data.returnUrl
this.props.dispatch(paymentAuthorized(payment))
})
}
const onCancel = (data) => {
console.log('The payment was cancelled!', data)
this.props.dispatch(paymentCancelled())
}
return (
<If condition={this.state.showButton}>
<paypal.Button.react
env={'sandbox'}
client={client}
payment={payment}
commit={true}
onAuthorize={onAuthorize}
onCancel={onCancel}
/>
</If>
)
}
}
const selector = (appState) => ({
payment: appState.payment,
})
const PayPalButtonRedux = connect(selector)(PayPalButton)
export default scriptLoader('https://www.paypalobjects.com/api/checkout.js')(PayPalButtonRedux)
What do you think of this solution?
@bluepnume
Is it possible for you to just do window.React = React; before checkout.js loads?
I wonder how, though. I have two script tags: my app webpack-bundled, and checkout.js. I believe they just load concurrently. Can I use checkout.js as an npm dependency?
Hey @dminkovsky @vagonzalez @ezzatron -- I fixed this. You can now do:
let ReactButton = paypal.Button.driver('react', {
React: window.React,
ReactDOM: window.ReactDOM
});
Wah, whoops, thought this issue was the one on xcomponent. It's fixed but I still need to push out checkout.js. I'll keep the issue open until then,
thank you @vagonzalez for that workaround, you saved my day! 馃憤
Pushed with support for:
let ReactButton = paypal.Button.driver('react', {
React: window.React,
ReactDOM: window.ReactDOM
});
So example for anyone using React with webpack would be:
import React from 'react';
import ReactDOM from 'react-dom';
import paypal from 'paypal-checkout';
//.....
const ReactButton = paypal.Button.driver('react', {React, ReactDOM});
I believe there should be example like this somewhere in DOCS. Hovewer after refreshing page I get this error:
bundle.js:56523 Uncaught TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at bundle.js:56523
at trycatch (bundle.js:59437)
at new SyncPromise (bundle.js:59508)
at ajax (bundle.js:56509)
at immediateFlush (bundle.js:56228)
at bundle.js:56060
at r._wrapped (review:21)
@Tomekmularczyk can you share an example of your code?
@bluepnume sure. I have modified a little example from the docs:
import React from 'react';
import ReactDOM from 'react-dom';
import paypal from 'paypal-checkout';
const PayPalButton = paypal.Button.driver('react', {React, ReactDOM});
export default class PayPalCheckout extends React.Component {
onAuthorize(data, actions) {
// Optional: display a confirmation page here
return actions.payment.execute().then(function () {
// Show a success page to the buyer
});
}
payment() {
const env = this.props.env;
const client = this.props.client;
return paypal.rest.payment.create(env, client, {
transactions: [
{
amount: {total: '1.00', currency: 'USD'}
}
]
});
}
render() {
const client = {
sandbox: 'AWi18rxt26-hrueMoPZ0tpGEOJnNT4QkiMQst9pYgaQNAfS1FLFxkxQuiaqRBj1vV5PmgHX_jA_c1ncL',
production: 'AVZhosFzrnZ5Mf3tiOxAD0M6NHv8pcB2IFNHAfp_h69mmbd-LElFYkJUSII3Y0FPbm7S7lxBuqWImLbl'
};
return (
<PayPalButton env={'sandbox'}
client={client}
payment={this.payment}
commit={true} // Optional: show a 'Pay Now' button in the checkout flow
onAuthorize={this.onAuthorize}/>
);
}
}
Could it be that I'm not using own client keys yet?
Found the issue. I'll roll a fix out for this tomorrow. Thanks for highlighting this!
@Tomekmularczyk I'm struggling through with the same library and have gotten it to work _most_ of the time.
One thing I needed to fix was proper binding of this.props.env and this.props.client, if you use big arrow syntax it wouldn't work. I suspect you may need to assign payment to a function (){...} syntax and then use those props in there.
I'm still trying to get my thing to work using the React binding by using the tips in this issue, so I'll follow up.
@jehartzog -- if you can post any examples of code snippets which don't work for you, I can try to identify why, and see if there's any fix that needs to be made on our side.
@bluepnume I think that's the fastest response I've ever seen on a GH issue :). Sure thing, I was able to get the payment processing working using a thrown together non-react method, but was getting errors in ~5% of checkouts on client side that I haven't been able to fix.
Please don't judge, I actually had a new production application fall through and had to re-write a payment solution while customers were waiting, so I was going very fast and dirty with this.
Here is what the mostly working code looked like:
// TODO Need to call this on mount AND update!!!
componentDidMount() {
scriptjs('https://www.paypalobjects.com/api/checkout.js', () => {
const items = [];
let total = 0;
let couponSavings = 0;
.. code I removed to build the variables above ...
}
const cart = this.props.cart;
const env = Meteor.isProduction ? 'production' : 'sandbox';
window.paypal.Button.render({
env,
client: {
sandbox: Meteor.settings.public.paypal.sandbox.client_id,
production: Meteor.settings.public.paypal.production.client_id,
},
commit: true, // Show a 'Pay Now' button
payment: function createPayment() {
logger.info(`${Meteor.user().profile.name} started the checkout process`, cart);
const paymentId = window.paypal.rest.payment.create(this.props.env, this.props.client, {
transactions: [{
item_list: {
items,
},
amount: {
currency: 'USD',
total,
},
description: 'Purchase the listed reviews.',
}],
});
paymentId.then((res) => {
logger.info(`${Meteor.user().profile.name} received a paymentId ${res}`);
}).catch((err) => {
logger.error(`${Meteor.user().profile.name} received checkout error ${err}`, err);
});
return paymentId;
},
onAuthorize: (data, actions) => actions.payment.execute().then((payment) => {
paymentCompleted.call({
cartId: this.props.cart._id,
state: payment.state,
id: payment.id,
}, (err, res) => {
if (err) {
Alerts.errorWithIcon(`Error ${err}`);
} else {
Alerts.successWithIcon('Purchase complete');
logger.debug(res);
}
});
}).catch((err) => {
logger.error(`${Meteor.user().profile.name} had paypal authorization error`, err);
}),
}, '#paypal-button');
});
}
render() {
return (
<div id="paypal-button" />
);
}
}
Here is the stack trace I am getting in some cases, this only shows up client side and I was able to catch it finally while debugging:
Error: Expected reject to be called with Error, got [object XMLHttpRequest]
at SyncPromise.reject (https://www.paypalobjects.com/api/checkout.4.0.69.js:3626:48)
at https://www.paypalobjects.com/api/checkout.4.0.69.js:3605:33
at flush (https://www.paypalobjects.com/api/checkout.4.0.69.js:3525:41)
at https://www.paypalobjects.com/api/checkout.4.0.69.js:3538:21
at XMLHttpRequest.<anonymous> (https://www.paypalobjects.com/api/checkout.4.0.69.js:10855:68)
at Object._RECEIVE_MESSAGE_TYPE.(anonymous function) [as postrobot_message_response] (http://localhost:3000/packages/modules.js?hash=e53a62aba984a1646a9faae6f43db09916e95610:117309:122)
at receiveMessage (http://localhost:3000/packages/modules.js?hash=e53a62aba984a1646a9faae6f43db09916e95610:117217:77)
at messageListener (http://localhost:3000/packages/modules.js?hash=e53a62aba984a1646a9faae6f43db09916e95610:117237:17)
at SyncPromise.reject (https://www.paypalobjects.com/api/checkout.4.0.69.js:3626:48)
at https://www.paypalobjects.com/api/checkout.4.0.69.js:3605:33
at flush (https://www.paypalobjects.com/api/checkout.4.0.69.js:3525:41)
at https://www.paypalobjects.com/api/checkout.4.0.69.js:3538:21
at XMLHttpRequest.<anonymous> (https://www.paypalobjects.com/api/checkout.4.0.69.js:10855:68)
Here is the error object in the console:
https://www.dropbox.com/s/h2xbi97ke63k8z4/Screenshot%202017-05-11%2014.22.53.png?dl=0
I'm currently cleaning up my implementation by using the React binding as described in the issue but so far am still receiving that same error, which limits my ability to dive into why it's happening :).
Error: Expected reject to be called with Error, got [object XMLHttpRequest]
This is admittedly a terrible error message -- thanks for reminding me about this, I'll get a better one in there shortly.
In the mean time, you should see a failed http request in your network log to the paypal REST api. The message in the response should give you a better clue of what's going wrong here.
Thanks, that helped a lot! Looking at the http request let me know that my request was invalid due to some incorrect scoping while building the cart items in my new code, so I was sending an invalid request.
It would be very helpful to be able to get the content of that 400 response without needing dev-tools, that way I can pass back any errors that occur while clients are checking out to my server for me to analyze and correct. Is there an easy way to do this without patching this library?
It's going to need a patch but I'm gonna try to get to that today. It's been bugging me for a while.
@Tomekmularczyk I got the React binding working with @bluepnume awesome assistance, here is my working code:
renderButton() {
const items = this.buildCartItems();
let total = this.getCartTotal();
let couponSavings = this.getCouponSavings();
const opts = {
env: Meteor.isProduction ? 'production' : 'sandbox',
client: {
sandbox: Meteor.settings.public.paypal.sandbox.client_id,
production: Meteor.settings.public.paypal.production.client_id,
},
commit: true, // Show a 'Pay Now' button
payment: function createPayment() {
logger.info(`${Meteor.user().profile.name} started the checkout process`, this.cart);
// eslint-disable-next-line react/prop-types
const paymentId = paypal.rest.payment.create(this.props.env, this.props.client, {
transactions: [{
item_list: {
items,
},
amount: {
currency: 'USD',
total,
},
description: 'Purchase the listed reviews.',
}],
});
paymentId.then((res) => {
logger.info(`${Meteor.user().profile.name} received a paymentId ${res}`);
}).catch((err) => {
logger.error(`${Meteor.user().profile.name} received checkout error ${err}`, { err, items, total });
});
return paymentId;
},
onAuthorize: (data, actions) => actions.payment.execute().then((payment) => {
paymentCompleted.call({
cartId: this.props.cart._id,
state: payment.state,
id: payment.id,
}, (err, res) => {
if (err) {
Alerts.errorWithIcon(`Error ${err}`);
} else {
Alerts.successWithIcon('Purchase complete');
logger.debug(res);
}
});
}).catch((err) => {
logger.error(`${Meteor.user().profile.name} had paypal authorization error`, { err, items, total });
}),
};
// Odd way to do this, based on https://github.com/paypal/paypal-checkout/issues/149
const ReactButton = paypal.Button.driver('react', { React, ReactDOM });
return (
<ReactButton
env={opts.env}
client={opts.client}
payment={opts.payment}
commit={opts.commit}
onAuthorize={opts.onAuthorize}
/>
);
}
@jehartzog just pushed out a fix to give better error messages from REST api failures. Should be able to clean your cache and make sure paypal.version >= '4.0.70' and you're good to go.
Edit: Moved this to issue #334.
Was really hard to find this issue!
I would say most developers who use react for web apps use webpack and WILL encounter this problem - I would expect this variation to be documented here: https://github.com/paypal/paypal-checkout/blob/master/docs/frameworks.md#reactjs-element
Thanks!
The latest version doesn't require referencing this in the payment() function, so hopefully this integration should be a bit easier. See https://github.com/paypal/paypal-checkout/blob/master/demo/react.htm
@bluepnume I am not sure if you are referring to my comment - if so, thanks :) but I was talking about the original issue of this thread (paypal.Button.react is undefined) which happened to me today and I couldn't figure out why until I got here.
for me, it works when using it like this:
import React from 'react';
import ReactDOM from 'react-dom';
import paypal from 'paypal-checkout';
const ReactButton = paypal.Button.driver('react', { React, ReactDOM });
with webpack by create-react-app. The only problem is that paypal-checkout is so damn huge, thanks to webpack2+ allows I can use dynamic imports so it does not have such huge impact on the bundle size. But the part is still huge.
@eXtreme You don't have to use webpack for paypal-checkout you can simply include a script tag.
@itaysabato -- fair point. At this point really paypal.Button.react is deprecated, so let me fix those docs.
Done.
Hi, can anyone pls let me know what does "paypal.Button.driver('react', { React, ReactDOM }); mean?
what is button.driver() do?
This function returns a React component that you can drop into your render().
how can we know more about it? Is it reactjs specific or paypal? Is there any docs on paypal developers site related to it?
When the button renders, the iframe takes a few secs to load. So if the user clicks on the button really fast (before iframe is completely loaded), the paypal button is non-responsive. Is there a way for paypal button to be responsive when as soon as the react component renders?
@artofspeed yes, we have that planned.
@artofspeed yes, we have that planned.
Any news on this?
@bluepnume What is driver in react.button.driver()? Where can I find a source code of this driver implementation?
Mainly because I need to know how can I pass a div inside to implement REST API based integration using paypal button? paypal.button.render did accept an id of the component to render server-based iframe into. How do it in react with driver?
@bluepnume I am trying to render a PayPal button in a dialog in React, when I close the dialog after the component is loaded. It gives the error Uncaught Error: No response from window - cleaned up. Do you know how to fix this?
@leogoesger @bluepnume I'm seeing the same error occasionally, but I were not able to reproduce it.
Any suggestions on what might fix it or at least how to track it down?
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
I have done a workaround...
Using react-async-script-loader (https://github.com/leozdgao/react-async-script-loader), I can register window.React and window.ReactDOM before checkout.js loads...
Basically:
What do you think of this solution?