saveinmed/saveinmed-frontend/src/components/MercadoPagoPaymentBrick.tsx
Tiago Yamamoto b39caf0fd0 first commit
2025-12-17 13:58:26 -03:00

306 lines
12 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from 'react';
import { initMercadoPago, Payment } from '@mercadopago/sdk-react';
import { toast } from 'react-hot-toast';
interface DadosComprador {
nome?: string;
email?: string;
}
interface MercadoPagoPaymentBrickProps {
pedidoId: string;
valorTotal: number;
dadosComprador?: DadosComprador;
faturaId?: string | null;
onSuccess?: (payment: any) => void;
onError?: (error: string) => void;
onProcessingChange?: (processing: boolean) => void;
}
export const MercadoPagoPaymentBrick = ({
pedidoId,
valorTotal,
dadosComprador,
faturaId,
onSuccess,
onError,
onProcessingChange
}: MercadoPagoPaymentBrickProps) => {
const [brickReady, setBrickReady] = useState(false);
const [processing, setProcessing] = useState(false);
const publicKey = process.env.NEXT_PUBLIC_MERCADO_PAGO_PUBLIC_KEY;
const amount = useMemo(() => {
const parsed = Number(valorTotal || 0);
return Number(parsed.toFixed(2));
}, [valorTotal]);
useEffect(() => {
if (!publicKey) {
console.error('❌ NEXT_PUBLIC_MERCADO_PAGO_PUBLIC_KEY não configurada');
return;
}
// 🔍 Log da chave pública para verificar ambiente (sandbox vs produção)
const keyPrefix = publicKey.substring(0, 15);
const isSandbox = publicKey.includes('TEST');
initMercadoPago(publicKey, {
locale: 'pt-BR'
});
}, [publicKey]);
const enviarPagamento = async (formData: any) => {
if (!pedidoId) {
throw new Error('Pedido não encontrado');
}
onProcessingChange?.(true);
setProcessing(true);
const nome = dadosComprador?.nome?.trim() || 'Cliente SaveInMed';
const [firstName, ...resto] = nome.split(' ');
const lastName = resto.join(' ') || 'SaveInMed';
const payload = {
...formData,
transaction_amount: amount,
pedidoId,
faturaId: faturaId || undefined,
description: `Pagamento do pedido ${pedidoId}`,
payer: {
...formData.payer,
email: dadosComprador?.email || formData?.payer?.email,
first_name: firstName,
last_name: lastName
},
metadata: {
origem: 'checkout',
pedidoId
}
};
const response = await fetch('/api/mercadopago/pagamentos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
console.log('📡 [API RESPONSE] Status:', response.status);
console.log('📡 [API RESPONSE] OK?:', response.ok);
console.log('📡 [API RESPONSE] Body:', result);
if (!response.ok || !result.success) {
console.error('❌ [API ERROR] Detalhes completos:', {
status: response.status,
result: result,
error: result.error,
details: result.details
});
throw new Error(result.error || 'Erro ao criar pagamento');
}
return result.payment;
};
const handleSubmit = ({ formData }: { formData: any }) => {
return new Promise(async (resolve, reject) => {
try {
// ⚠️ Validação crítica: verificar se token existe para cartões
if (formData.payment_method_id !== 'pix' && !formData.token) {
console.error('❌ [PAYMENT BRICK] Token do cartão não foi gerado! formData:', formData);
throw new Error('Token do cartão não foi gerado. Por favor, verifique os dados do cartão e tente novamente.');
}
const payment = await enviarPagamento(formData);
const status = payment.status as string;
if (status === 'approved') {
toast.success('Pagamento aprovado com sucesso!');
} else if (status === 'pending' && payment.payment_method_id === 'pix') {
toast.success('PIX gerado! Conclua no seu aplicativo bancário.');
} else {
toast.success('Pagamento iniciado! Aguarde a confirmação.');
}
onSuccess?.(payment);
resolve(payment);
} catch (error) {
console.error('❌ [PAYMENT BRICK] Erro:', error);
const message = error instanceof Error ? error.message : 'Erro ao processar pagamento';
toast.error(message);
onError?.(message);
reject(error);
} finally {
onProcessingChange?.(false);
setProcessing(false);
}
});
};
if (!publicKey) {
return (
<div className="p-4 border border-red-200 bg-red-50 rounded-lg text-sm text-red-700">
Configuração do Mercado Pago ausente. Defina a variável
<code className="px-1 mx-1 rounded bg-red-100 text-red-800 text-xs">
NEXT_PUBLIC_MERCADO_PAGO_PUBLIC_KEY
</code>
e tente novamente.
</div>
);
}
if (!pedidoId) {
return (
<div className="p-4 border border-yellow-200 bg-yellow-50 rounded-lg text-sm text-yellow-800">
Aguarde a criação do pedido para liberar o pagamento.
</div>
);
}
return (
<div className="w-full">
{/* Premium Header Card */}
<div className="bg-gradient-to-br from-blue-600 to-indigo-700 rounded-t-2xl p-6 shadow-lg">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center mb-2">
<div className="bg-white/20 backdrop-blur-sm rounded-lg p-2 mr-3">
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-blue-100">Pagamento Seguro</p>
<p className="text-xs text-blue-200">Mercado Pago</p>
</div>
</div>
<div className="mt-3">
<p className="text-3xl font-bold text-white">
{amount.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
</p>
<p className="text-sm text-blue-100 mt-1">Cartão à vista PIX Sem taxas extras</p>
</div>
</div>
<div className="text-right">
<div className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${processing ? 'bg-yellow-400 text-yellow-900' :
brickReady ? 'bg-green-400 text-green-900' :
'bg-white/20 text-white'
}`}>
{processing ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-3 w-3" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Processando...
</>
) : brickReady ? (
<>
<svg className="w-3 h-3 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Pronto
</>
) : (
'Carregando...'
)}
</div>
</div>
</div>
</div>
{/* Payment Form Container */}
<div className="bg-white rounded-b-2xl shadow-xl border border-gray-200 border-t-0">
<div className="p-6">
{/* Info Banner */}
<div className="mb-6 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-xl p-4">
<div className="flex items-start">
<svg className="w-5 h-5 text-blue-600 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
<div>
<p className="text-sm font-medium text-blue-900">Pagamento 100% Seguro</p>
<p className="text-xs text-blue-700 mt-1">
Seus dados são protegidos com criptografia de ponta. Não armazenamos informações do cartão.
</p>
</div>
</div>
</div>
{/* Payment Brick */}
<div className="rounded-xl overflow-hidden border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 shadow-inner">
<Payment
initialization={{
amount,
payer: {
email: dadosComprador?.email || 'cliente@saveinmed.com.br',
}
}}
customization={{
visual: {
style: {
theme: 'default',
},
hidePaymentButton: false,
},
paymentMethods: {
creditCard: 'all',
debitCard: 'all',
bankTransfer: ['pix'],
maxInstallments: 1,
},
}}
onSubmit={handleSubmit}
onError={(error: any) => {
console.error('❌ [PAYMENT BRICK SDK] Erro ao renderizar/processar:', error);
console.error('❌ [PAYMENT BRICK SDK] Detalhes do erro:', JSON.stringify(error, null, 2));
const errorMessage = error?.message || error?.error || 'Erro ao carregar métodos de pagamento';
toast.error(errorMessage);
onError?.(errorMessage);
}}
onReady={() => {
setBrickReady(true);
}}
/>
</div>
{/* Security Badges */}
<div className="mt-6 pt-6 border-t border-gray-200">
<div className="flex items-center justify-center space-x-6 text-xs text-gray-500">
<div className="flex items-center">
<svg className="w-4 h-4 text-green-500 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>SSL Seguro</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-blue-500 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
</svg>
<span>Dados Criptografados</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-purple-500 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Certificado PCI</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default MercadoPagoPaymentBrick;