diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8db7b5 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ad8a7d2 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d5e760 --- /dev/null +++ b/README.md @@ -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 🍝. + diff --git a/index.html b/index.html new file mode 100644 index 0000000..03306ab --- /dev/null +++ b/index.html @@ -0,0 +1,86 @@ + + + + + + + BezahlBrudi + + + + + + +
+
+

BezahlBrudi

+

Transfer money without any 3rd parties

+

The QR code can be scanned by most banking apps. Give it a try!

+
+
+ +
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + € + +
+
+ +
+ +
+ + +
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+ + + + diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..460e04b --- /dev/null +++ b/js/index.js @@ -0,0 +1,197 @@ +getCookie = (key) => { + let name = key + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(';'); + for(let i = 0; i { + 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();