Paypal-checkout-components: Deprecate NPM package.

Created on 25 Jan 2019  路  21Comments  路  Source: paypal/paypal-checkout-components

We need to deprecate the NPM package so people quit integrating with it.

https://docs.npmjs.com/cli/deprecate

I think you should be able to do

`npm deprecate paypal-checout@"> 4.0.289" "CDN script include is now required moving forward. Please update your integration to include the paypal script from CDN"

outdated

Most helpful comment

@alex-exerve can you elaborate? Using the new sdk should be as simple as dropping a script tag. See https://developer.paypal.com/docs/checkout

I'm afraid it's not that simple, especially for people who are focusing on page speed. We don't want to reference a large SDK in our client-side applications, especially in the index file. We want to install the package and run it dynamically on the payment pages, that gives us code-splitting (chunks) out of the box, so that the JS behind this library is only fetched when needed.

If we're forced to use the script tag, then we'd need to find a good way to reference it only on payment pages. That could be a trouble since the client-side app might load the component before the script has loaded, which makes the implementation complex.

All 21 comments

Agreed. We're going for a feb 7th doc release for the new sdk, we can make that deprecation then.

@bluepnume any specific reason to wait? It won't break anything but will show that message so people can prepare. At least the documentation says it just displays a warning so I don't think it would break anything just display the warning.

Mainly because if we're deprecating we should have a recommended upgrade path. Upgrading to checkout.js on the CDN is only a short-term solve, so I'd rather wait a few weeks and give people a long-term path.

Our application which uses "paypal-checkout": "^4.0.204" stopped working on these days. You click the button and it sends logging info to paypal but our callback doesnt happen. Is this related?

@georgiosd Kind-of. We want to be sure merchants don't integrate with broken versions and are always on the latest, which is why we are talking about depreciating the npm package and pushing integration through checkout.js instead.

Will i be able to properly integrate the cdn version with my build process? I have a Vue application that renders client side and it's gonna be really detrimental if i have to rework it to use an external dependency.

@alex-exerve can you elaborate? Using the new sdk should be as simple as dropping a script tag. See https://developer.paypal.com/docs/checkout

@alex-exerve can you elaborate? Using the new sdk should be as simple as dropping a script tag. See https://developer.paypal.com/docs/checkout

I'm afraid it's not that simple, especially for people who are focusing on page speed. We don't want to reference a large SDK in our client-side applications, especially in the index file. We want to install the package and run it dynamically on the payment pages, that gives us code-splitting (chunks) out of the box, so that the JS behind this library is only fetched when needed.

If we're forced to use the script tag, then we'd need to find a good way to reference it only on payment pages. That could be a trouble since the client-side app might load the component before the script has loaded, which makes the implementation complex.

Exactly. the work around I've created is a function that will add the script to the page when I need it, and in theory it chains a promise off of it too so i can not try to call the paypal object until the script is loaded.

Another issue I had is when you have different payment pages on a client-side application. For example a page for subscriptions and another page for one-time payments. There doesn't seem to be a way to configure the SDK after it has been loaded. This is the biggest issue I've had with this SDK. I created an issue for that here: https://github.com/paypal/paypal-checkout-components/issues/1028

We want to install the package and run it dynamically on the payment pages, that gives us code-splitting (chunks) out of the box, so that the JS behind this library is only fetched when needed.

Agreed on the need to lazy-load the script; especially when the button is being rendered as part of a SPA, after a user-action. The performance doc elaborates on this: https://developer.paypal.com/docs/checkout/troubleshoot/performance/#delayed-render

Bundling the script from an npm module would actually degrade performance, for the following reasons:

a) The new sdk script is dynamically compiled on demand when your browser hits that /sdk/js url, based on the parameters you pass in. We're aggressively cutting the script back to only include the code / buttons / images / other resources for the specific buyer and transaction on your page (rather than loading everything needed for the initial render for every possible buyer, like the original checkout.js did). We've managed to pull the size of the script back significantly already with this approach.

b) We load the same sdk script inside the button iframe and checkout popup, in order to communicate across the different windows. Loading the script from paypal.com consistently, means that it's cached across all windows and frames, and only gets downloaded a single time -- rather than needing to be downloaded twice from two different locations (your site's bundle, and paypal.com)

If we're forced to use the script tag, then we'd need to find a good way to reference it only on payment pages. That could be a trouble since the client-side app might load the component before the script has loaded, which makes the implementation complex.

In theory that can be achieved by dynamically adding a script tag (per the doc above) and listening for onload - as @alex-exerve suggested. We'll explore ways to make that easier.

Another issue I had is when you have different payment pages on a client-side application. For example a page for subscriptions and another page for one-time payments. There doesn't seem to be a way to configure the SDK after it has been loaded.

This is a fair ask, and we're working on a way for the script to be seamlessly destroyed and reloaded with different inputs. (since those inputs determine the script which is generated and the funding sources which are loaded)

Thanks for the detailed answer @bluepnume. Much appreciated.

Agreed on the need to lazy-load the script; especially when the button is being rendered as part of a SPA, after a user-action. The performance doc elaborates on this: https://developer.paypal.com/docs/checkout/troubleshoot/performance/#delayed-render

That's a very generic solution. I highly recommend adding a section for popular SPA frameworks.

For React I add the script to the document.head in the component constructor. I also make sure that window.paypal is undefined, because you get an error from the SDK if you try to add the script twice, and it could cause unwanted side effects.

if (window.paypal === undefined) {
    this.script = document.createElement('script')
    // this.script.type = 'text/javascript'
    this.script.src = `https://www.paypal.com/sdk/js?client-id=${this.paypalClientId}&currency=${this.props.currency.toUpperCase()}&commit=true&intent=capture`
    this.script.async = true;
    document.head.appendChild(this.script)
}

In componentDidMount I check if paypal.window is undefined, and if that's the case I re-check every 250ms until paypal.window is not undefined. This is done because the script might take a few seconds or milliseconds to load.

componentDidMount() {
    if (window.paypal) {
        this.setState({
            hasPaypalSdkLoaded: true,
        })
    } else {
        this.interval = setInterval(() => {
            if (window.paypal) {
                clearInterval(this.interval)
                if (this.isRemoved === false) {
                    this.setState({
                        hasPaypalSdkLoaded: true,
                    })
                }
            }
        }, 250)
    }
}

And here's componentWillUnmount() to clean up the interval if it's running. I also set this.isRemoved to true to avoid a race condition if the interval in componentDidMount() is called when the component has been removed.

componentWillUnmount = () => {
    this.isRemoved = true
    if (this.interval) {
        clearInterval(this.interval)
    }
}

Kinda messy but it does the job. Perhaps it would be best if a React wrapper library is created, e.g. react-paypal-checkout-components that does this for you. It could be a hook, HOC or something else. I'll probably look into it if I have the time.

This is a fair ask, and we're working on a way for the script to be seamlessly destroyed and reloaded with different inputs. (since those inputs determine the script which is generated and the funding sources which are loaded)

That sounds good. Any ETA? :)

So, I'm sharing this function because it's pretty generic and is useful.

function getScript(src, id="", isAsync = true, appendTo=null){
  if(document.querySelectorAll('script[src="'+src+'"]').length){
    return Promise.resolve()
  }
  let script = document.createElement('script')
  script.id = id
  if(isAsync) script.async = isAsync
  script.src = src

  let scriptDone = new Promise((res,rej)=>{
    script.onload = script.onreadystatechange = res
  })

  if(typeof appendTo ==="string"){
    document.querySelector(appendTo).appendChild(script)
  }else{
    document.body.appendChild(script)
  }

  return scriptDone

}

ideally, you can use this like so:

getScript('https://www.paypal.com/sdk/js?client-id={clientid}').then(()=>paypal.Buttons().render('#paypal-button-container'));

and it will load correctly. in theory. I haven't done extensive testing with this.

@raRaRa that works -- but I would highly recommend using the onload event instead of polling using setInterval. Then you can use setState to set a flag indicating paypal is loaded, and trigger a re-render.

@alex-exerve -- looks good. I would also recommend handing the onerror event and rejecting the promise.

@alex-exerve Where exactly would you call getScript() in your example? I tried calling it at the top of render() (after plugging in my own Client ID), and am still getting: 'paypal' is not defined no-undef.

Update: replaced paypal with window.paypal and now it works!

That's a very generic solution. I highly recommend adding a section for popular SPA frameworks.

+1 for that recommendation from raRaRa above.

It would be awesome if the same recommendation could apply to Braintree's documentation on how to implement with checkout.js. For folks building react apps, especially with static type checking, it seems ideal to be able to simply import checkout from 'paypal-checkout' as otherwise there are lots of edge cases around whether the network request failed or not which would influence whether or not paypal variable is safe to access in code examples such as Braintree's example below:

// Be sure to have PayPal's checkout.js library loaded on your page.
// <script src="https://www.paypalobjects.com/api/checkout.js" data-version-4></script>

// Create a client.
braintree.client.create({
  authorization: CLIENT_AUTHORIZATION
}).then(function (clientInstance) {
  // Create a PayPal Checkout component.
  return braintree.paypalCheckout.create({
    client: clientInstance
  });
}).then(function (paypalCheckoutInstance) {
  // Set up PayPal with the checkout.js library
  paypal.Button.render({
    env: 'production', // Or 'sandbox'
    commit: true, // This will add the transaction amount to the PayPal button
    payment: function () {
      return paypalCheckoutInstance.createPayment({
...

Thanks for the detailed answer @bluepnume. Much appreciated.

Agreed on the need to lazy-load the script; especially when the button is being rendered as part of a SPA, after a user-action. The performance doc elaborates on this: https://developer.paypal.com/docs/checkout/troubleshoot/performance/#delayed-render

That's a very generic solution. I highly recommend adding a section for popular SPA frameworks.

For React I add the script to the document.head in the component constructor. I also make sure that window.paypal is undefined, because you get an error from the SDK if you try to add the script twice, and it could cause unwanted side effects.

if (window.paypal === undefined) {
    this.script = document.createElement('script')
    // this.script.type = 'text/javascript'
    this.script.src = `https://www.paypal.com/sdk/js?client-id=${this.paypalClientId}&currency=${this.props.currency.toUpperCase()}&commit=true&intent=capture`
    this.script.async = true;
    document.head.appendChild(this.script)
}

In componentDidMount I check if paypal.window is undefined, and if that's the case I re-check every 250ms until paypal.window is not undefined. This is done because the script might take a few seconds or milliseconds to load.

componentDidMount() {
    if (window.paypal) {
        this.setState({
            hasPaypalSdkLoaded: true,
        })
    } else {
        this.interval = setInterval(() => {
            if (window.paypal) {
                clearInterval(this.interval)
                if (this.isRemoved === false) {
                    this.setState({
                        hasPaypalSdkLoaded: true,
                    })
                }
            }
        }, 250)
    }
}

And here's componentWillUnmount() to clean up the interval if it's running. I also set this.isRemoved to true to avoid a race condition if the interval in componentDidMount() is called when the component has been removed.

componentWillUnmount = () => {
    this.isRemoved = true
    if (this.interval) {
        clearInterval(this.interval)
    }
}

Kinda messy but it does the job. Perhaps it would be best if a React wrapper library is created, e.g. react-paypal-checkout-components that does this for you. It could be a hook, HOC or something else. I'll probably look into it if I have the time.

This is a fair ask, and we're working on a way for the script to be seamlessly destroyed and reloaded with different inputs. (since those inputs determine the script which is generated and the funding sources which are loaded)

That sounds good. Any ETA? :)

I found a node package react-paypal-button-v2 and added your solution to it. The pull request is already merged and hopefully released soon.

Going to close this issue for now as I don't see us deprecating the npm package any time soon. We don't want any new integrations on it, but we will still periodically release v4 with new customer facing updates and we need a path for merchants to seamlessly upgrade.

Yesterday we updated our npm dependencies (from 4.0.202 to 4.0.315) and the button stopped working in production (button didn't render, no error in console). We rollbacked to the 4.0.202 version and right now I am not sure what to do. Should I stick to this old version?

@Tiagojdferreira Thank you for reaching out to us! I would recommend that you update to using the JS SDK, https://developer.paypal.com/docs/checkout/reference/upgrade-integration/#, or if there is anything you can send me with your setup, that will help me to further debug your integration. This current issue is closed and not related to your question. Can you open another issue and fill in the template? Thank you.

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.

Was this page helpful?
0 / 5 - 0 ratings