Merge pull request #3 from liftlearning/pix-qr-code

QR Code PIX
This commit is contained in:
Ronyell Henrique 2022-11-10 18:57:10 -03:00 committed by GitHub
commit 99add99af0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2625 additions and 2019 deletions

View File

@ -13,13 +13,13 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16.x node-version: 16.x
cache: 'npm' cache: 'yarn'
- name: 🏗 Install dependencies - name: 🏗 Install dependencies
run: npm ci run: yarn
- name: 📦 Lint with eslint - name: 📦 Lint with eslint
run: npm run lint run: yarn lint
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -2,9 +2,9 @@ FROM node:lts-alpine
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package.json yarn.lock ./
RUN npm install RUN yarn
COPY ./ ./ COPY ./ ./
EXPOSE 3000 EXPOSE 3000
CMD ["npm", "run", "start"] CMD ["yarn", "start"]

View File

@ -14,12 +14,16 @@
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.3", "@headlessui/vue": "^1.7.3",
"@heroicons/vue": "^2.0.12", "@heroicons/vue": "^2.0.12",
"crc": "^3.8.0",
"qrcode": "^1.5.1",
"vue": "^3.2.41", "vue": "^3.2.41",
"vue-router": "^4.1.5" "vue-router": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@types/crc": "^3.8.0",
"@types/node": "^16.11.68", "@types/node": "^16.11.68",
"@types/qrcode": "^1.5.0",
"@vitejs/plugin-vue": "^3.1.2", "@vitejs/plugin-vue": "^3.1.2",
"@vitejs/plugin-vue-jsx": "^2.0.1", "@vitejs/plugin-vue-jsx": "^2.0.1",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",

View File

@ -15,10 +15,10 @@ import HelloWorld from "./components/HelloWorld.vue";
<div class="wrapper"> <div class="wrapper">
<HelloWorld msg="You did it!" /> <HelloWorld msg="You did it!" />
<nav> <nav>
<RouterLink to="/">Home</RouterLink> <RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink> <RouterLink to="/about">About</RouterLink>
<RouterLink to="/pix">QrCode Pix</RouterLink>
</nav> </nav>
</div> </div>
</header> </header>

View File

@ -26,6 +26,7 @@
--color-background: var(--vt-c-white); --color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft); --color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute); --color-background-mute: var(--vt-c-white-mute);
--color-background-indigo: var(--vt-c-indigo);
--color-border: var(--vt-c-divider-light-2); --color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1); --color-border-hover: var(--vt-c-divider-light-1);

View File

@ -17,6 +17,11 @@ const router = createRouter({
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => import("../views/AboutView.vue"), component: () => import("../views/AboutView.vue"),
}, },
{
path: "/pix",
name: "pix",
component: () => import("../views/QrCodeForm.vue"),
},
], ],
}); });

87
src/utils/QrCodePix.ts Normal file
View File

@ -0,0 +1,87 @@
import qrcode from "qrcode";
import type { QRCodeToDataURLOptions } from "qrcode";
import { crc16ccitt } from "crc";
interface PixParams {
pixKey: string;
merchantCity?: string;
merchantName?: string;
value?: number;
transactionId?: string;
message?: string;
cep?: string;
currency?: number;
countryCode?: string;
}
const pix = ({
pixKey,
merchantCity = "city",
merchantName = "name",
value,
message,
cep,
transactionId = "***",
currency = 986,
countryCode = "BR",
}: PixParams) => {
const payloadKeyString = generatePixKey(pixKey, message);
const payload: string[] = [
formatEMV("00", "01"), //Payload Format Indicator
formatEMV("26", payloadKeyString), // Merchant Account Information
formatEMV("52", "0000"), //Merchant Category Code
formatEMV("53", String(currency)), // Transaction Currency
];
if (String(value) === "0") {
value = undefined;
}
if (value) {
payload.push(formatEMV("54", value.toFixed(2)));
}
payload.push(formatEMV("58", countryCode.toUpperCase())); // Country Code
payload.push(formatEMV("59", merchantName)); // Merchant Name
payload.push(formatEMV("60", merchantCity)); // Merchant City
if (cep) {
payload.push(formatEMV("61", cep)); // Postal Code
}
payload.push(formatEMV("62", formatEMV("05", transactionId))); // Additional Data Field Template
payload.push("6304");
const stringPayload = payload.join("");
const crcResult = crc16ccitt(stringPayload)
.toString(16)
.toUpperCase()
.padStart(4, "0");
const payloadPIX = `${stringPayload}${crcResult}`;
return {
payload: (): string => payloadPIX,
base64QrCode: (options?: QRCodeToDataURLOptions): Promise<string> =>
qrcode.toDataURL(payloadPIX, options),
};
};
const generatePixKey = (pixKey: string, message?: string): string => {
const payload: string[] = [
formatEMV("00", "BR.GOV.BCB.PIX"),
formatEMV("01", pixKey),
];
if (message) {
payload.push(formatEMV("02", message));
}
return payload.join("");
};
const formatEMV = (id: string, param: string): string => {
const len = param.length.toString().padStart(2, "0");
return `${id}${len}${param}`;
};
export { type PixParams, pix };

216
src/views/QrCodeForm.vue Normal file
View File

@ -0,0 +1,216 @@
<script setup lang="ts">
import { pix } from "../utils/QrCodePix";
import { ref } from "vue";
const pixModel = ref({
pixKey: "",
name: "",
city: "",
transactionId: "",
value: 0,
message: "",
qrCodePayload: "",
});
const qrCode = ref<string>("");
const qrCodePayload = ref<string>("");
const toggleModal = ref<boolean>(false);
const errors = ref({
pixRequiredError: false,
nameRequiredError: false,
cityRequiredError: false,
});
const submit = () => {
errors.value["pixRequiredError"] = pixModel.value["pixKey"] == "";
if (errors.value["pixRequiredError"]) return;
const pixQrCode = pix({
pixKey: pixModel.value.pixKey,
merchantName: pixModel.value.name.trim()
? pixModel.value.name
: "name",
merchantCity: pixModel.value.city.trim()
? pixModel.value.city
: "city",
transactionId: pixModel.value.transactionId.trim()
? pixModel.value.transactionId
: "***",
message: pixModel.value.message,
value: pixModel.value["value"],
});
pixQrCode.base64QrCode().then((code: string) => {
qrCode.value = code;
});
qrCodePayload.value = pixQrCode.payload();
toggleModal.value = true;
};
</script>
<template>
<div class="container">
<h2 class="text-center font-bold text-emerald-50 text-2xl">
pixModel QR Code
</h2>
<form>
<div class="grid gap-4 grid-cols-1 p-2">
<div class="col-div">
<div class="mb-2">
<label for="pixKey" class="form-label">Chave PIX</label>
<span v-if="errors['pixRequiredError']" class="required-error"
>(Esse campo é obrigatório)</span
>
</div>
<input
type="text"
name="pixKey"
id="pixKey"
class="form-input"
v-model="pixModel.pixKey"
/>
</div>
<div class="col-div">
<div class="mb-2">
<label for="name" class="form-label">Nome do beneficiário</label>
<span v-if="errors['nameRequiredError']" class="required-error"
>(Esse campo é obrigatório)</span
>
</div>
<input
type="text"
name="name"
id="name"
class="form-input"
v-model="pixModel.name"
/>
</div>
<div class="col-div">
<div class="mb-2">
<label for="city" class="form-label">Cidade do beneficiário</label>
<span v-if="errors['cityRequiredError']" class="required-error"
>(Esse campo é obrigatório)</span
>
</div>
<input
type="text"
name="city"
id="city"
class="form-input"
v-model="pixModel.city"
/>
</div>
<div class="col-div">
<label for="value" class="form-label"
>Valor de transferência (Opcional)</label
>
<input
type="number"
name="value"
id="value"
class="form-input"
v-model="pixModel.value"
/>
</div>
<div class="col-div">
<label for="code" class="form-label"
>Código da transferência (Opcional)</label
>
<input
type="text"
name="code"
id="code"
class="form-input"
v-model="pixModel.transactionId"
/>
</div>
<div class="col-div">
<label for="message" class="form-label">Mensagem (Opcional)</label>
<input
type="text"
name="message"
id="message"
class="form-input"
v-model="pixModel.message"
/>
</div>
<button type="button" class="button" @click="submit">
Gerar QR code
</button>
</div>
</form>
<div
v-if="toggleModal"
class="fixed overflow-x-hidden overflow-y-auto inset-0 flex justify-center items-center z-50"
>
<div class="relative mx-auto w-auto max-w-2xl">
<div
class="bg-white w-[500px] p-2 rounded shadow-2xl flex flex-col justify-center items-center gap-2"
>
<img v-if="qrCode != ''" :src="qrCode" alt="QR code image" />
<div>
<span class="text-black font-semibold mr-1">Chave pix:</span>
<span class="text-gray-700">{{ pixModel.pixKey }}</span>
</div>
<div v-if="pixModel.name.trim() != ''">
<span class="text-black font-semibold mr-1"
>Nome do Beneficiário:</span
>
<span class="text-gray-700">{{ pixModel.name }}</span>
</div>
<div v-if="pixModel.value != 0">
<span class="text-black font-semibold mr-1"
>Valor da transferência:</span
>
<span class="text-gray-700">{{ pixModel.value }}</span>
</div>
<div
class="flex flex-col w-auto break-all justify-center items-center"
>
<span class="text-black font-semibold mb-2">Código QR Code:</span>
<span class="text-gray-700">{{ qrCodePayload }}</span>
</div>
<button type="button" class="button" @click="toggleModal = false">
Fechar
</button>
</div>
</div>
</div>
<div
v-if="toggleModal"
class="fixed z-40 inset-0 opacity-25 bg-black"
></div>
</div>
</template>
<style scoped>
.container {
background-color: var(--color-background-indigo);
@apply rounded-md p-2 mt-8;
}
.col-div {
@apply flex flex-col;
}
.form-input {
@apply rounded-lg border border-gray-200 p-2 text-black;
}
.form-label {
@apply font-semibold tracking-wide text-emerald-50;
}
.button {
@apply rounded-lg w-full border border-emerald-900 text-white py-2 bg-emerald-600 hover:bg-emerald-500;
}
.required-error {
@apply ml-2 text-red-500;
}
</style>

4317
yarn.lock

File diff suppressed because it is too large Load Diff