version 1 ready
This commit is contained in:
parent
3cc37c2d34
commit
08faec5e16
5 changed files with 539 additions and 0 deletions
193
.gitignore
vendored
Normal file
193
.gitignore
vendored
Normal 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
20
Makefile
Normal 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
43
README.md
Normal 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
86
index.html
Normal 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">€</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
197
js/index.js
Normal 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 = "✅";
|
||||
if (document.querySelector("#name p")) {
|
||||
document.querySelector("#name p").remove();
|
||||
}
|
||||
|
||||
regenerate();
|
||||
} else {
|
||||
target.classList.add("is-danger")
|
||||
document.querySelector("#name div span").innerHTML = "❌";
|
||||
|
||||
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 = "✅";
|
||||
if (document.querySelector("#bic p")) {
|
||||
document.querySelector("#bic p").remove();
|
||||
}
|
||||
|
||||
regenerate();
|
||||
} else {
|
||||
target.classList.add("is-danger")
|
||||
document.querySelector("#bic div span").innerHTML = "❌";
|
||||
|
||||
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 = "✅";
|
||||
if (document.querySelector("#iban p")) {
|
||||
document.querySelector("#iban p").remove();
|
||||
}
|
||||
|
||||
regenerate();
|
||||
} else {
|
||||
target.classList.add("is-danger")
|
||||
document.querySelector("#iban div span").innerHTML = "❌";
|
||||
|
||||
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 = "✅";
|
||||
if (document.querySelector("#purpose p")) {
|
||||
document.querySelector("#purpose p").remove();
|
||||
}
|
||||
|
||||
regenerate();
|
||||
} else {
|
||||
target.classList.add("is-danger")
|
||||
document.querySelector("#name div span").innerHTML = "❌";
|
||||
|
||||
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();
|
||||
Loading…
Reference in a new issue