一起养成写作习惯!这是我参与「掘金日新计划 · 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…
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;
}
纯手写代码,如有错误欢迎指正。