qivopay-sdk
Version:
SDK de pagamentos com MercadoPago e PagarMe
401 lines (330 loc) • 14.1 kB
text/typescript
import pagarme from 'pagarme'
import 'dotenv/config'
type Gateway = 'mercadopago' | 'pagarme' | 'asaas'
interface SdkConfig {
gateway: Gateway
chooseBestGateway?: boolean
preference?: 'fee' | 'daysToReceive'
paymentMethod: string
installmentCount?: number
publicKey: string
containerId: string
amount: number
buttonText?: string
onTokenGenerated: (token: string) => void
customer: {
name: string
email: string
phone: string
document: string
address: {
number: string
postalCode: string
}
}
}
export class PaymentSdk {
private gatewayKey = ''
private baseApiQivoPay = process.env.BASE_API_QIVOPAY
constructor(private config: SdkConfig) {}
async init() {
if (this.config.chooseBestGateway) {
const { gateway, apiKey } = await this.chooseBestGateway()
this.config.gateway = gateway
this.gatewayKey = apiKey
await this.loadGateways(this.config.gateway)
return
}
if (!this.config.chooseBestGateway) {
if (!this.config.gateway) {
const { gateway, apiKey } = await this.chooseBestGateway()
this.config.gateway = gateway
this.gatewayKey = apiKey
await this.loadGateways(this.config.gateway)
return
}
const { gateway, apiKey } = await this.getGatewayInfo(this.config.gateway)
this.config.gateway = gateway
this.gatewayKey = apiKey
await this.loadGateways(this.config.gateway)
}
}
private async loadGateways(gateway: Gateway) {
switch(gateway) {
case 'mercadopago':
await this.loadMercadoPago()
this.mountMercadoPagoComponent()
break
case 'pagarme':
await this.loadPagarme()
this.mountPagarmeForm()
break
case 'asaas':
this.mountAsaasForm()
break
default:
throw new Error('Gateway não suportado')
}
}
private async chooseBestGateway() {
const { amount, publicKey } = this.config
const response = await fetch(`${this.baseApiQivoPay}/gateway/best-gateway?amount=${amount}&paymentMethod=${this?.config?.paymentMethod || 'credit_card'}&preference=${this?.config?.preference || 'fee'}&installmentCount=${this?.config?.installmentCount || 1}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${publicKey}`
}
})
if (!response.ok) {
throw new Error('Erro ao buscar gateway')
}
const { data: { gateway, apiKey } } = await response.json()
return { gateway, apiKey }
}
private async getGatewayInfo(gatewayToFind: Gateway) {
const { publicKey } = this.config
const response = await fetch(`${this.baseApiQivoPay}/choosed-gateway?gateway=${gatewayToFind}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${publicKey}`
}
})
if (!response.ok) {
throw new Error('Erro ao buscar gateway')
}
const { gateway, apiKey } = await response.json()
return { gateway, apiKey }
}
async loadMercadoPago() {
return new Promise((resolve, reject) => {
const scriptId = 'mercado-pago-sdk'
if (document.getElementById(scriptId)) return resolve(null)
const script = document.createElement('script')
script.id = scriptId
script.src = 'https://sdk.mercadopago.com/js/v2'
script.onload = () => resolve(null)
script.onerror = reject
document.body.appendChild(script)
})
}
mountMercadoPagoComponent() {
const mp = new window.MercadoPago(this.gatewayKey)
const form = document.getElementById(this.config.containerId)
if (!form) {
throw new Error('Formulário não encontrado')
}
form.innerHTML = `
<form id="mercadopago-form" style="max-width:400px;margin:auto;padding:16px;background:#fff;border-radius:8px;display:flex;flex-direction:column;gap:12px;">
<input type="text" id="form-name" placeholder="Nome no cartão"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="form-number" placeholder="Número do cartão"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="form-expiration" placeholder="MM/YY"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="form-cvv" placeholder="CVV"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<select id="form-installments"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;">
${[...Array(12)].map((_, i) => `<option value="${i + 1}">${i + 1}x</option>`).join('')}
</select>
<select id="form-issuer"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%; display: none;">
${[...Array(12)].map((_, i) => `<option value="${i + 1}">Bandeira ${i + 1}</option>`).join('')}
</select>
<button type="submit"
style="padding:12px;background:#2563eb;color:white;border:none;border-radius:6px;font-weight:bold;font-size:16px;cursor:pointer;transition:background 0.2s;">
${this.config.buttonText || 'Comprar'}
</button>
</form>
`
const formFinded = document.getElementById('mercadopago-form')
if (!formFinded) {
throw new Error('Formulário não encontrado')
}
if (formFinded) {
const cardForm = mp.cardForm({
amount: this.config.amount.toString() || '0',
autoMount: true,
form: {
id: `mercadopago-form`,
cardholderName: { id: 'form-name', placeholder: 'Nome no cartão' },
cardNumber: { id: 'form-number', placeholder: 'Número do cartão' },
expirationDate: { id: 'form-expiration', placeholder: 'MM/YY' },
securityCode: { id: 'form-cvv', placeholder: 'CVV' },
installments: { id: 'form-installments', placeholder: 'Parcelas' },
issuer: { id: 'form-issuer', placeholder: 'Bandeira' }
},
callbacks: {
onFormMounted: (error: any) => {
if (error) return console.warn('Erro ao montar o form:', error)
},
onSubmit: async (event: any) => {
event.preventDefault()
const { token } = await cardForm.getCardFormData()
this.config.onTokenGenerated(token)
}
}
})
requestAnimationFrame(() => {
cardForm.mount()
})
}
}
async loadPagarme() {
if (!pagarme) {
throw new Error('SDK do Pagar.me não carregado')
}
}
mountPagarmeForm() {
const form = document.getElementById(this.config.containerId)
if (!form) {
throw new Error('Formulário não encontrado')
}
form.innerHTML = `
<form id="pagarme-form" style="max-width:400px;margin:auto;padding:16px;background:#fff;border-radius:8px;display:flex;flex-direction:column;gap:12px;">
<input type="text" id="card-number" name="number" placeholder="Número do cartão"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="card-holder" name="holderName" placeholder="Nome do titular"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<div style="display:flex;gap:8px;">
<input type="text" id="card-exp-month" name="expMonth" placeholder="MM"
style="flex:1;padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;" />
<input type="text" id="card-exp-year" name="expYear" placeholder="YYYY"
style="flex:1;padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;" />
</div>
<input type="text" id="card-cvv" name="cvv" placeholder="CVV"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<select id="form-installments" style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;">
${[...Array(12)].map((_, i) => `<option value="${i + 1}">${i + 1}x</option>`).join('')}
</select>
<select id="form-issuer" style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%; display: none;">
${[...Array(12)].map((_, i) => `<option value="${i + 1}">Bandeira ${i + 1}</option>`).join('')}
</select>
<button type="submit"
style="padding:12px;background:#2563eb;color:white;border:none;border-radius:6px;font-weight:bold;font-size:16px;cursor:pointer;transition:background 0.2s;">
${this.config.buttonText || 'Comprar'}
</button>
</form>
`
const getValue = (name: string) =>
(form.querySelector(`input[name="${name}"]`) as HTMLInputElement)?.value?.trim() || ''
form.addEventListener('submit', async (e) => {
e.preventDefault()
const cardData = {
number: getValue('number'),
holderName: getValue('holderName'),
expMonth: getValue('expMonth'),
expYear: getValue('expYear'),
cvv: getValue('cvv'),
}
const client = await pagarme.client.connect({ api_key: this.gatewayKey })
const cardHash = await client.security.createCardHash({
card_number: cardData.number,
card_holder_name: cardData.holderName,
card_expiration_date: `${cardData.expMonth}${cardData.expYear.slice(2)}`, // MMYY
card_cvv: cardData.cvv
})
this.config.onTokenGenerated(cardHash)
})
}
mountAsaasForm() {
const form = document.getElementById(this.config.containerId)
if (!form) {
throw new Error('Formulário não encontrado')
}
form.innerHTML = `
<form id="asaas-form" style="max-width:400px;margin:auto;padding:16px;background:#fff;border-radius:8px;display:flex;flex-direction:column;gap:12px;">
<input type="text" id="form-name" placeholder="Nome no cartão"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="form-number" placeholder="Número do cartão"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="form-expiration" placeholder="MM/YY"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<input type="text" id="form-cvv" placeholder="CVV"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;" />
<select id="form-installments"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%;">
${[...Array(12)].map((_, i) => `<option value="${i + 1}">${i + 1}x</option>`).join('')}
</select>
<select id="form-issuer"
style="padding:10px;border:1px solid #ccc;border-radius:6px;font-size:14px;width:100%; display: none;">
${[...Array(12)].map((_, i) => `<option value="${i + 1}">Bandeira ${i + 1}</option>`).join('')}
</select>
<button type="submit"
style="padding:12px;background:#2563eb;color:white;border:none;border-radius:6px;font-weight:bold;font-size:16px;cursor:pointer;transition:background 0.2s;">
${this.config.buttonText || 'Comprar'}
</button>
</form>
`
const getValue = (name: string) =>
(form.querySelector(`input[name="${name}"]`) as HTMLInputElement)?.value?.trim() || ''
form.addEventListener('submit', async (e) => {
e.preventDefault()
const apiKey = this.gatewayKey
const asaasBaseUrl = 'https://api.asaas.com/v3'
const getCustomer: any = await fetch(`${asaasBaseUrl}/customers?email=${this.config.customer.email}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
}
})
let createdCustomer: any = null
if (!getCustomer.data) {
createdCustomer = await fetch(`${asaasBaseUrl}/customers`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
email: this.config.customer.email,
name: this.config.customer.name,
phone: this.config.customer.phone,
cpfCnpj: this.config.customer.document
})
})
}
const cardData = {
customer: createdCustomer.data.id,
number: getValue('number'),
holderName: getValue('holderName'),
expMonth: getValue('expMonth'),
expYear: getValue('expYear'),
cvv: getValue('cvv'),
creditCard: {
holderName: getValue('holderName'),
number: getValue('number'),
expiryMonth: getValue('expMonth'),
expiryYear: getValue('expYear'),
cvv: getValue('cvv')
},
creditCardHolderInfo: {
name: getValue('holderName'),
email: getValue('email'),
cpfCnpj: getValue('document'),
phone: getValue('phone'),
postalCode: this.config.customer.address.postalCode,
addressNumber: this.config.customer.address.number,
}
}
fetch(`${asaasBaseUrl}/creditCard/tokenizeCreditCard`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.gatewayKey}`
},
body: JSON.stringify(cardData)
})
.then(res => res.json())
.then(data => {
this.config.onTokenGenerated(data.token)
})
})
}
destroy() {
const container = document.getElementById(this.config.containerId)
if (container) container.innerHTML = ''
}
}