├── 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 |
158 |
166 |
167 |
Costo de envio
168 |
169 |
177 |
180 |
186 | {" "}
187 |
$ {total.toFixed(2)}
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | >
196 | );
197 | }
198 |
--------------------------------------------------------------------------------