vue中如何优雅的接入PayPal

3,085 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

本文中写的是PayPal V4版本,文档地址developer.paypal.com/docs/archiv…

paypal sdk的基本原理

PayPal的基本流程是先创建按钮所在的dom,然后PayPal sdk会在里面生成一个按钮,为了安全,按钮将会是一个iframe。

第一步:引入sdk

不建议使用官方的地址,这个地址很慢,而且偶尔打不开

<script src="https://www.paypalobjects.com/api/checkout.js"></script>

可以使用这个地址,直接使用github上的文件,并通过jsdelivr做了一个加速:

<script src="https://cdn.jsdelivr.net/gh/paypal/paypal-checkout-components@v4.0.331/dist/checkout.min.js"></script>

创建支付按钮

在vue中,我们只需要写一个template就可以了。提示一下:不建议把按钮的id命名为paypal,会造成冲突。

<template>
<div ref="paypal" id="paypalDom"></div>
</template>

为了考虑兼容vue2/3,后续我用options API去写。

监听dom创建完成

PayPal按钮的初始化必须在dom创建之后,这里可以采用2种办法:ref+watch,mounted.之所以不全部使用mounted,是因为在某些复杂页面时,你可能希望提前加载PayPal按钮

ref+watch

<script>
  export default {
      create(){
       this.$watch(()=>this.$refs.paypal,()=>{consoloe.log('paypal dom rendered')})
      }
  }
</script>

mounted

<script>
  export default {
      mounted(){
       consoloe.log('paypal dom rendered')
      }
  }
</script>

sdk异常处理

sdk加载后,会在window下创建一个window.paypal,可以通过这个方法验证sdk是否加载成功.这里需要加载失败的时候重试,这时候建议将之前的js下载到本地,放到public文件夹下

if(window.paypal){
  this.renderPaypal()
} else {
  const timer = setInterval(()=>{
    const s = document.createElement('script')
    s.src = 'js/paypal-checkout.min.js'
    s.onload = ()=>{
      if(window.paypal){
        this.paypalReady = true
        clearInterval(timer)
        this.renderPaypal()
      }
    }
    document.querySelector('head').appendChild(s)
  },1000)

}

按钮加载前

这时候就可以开始尝试加载PayPal按钮了,这里就是最基本的API。

window.paypal.Button.render({
  env:'sandbox', // sandbox or production
  client: {
      sandbox:'paypal sandbox key',
      production:'paypal production key',
  },
  style:{},
  commit: true,
  payment: async function (data, actions) {
      return actions.payment.create({
                        payment: {
                            transactions: [{
                                invoice_number: '订单id,选传',
                                notify_url: "webhook地址,选传",
                                amount: {
                                    details: {},
                                    total: '总金额,必传',
                                    currency: '货币代码,必传'
                                }
                            }],
                        },
                        experience: {
                            input_fields: {
                                no_shipping: 1
                            }
                        }
                    })
  },
  onAuthorize: function (data, actions) {
      return actions.payment.execute()
            .then(function (paymentDetails) {
                 //paypal弹框此时关闭
                 if (paymentDetails.state === 'approved') {
                      const payment_id = paymentDetails.id
                      //支付完成逻辑
                  }
            })
  
  },
  onCancel: function (data, actions) {},
  onError: function (err) {}
  },('#paypalDom'))

但是有一些问题,当你所用的手机是全面屏时,例如iPhoneX,PayPal的显示会是这样。PayPal弹层的关闭按钮会出现在非安全区域导致无法点击。这应该是PayPal没有适配全面屏的问题,因此得手工处理一下。我的想法是在payment之前放一个定时器去判断。其中--safe-top是获取到的安全区域变量,可以参考这篇文章jelly.jd.com/article/600…

image.png

const fixPopup = ()=>{
     setTimeout(()=>{
          const paypalPopup = document.querySelector('.paypal-checkout-sandbox')
            if(paypalPopup){
              paypalPopup.style.top = document.body.style.getPropertyValue('--safe-top')
              paypalPopup.style.height = 'calc(100vh - var(--safe-top))'
              paypalPopup.style.maxHeight = 'calc(100% - var(--safe-top))'
              paypalPopup.style.minHeight = 'calc(100% - var(--safe-top))'
           }
    },500)
}

按钮加载后

通过打印window.paypal.Button.render,可以发现这是一个promises,于是可以通过then方法来实现之后加载后的逻辑,但实际上,当按钮展示出来时,有一段时间是不可点击的,仅是一个svg

//配置省略
window.paypal.Button.render({xxx},('#paypalDom'))
 .then(() => {
     //解决渲染2个paypal button的问题
     this.$nextTick(() =>{
        const btn = document.querySelectorAll('#paypalDom .paypal-button')
         if(btn&&btn.length>1){
           for(let i=1;i<btn.length;i++){
              // btn[i].style.display='none'
               document.getElementById('payButton').removeChild(btn[i])
               }
            }
        })
     //判断PayPal能否点击  
    
    let i = 0
    const timer = setInterval(() => {
      i++
      if (i >= 30) {
        logger('paypal render timeout','paypal loading layer had run more than 6s')
        clearInterval(timer)
        return
      }
      const iframes = document.querySelectorAll('#payButton iframe')
      if (iframes[0]&&iframes[0].className.indexOf('prerender') >= 0) {
        // console.log('paypal不能点击')
        this.paypalCanClick = false
      } else {
        // console.log('paypal能点击')
        this.paypalCanClick = true
        clearInterval(timer)
      }
    }, 200)
     
   }

全部代码

<template>
  <div class="paypal">
    <div v-if="paypalReady" id="payButton" class="paypal_button"></div>
  </div>
</template>

<script>
import {logger} from "../utils" 
export default {
  name: "Paypal",
  data() {
    return {
      paypalReady: false,
      paypalCanClick: false,
      paypal_style:{},
    }
  },
  computed: {
    client() {
      return {
          sandbox:'',
          productions:''
      }
    }
  },

  mounted() {
    if (window.paypal) {
      this.paypalReady = true
      this.$nextTick(this.renderPaypal)
    } else {
      this.paypalReady = false
      const s = document.createElement('script')
      s.src = location.origin + location.pathname + 'js/paypal-checkout.min.js'
      s.setAttribute('ver','4.0.332')
      s.onload = () => {
        if (window.paypal) {
          this.paypalReady = true
          this.$nextTick(this.renderPaypal)
        }
      }
      document.body.appendChild(s)
    }
  },
  methods: {
    // 渲染PayPal支付按钮
    initPayPal() {
      const _self = this
      return window.paypal.Button.render({
        env: '',
        client: this.client,
        style: this.paypal_style,
        commit: true,
        payment: async function (data, actions) {
   
            // 处理paypal弹层关闭按钮的适配问题
            setTimeout(()=>{
              const paypalPopup = document.querySelector('.paypal-checkout-sandbox')
              if(paypalPopup){
                paypalPopup.style.top = document.body.style.getPropertyValue('--safe-top')
                paypalPopup.style.height = 'calc(100vh - var(--safe-top))'
                paypalPopup.style.maxHeight = 'calc(100% - var(--safe-top))'
                paypalPopup.style.minHeight = 'calc(100% - var(--safe-top))'
              }
            },500)
            return actions.payment.create({
              payment: {
                transactions: [{
                  invoice_number:'',
                  notify_url: "",
                  amount: {
                    details: {},
                    total: '',
                    currency:'USD'
                  }
                }],
              },
              experience: {
                input_fields: {
                  no_shipping: 1
                }
              }
            })
      
        },
        onAuthorize: function (data, actions) {
          return actions.payment.execute()
              .then(function (paymentDetails) {
                if (paymentDetails.state === 'approved') {
                 
                  return
                }
              })
        },
        onCancel: function (data, actions) {
          _self.$parent.$emit('cancel',_self.type,data)
          _self.$parent.$emit('paypalCancelled', data)
        },
        onError: function (err) {
          console.log('paypalError', err)
          _self.$parent.$emit('error',_self.type,err)
          _self.$parent.$emit('paypalError', err)
        }
      }, '#payButton')
          .then(() => {
            //解决渲染2个paypal button的问题
            _self.$nextTick(() =>{
              const btn = document.querySelectorAll('#payButton .paypal-button')
              if(btn&&btn.length>1){
                for(let i=1;i<btn.length;i++){
                  // btn[i].style.display='none'
                  document.getElementById('payButton').removeChild(btn[i])
                }
              }
            })
            // 在paypal加载完成之前加上一个loading
            let i = 0
            const timer = setInterval(() => {
              i++
              if (i >= 30) {
                if(this.PropsData.debug){
                  console.log('paypal loading layer had run more than 6s')
                }
                clearInterval(timer)
                _self.$parent.$emit('paypalTimeout')
                return
              }
              const iframes = document.querySelectorAll('#payButton iframe')
              if (iframes[0]&&iframes[0].className.indexOf('prerender') >= 0) {
                // console.log('paypal不能点击')
                _self.paypalCanClick = false
              } else {
                // console.log('paypal能点击')
                _self.paypalCanClick = true
                _self.$parent.$emit('canClick')
                clearInterval(timer)
              }
            }, 200)
            _self.$once('hook:beforeDestroy', () => clearInterval(timer))

          })
          .catch(err => {
            if(this.PropsData.debug){
              console.log('paypal render error',err)
            }
            _self.$parent.$emit('error',_self.type,err)
          })
    },
    // paypal被点击
 
}
</script>

<style scoped>
.paypal,
.paypal_button {
  width: 100%;
  height: auto;
  min-height: 35px;
}

纯手写代码,如有错误欢迎指正。