├── src ├── vite-env.d.ts ├── components │ ├── index.tsx │ └── Header │ │ └── index.tsx ├── index.css ├── pages │ ├── index.tsx │ ├── Home │ │ └── index.tsx │ └── Cart │ │ └── index.tsx ├── types │ └── index.d.ts ├── App.tsx ├── services │ ├── api.ts │ ├── mercadopago.ts │ ├── pay-test.ts │ └── products.ts ├── interfaces │ └── products.ts ├── main.tsx ├── router │ └── index.tsx ├── App.css └── assets │ └── react.svg ├── .env ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── package.json ├── tsconfig.json ├── README.md ├── index.html └── public └── vite.svg /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Header } from "./Header"; 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Arial, Helvetica, sans-serif; 3 | box-sizing: border-box; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as HomeView } from "./Home"; 2 | export { default as CartView } from "./Cart"; 3 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Window { 5 | cardPaymentBrickController: any; 6 | } 7 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Router from "./router"; 2 | import "./App.css"; 3 | 4 | function App() { 5 | return ; 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | ACCESS_TOKEN: import.meta.env.VITE_ACCESS_TOKEN, 3 | API_URL: "https://api.mercadopago.com/v1/payments", 4 | }; 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_ACCESS_TOKEN="TEST-2978213897940478-011021-494d8204c2a57d709a41813dc4e8ed00-1284333207" 2 | VITE_PUBLIC_KEY="TEST-37b0aa8b-93a4-4943-91e4-6a24e05f2fa7" 3 | -------------------------------------------------------------------------------- /src/interfaces/products.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | id: number; 3 | image: string; 4 | name: string; 5 | description: string; 6 | price: number; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 3000, 9 | open: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | export default function Header() { 4 | return ( 5 |
6 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 2 | import { HomeView, CartView } from "../pages"; 3 | 4 | export default function Router() { 5 | return ( 6 | 7 | 8 | } /> 9 | } /> 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .noProduct { 2 | display: flex; 3 | justify-content: center; 4 | margin-top: 20px; 5 | font-size: 1.5rem; 6 | } 7 | 8 | header { 9 | background-color: #f0f0f0; 10 | } 11 | 12 | nav { 13 | display: flex; 14 | } 15 | 16 | nav a { 17 | color: #333; 18 | text-decoration: none; 19 | padding: 1em; 20 | } 21 | 22 | @media (max-width: 600px) { 23 | nav { 24 | flex-wrap: wrap; 25 | } 26 | 27 | nav a { 28 | width: 100%; 29 | text-align: center; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercadopago_prueba", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.6.2", 15 | "sweetalert2": "^11.7.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.0.26", 19 | "@types/react-dom": "^18.0.9", 20 | "@vitejs/plugin-react": "^3.0.0", 21 | "typescript": "^4.9.3", 22 | "vite": "^4.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/mercadopago.ts: -------------------------------------------------------------------------------- 1 | import api from "./api"; 2 | import { paymentJSON } from "./pay-test"; 3 | 4 | export const storePay = async (name: string, price: number) => { 5 | const { API_URL, ACCESS_TOKEN } = api; 6 | 7 | try { 8 | const response = await fetch(API_URL, { 9 | method: "POST", 10 | headers: { 11 | "Content-Type": "application/json", 12 | Authorization: `Bearer ${ACCESS_TOKEN}`, 13 | }, 14 | body: JSON.stringify(paymentJSON(name, price)), 15 | }); 16 | const data = await response.json(); 17 | return data; 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pasos para clonar el proyecto 2 | 3 | 1. Clonar el proyecto 4 | 5 | ```bash 6 | git clone https://github.com/silabuzinc/react-mercadopago-example 7 | ``` 8 | 9 | 2. Entrar a la carpeta 10 | 11 | ```bash 12 | cd react-mercadopago-example 13 | ``` 14 | 15 | 3. Instalar las dependencias 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | 4. Crear el archivo .env si no existe 22 | 23 | ```bash 24 | touch .env 25 | 26 | VITE_ACCESS_TOKEN="access_token" 27 | VITE_PUBLIC_KEY="public_key" 28 | ``` 29 | 30 | 5. Correr el proyecto 31 | 32 | ```bash 33 | npm run dev 34 | ``` 35 | 36 | Pueden verlos en su navegador en la dirección `http://localhost:3000` 37 | -------------------------------------------------------------------------------- /src/services/pay-test.ts: -------------------------------------------------------------------------------- 1 | export const paymentJSON = (name: string, price: number) => { 2 | return { 3 | additional_info: { 4 | items: [ 5 | { 6 | id: "1", 7 | title: name, 8 | description: 9 | "Producto Point para cobros con tarjetas mediante bluetooth", 10 | category_id: "electronics", 11 | quantity: 1, 12 | unit_price: price, 13 | }, 14 | ], 15 | payer: { 16 | first_name: "Linder", 17 | last_name: "Hassinger", 18 | phone: { 19 | area_code: "51", 20 | number: "989772179", 21 | }, 22 | }, 23 | }, 24 | description: "Payment for product", 25 | installments: 1, 26 | token: "c89f4527b38ac77c645de0cdb9cdef4a", 27 | payer: { 28 | entity_type: "individual", 29 | type: "customer", 30 | email: "linderhassinger00@gmail.com", 31 | }, 32 | payment_method_id: "visa", 33 | transaction_amount: price, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Shopping cart 8 | 14 | 15 | 16 |
17 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import products from "../../services/products"; 2 | import { type Product } from "../../interfaces/products"; 3 | import { useEffect, useState } from "react"; 4 | import { Header } from "../../components"; 5 | import Swal from "sweetalert2"; 6 | 7 | export default function Home() { 8 | const [nItem, setNum] = useState( 9 | Number(JSON.parse(localStorage.getItem("n_item") ?? "[0]")) 10 | ); 11 | 12 | const addProducts = (product: Product) => { 13 | const productsCart = JSON.parse( 14 | localStorage.getItem("products_cart") || "[]" 15 | ); 16 | product.id = nItem; 17 | productsCart.push(product); 18 | setNum(nItem + 1); 19 | localStorage.setItem("products_cart", JSON.stringify(productsCart)); 20 | 21 | Swal.fire({ 22 | position: "bottom-end", 23 | icon: "success", 24 | title: "¡Añadido correctamente!", 25 | showConfirmButton: false, 26 | background: "#242424", 27 | color: "#fff", 28 | timer: 1250, 29 | }); 30 | }; 31 | 32 | useEffect(() => { 33 | localStorage.setItem("n_item", JSON.stringify(nItem)); 34 | }, [nItem]); 35 | 36 | return ( 37 | <> 38 |
39 |
40 |

Lista de productos

41 |
42 | {products.map((product: Product) => ( 43 |
50 | 59 |
60 |

{product.name}

61 |

{product.description}

62 |

$ {product.price}

63 |
64 | 70 |
71 |
72 |
73 | ))} 74 |
75 |
76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/services/products.ts: -------------------------------------------------------------------------------- 1 | const products =[ 2 | { 3 | id: 1, 4 | name: "Producto 1", 5 | image: 6 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 7 | description: "Este es el primer producto", 8 | price: 10.0, 9 | }, 10 | { 11 | id: 2, 12 | name: "Producto 2", 13 | image: 14 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 15 | description: "Este es el segundo producto", 16 | price: 20.0, 17 | }, 18 | { 19 | id: 3, 20 | name: "Producto 3", 21 | image: 22 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 23 | description: "Este es el tercer producto", 24 | price: 30.0, 25 | }, 26 | { 27 | id: 4, 28 | name: "Producto 4", 29 | image: 30 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 31 | description: "Este es el cuarto producto", 32 | price: 40.0, 33 | }, 34 | { 35 | id: 5, 36 | name: "Producto 5", 37 | image: 38 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 39 | description: "Este es el quinto producto", 40 | price: 50.0, 41 | }, 42 | { 43 | id: 6, 44 | name: "Producto 6", 45 | image: 46 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 47 | description: "Este es el sexto producto", 48 | price: 60.0, 49 | }, 50 | { 51 | id: 7, 52 | name: "Producto 7", 53 | image: 54 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 55 | description: "Este es el séptimo producto", 56 | price: 70.0, 57 | }, 58 | { 59 | id: 8, 60 | name: "Producto 8", 61 | image: 62 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 63 | description: "Este es el octavo producto", 64 | price: 80.0, 65 | }, 66 | { 67 | id: 9, 68 | name: "Producto 9", 69 | image: 70 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 71 | description: "Este es el noveno producto", 72 | price: 90.0, 73 | }, 74 | { 75 | id: 10, 76 | name: "Producto 10", 77 | image: 78 | "https://images.unsplash.com/2/02.jpg?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", 79 | description: "Este es el décimo producto", 80 | price: 100.0, 81 | }, 82 | ]; 83 | 84 | export default products; 85 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Cart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import { type Product } from "../../interfaces/products"; 3 | import { Header } from "../../components"; 4 | import Swal from "sweetalert2"; 5 | 6 | export default function Cart() { 7 | const [products, setProducts] = useState( 8 | JSON.parse(localStorage.getItem("products_cart") ?? "[]") 9 | ); 10 | 11 | const [nItem, setNum] = useState( 12 | Number(JSON.parse(localStorage.getItem("n_item") ?? "[]")) 13 | ); 14 | 15 | const [total, setTotal] = useState(0); 16 | 17 | const dataFetchedRef = useRef(false); 18 | 19 | const deleteProducts = (product: Product) => { 20 | const newProducts = products.filter( 21 | (productO: Product) => productO.id !== product.id 22 | ); 23 | setProducts(newProducts); 24 | setNum(nItem - 1); 25 | Swal.fire({ 26 | position: "bottom-end", 27 | icon: "success", 28 | title: "¡Eliminado!", 29 | showConfirmButton: false, 30 | background: "#242424", 31 | color: "#fff", 32 | timer: 1250, 33 | }); 34 | }; 35 | 36 | useEffect(() => { 37 | const sum = products.reduce( 38 | (acc: number, product: Product) => acc + product.price, 39 | 0 40 | ); 41 | setTotal(sum); 42 | }, []); 43 | 44 | useEffect(() => { 45 | localStorage.setItem("products_cart", JSON.stringify(products)); 46 | localStorage.setItem("n_item", JSON.stringify(nItem)); 47 | }, [products]); 48 | 49 | useEffect(() => { 50 | if (dataFetchedRef.current) return; 51 | dataFetchedRef.current = true; 52 | 53 | const mp = new MercadoPago(import.meta.env.VITE_PUBLIC_KEY, { 54 | ale: "es", 55 | }); 56 | 57 | const bricksBuilder = mp.bricks(); 58 | 59 | const renderCardPaymentBrick = async (bricksBuilder: any) => { 60 | const settings = { 61 | initialization: { 62 | amount: products.reduce( 63 | (acc: number, product: Product) => acc + product.price, 64 | 0 65 | ), 66 | payer: { 67 | email: "linderhassinger00@gmail.com", 68 | }, 69 | }, 70 | customization: { 71 | visual: { 72 | style: { 73 | theme: "default", 74 | }, 75 | }, 76 | }, 77 | callbacks: { 78 | onReady: () => {}, 79 | onSubmit: async (cardFormData: any) => { 80 | const response = await fetch( 81 | "http://localhost:9004/process_payment", 82 | { 83 | method: "POST", 84 | headers: { 85 | "Content-Type": "application/json", 86 | }, 87 | body: JSON.stringify(cardFormData), 88 | } 89 | ); 90 | console.log("response", response); 91 | }, 92 | onError: (error: any) => {}, 93 | }, 94 | }; 95 | window.cardPaymentBrickController = await bricksBuilder.create( 96 | "cardPayment", 97 | "cardPaymentBrick_container", 98 | settings 99 | ); 100 | }; 101 | renderCardPaymentBrick(bricksBuilder); 102 | }, []); 103 | 104 | return ( 105 | <> 106 |
107 |
108 |

Carrito de compras

109 | {products.length === 0 && ( 110 |
No hay productos añadidos
111 | )} 112 |
113 |
114 |

Payment

115 |
116 |
117 |
118 |
119 |

Orden Summary

120 |
121 | {products.map((product: Product) => ( 122 |
123 |
124 |
125 |
126 | 132 |
133 |
134 |
{product.name}
135 |

{product.description}

136 |
137 |
138 |

139 | $ {product.price.toFixed(2)} 140 |

141 | 147 |
148 |
149 |
150 |
151 | ))} 152 |
153 |
154 |
155 |
156 |

Productos

157 |
158 |
164 |

{nItem}

165 |
166 |
167 |

Costo de envio

168 |
169 |
175 |

0.00

176 |
177 |
178 |

Total

179 |
180 |
186 | {" "} 187 |

$ {total.toFixed(2)}

188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | 196 | ); 197 | } 198 | --------------------------------------------------------------------------------