version 1 ready

This commit is contained in:
Ricardo (XenGi) Band 2025-01-22 15:12:57 +01:00
commit 08faec5e16
No known key found for this signature in database
5 changed files with 539 additions and 0 deletions

193
.gitignore vendored Normal file
View file

@ -0,0 +1,193 @@
css/
js/qr-creator.js
js/qr-creator.min.js
# Created by https://www.toptal.com/developers/gitignore/api/linux,windows,macos,direnv,vim,webstorm+all
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,windows,macos,direnv,vim,webstorm+all
### direnv ###
.direnv
.envrc
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### WebStorm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### WebStorm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/linux,windows,macos,direnv,vim,webstorm+all

20
Makefile Normal file
View file

@ -0,0 +1,20 @@
.PHONY: bulma
bulma:
curl -Ls https://github.com/jgthms/bulma/releases/download/1.0.2/bulma-1.0.2.zip -o bulma.zip
unzip bulma.zip
mkdir -p css
js/qr-create.min.js:
curl -Ls https://cdn.jsdelivr.net/npm/qr-creator/dist/qr-creator.min.js -o js/qr-creator.min.js
js/qr-create.js:
curl -Ls https://raw.githubusercontent.com/nimiq/qr-creator/refs/heads/master/src/qr-creator.js -o js/qr-creator.js
css/bulma.css: bulma
mv bulma/css/bulma.css css/bulma.css
css/bulma.css.map: bulma
mv bulma/css/bulma.css.map css/bulma.css.map
css/bulma.min.css: bulma
mv bulma/css/bulma.min.css css/bulma.min.css

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# BezahlBrudi
In an effort to reduce the usage of Paypal, I created this simple web app. It can generate GiroCodes that can be used to easily transfer money between friends by showing a QR code that can be scanned by most banking apps. It will import all neccessary infos for a normal SEPA bank transfer. No third parties involved, only the banks of the friends who want to exchange money.
# How to use
Simple visit the webpage, and put in the details. The QR code will be generated automatically. You can choose to save your details in a cookie, so you don't have to input your bank details everytime. The cookie will be store on your device.
# Data privacy
The web app doesn't store any data of the sender, the receiver or the transaction. Everything runs locally in your browser.
# Notes
## Content of a Girocode
| Line | Example | Note |
| ---- | ----------- | ---- |
| 1 | BCD | Service Tag |
| 2 | 002 | Version (001 oder 002) |
| 3 | 2 | Encoding (1 = UTF-8, 2 = ISO 8859-1, 3 = ISO 8859-2, 4 = ISO 8859-4, 5 = ISO 8859-5, 6 = ISO 8859-7, 7 = ISO 8859-10, 8 = ISO 8859-15) |
| 4 | SCT | SEPA Credit Transfer |
| 5 | XXXXXXXXXXX | BIC of receiver bank (optional in EWR) |
| 6 | Max Muster | Name of receiver (max 70 chars) |
| 7 | DEXXXXXXXXXXXXXXXXXXXX | IBAN of receiver |
| 8 | EUR13.37 | Amount (Format `EUR#.##`) |
| 9 | CHAR | Reason (4 letter code, optional) |
| 10 | RF366 | Reference (structured code (ISO 11649 RF Creditor Reference) |
| 11 | Pizza | Purpose (unstructured text, max 140 chars) |
| 12 | Foobar | Note to user (optional) |
_The app will skip lines 9, 10 and 12._
## Valid chars for SEPA "Verwendungszweck"
```
a-zA-Z0-9/?:\(\).,'\+-[:space:]
```
---
Build with ❤️ and 🍝.

86
index.html Normal file
View file

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BezahlBrudi</title>
<link rel="stylesheet" href="css/bulma.min.css">
<script src="js/qr-creator.min.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">BezahlBrudi</h1>
<p class="subtitle">Transfer money without any 3rd parties</p>
<p>The QR code can be scanned by most banking apps. Give it a try!</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="columns">
<div class="column">
<div id="name" class="field">
<label for="input-name" class="label">Receiver Name</label>
<div class="control has-icons-right">
<input id="input-name" class="input" type="text" placeholder="Receiver Name" value="" maxlength="70" required>
<span class="icon is-small is-right"></span>
</div>
</div>
<div id="iban" class="field">
<label for="input-iban" class="label">Receiver IBAN</label>
<div class="control has-icons-right">
<input id="input-iban" class="input" type="text" placeholder="Receiver IBAN" value="" required>
<span class="icon is-small is-right"></span>
</div>
</div>
<div id="bic" class="field">
<label for="input-bic" class="label">Receiver BIC</label>
<div class="control has-icons-right">
<input id="input-bic" class="input" type="text" placeholder="Receiver BIC" value="" required>
<span class="icon is-small is-right"></span>
</div>
</div>
<div id="amount" class="field">
<label for="input-amount" class="label">Amount</label>
<div class="control has-icons-left has-icons-right">
<input id="input-amount" class="input" type="number" min="0.00" step="0.01" value="0.00" pattern="[0-9]+.[0-9]{2}" required>
<span class="icon is-small is-left">&euro;</span>
<span class="icon is-small is-right"></span>
</div>
</div>
<div id="purpose" class="field">
<label for="input-purpose" class="label">Purpose</label>
<div class="control has-icons-right">
<input id="input-purpose" class="input" type="text" placeholder="Purpose" value="" maxlength="140" required>
<span class="icon is-small is-right"></span>
</div>
</div>
<div class="field">
<label class="checkbox">
<input id="rememberme" type="checkbox" />
Remember me
</label>
</div>
</div>
<div class="column">
<figure class="image is-square">
<div id="qr-code"></div>
</figure>
</div>
</div>
</div>
</section>
<script src="js/index.js"></script>
</body>
</html>

197
js/index.js Normal file
View file

@ -0,0 +1,197 @@
getCookie = (key) => {
let name = key + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
setCookie = () => {
const d = new Date();
d.setTime(d.getTime() + (180*24*60*60*1000)); // 180 days
document.cookie = `name=${document.querySelector("#name input").value};iban=${document.querySelector("#iban input").value};bic=${document.querySelector("#bic input").value};expires=${d.toUTCString()};path=/`;
}
// Fill in form from cookie
const cookieName = getCookie("name");
if (cookieName !== "") {
document.querySelector("#name input").value = cookieName;
}
const cookieIban = getCookie("iban");
if (cookieIban !== "") {
document.querySelector("#iban input").value = cookieIban;
}
const cookieBic = getCookie("bic");
if (cookieBic !== "") {
document.querySelector("#bic input").value = cookieBic;
}
// Field data checks
validateName = (target) => {
if (target.value.length > 0 && target.value.length <= 70) {
target.classList.remove("is-danger")
document.querySelector("#name div span").innerHTML = "&#x2705;";
if (document.querySelector("#name p")) {
document.querySelector("#name p").remove();
}
regenerate();
} else {
target.classList.add("is-danger")
document.querySelector("#name div span").innerHTML = "&#x274C;";
if (!document.querySelector("#name p")) {
const help = document.createElement("p");
help.classList.add("help", "is-danger");
help.textContent = "Name can only have 70 characters.";
document.querySelector("#name").appendChild(help);
}
}
}
validateBic = (target) => {
const re = /^[a-z]{6}[2-9a-z][0-9a-np-z]([a-z0-9]{3}|x{3})?$/i;
if (re.test(target.value)) {
target.classList.remove("is-danger")
document.querySelector("#bic div span").innerHTML = "&#x2705;";
if (document.querySelector("#bic p")) {
document.querySelector("#bic p").remove();
}
regenerate();
} else {
target.classList.add("is-danger")
document.querySelector("#bic div span").innerHTML = "&#x274C;";
if (!document.querySelector("#bic p")) {
const help = document.createElement("p");
help.classList.add("help", "is-danger");
help.textContent = "BIC is invalid";
document.querySelector("#bic").appendChild(help);
}
}
}
validateIban = (target) => {
// IBANs are fucking complicated. this is a hugely simplified check
const re = /^[a-z]{2}[0-9]{20}$/i;
if (re.test(target.value)) {
target.classList.remove("is-danger")
document.querySelector("#iban div span").innerHTML = "&#x2705;";
if (document.querySelector("#iban p")) {
document.querySelector("#iban p").remove();
}
regenerate();
} else {
target.classList.add("is-danger")
document.querySelector("#iban div span").innerHTML = "&#x274C;";
if (!document.querySelector("#iban p")) {
const help = document.createElement("p");
help.classList.add("help", "is-danger");
help.textContent = "IBAN is invalid";
document.querySelector("#iban").appendChild(help);
}
}
}
validatePurpose = (target) => {
if (target.value.length > 0 && target.value.length <= 140) {
target.classList.remove("is-danger")
document.querySelector("#purpose div span").innerHTML = "&#x2705;";
if (document.querySelector("#purpose p")) {
document.querySelector("#purpose p").remove();
}
regenerate();
} else {
target.classList.add("is-danger")
document.querySelector("#name div span").innerHTML = "&#x274C;";
if (!document.querySelector("#purpose p")) {
const help = document.createElement("p");
help.classList.add("help", "is-danger");
help.textContent = "Purpose can only have 140 characters.";
document.querySelector("#purpose").appendChild(help);
}
}
}
document.querySelector("#name input").addEventListener("input", (event) => {
validateName(event.target);
});
document.querySelector("#iban input").addEventListener("input", (event) => {
validateIban(event.target);
});
document.querySelector("#bic input").addEventListener("input", (event) => {
validateBic(event.target);
});
document.querySelector("#purpose input").addEventListener("input", (event) => {
validatePurpose(event.target);
});
// Remember inputs
document.querySelector("#rememberme").addEventListener("change", (event) => {
if (event.target.checked) {
console.log("Save Name, IBAN and BIC to cookie..");
setCookie();
} else {
console.log("Remove Cookie..");
}
})
// qr code generator
regenerate = () => {
if (document.querySelector("#name input").classList.contains("is-danger")
|| document.querySelector("#iban input").classList.contains("is-danger")
|| document.querySelector("#bic input").classList.contains("is-danger")) {
console.log("Error in inputs")
} else {
document.querySelector('#qr-code').innerHTML = "";
QrCreator.render({
text: `BCD
002
1
SCT
${document.querySelector("#bic input").value.toUpperCase()}
${document.querySelector("#name input").value}
${document.querySelector("#iban input").value.toUpperCase()}
EUR${document.querySelector("#amount input").value}
${document.querySelector("#purpose input").value}
`,
radius: 0.25,
ecLevel: "H",
fill: "#fff",
background: null,
size: 512
}, document.querySelector('#qr-code'));
if (document.querySelector("#rememberme").checked) {
setCookie()
}
}
}
// generate qr code on first visit
validateName(document.querySelector("#name input"));
validateIban(document.querySelector("#iban input"));
validateBic(document.querySelector("#bic input"));
validatePurpose(document.querySelector("#purpose input"));
regenerate();