From 3b32f8d0caa5996bf95ece48131761b06610fed1 Mon Sep 17 00:00:00 2001 From: Dominik Hebeler <dominik@suma-ev.de> Date: Wed, 9 Nov 2022 16:11:27 +0100 Subject: [PATCH] basic paypal checkout implementation (sandbox) --- pass/app.js | 2 + pass/package-lock.json | 63 +++++++++++++++---- pass/package.json | 4 +- pass/public/js/index.js | 35 ++++++----- pass/public/styles/checkout.css | 2 +- pass/public/styles/checkout.less | 8 ++- pass/resources/js/checkout.js | 73 ++++++++++++++++++++-- pass/routes/checkout/checkout.js | 37 +++++------- pass/routes/checkout/paypal.js | 100 +++++++++++++++++++++++++++++++ pass/views/checkout/checkout.ejs | 46 +++++++------- pass/views/index.ejs | 5 +- 11 files changed, 296 insertions(+), 79 deletions(-) create mode 100644 pass/routes/checkout/paypal.js diff --git a/pass/app.js b/pass/app.js index ddb0596..8d67af5 100644 --- a/pass/app.js +++ b/pass/app.js @@ -8,6 +8,7 @@ var logger = require("morgan"); var indexRouter = require("./routes/index"); var checkoutRouter = require("./routes/checkout/checkout"); +var paypalRouter = require("./routes/checkout/paypal.js"); var app = express(); @@ -24,6 +25,7 @@ app.use(express.static(path.join(__dirname, "public"))); app.use("/", indexRouter); app.use("/checkout", checkoutRouter); +app.use("/payment/paypal", paypalRouter); // Browserified Javascript files app.get( diff --git a/pass/package-lock.json b/pass/package-lock.json index 5defdb3..fa4a82d 100644 --- a/pass/package-lock.json +++ b/pass/package-lock.json @@ -8,6 +8,7 @@ "name": "pass", "version": "0.0.0", "dependencies": { + "@paypal/paypal-js": "^5.1.1", "blind-signatures": "^1.0.7", "browserify-middleware": "^8.1.1", "config": "^3.3.8", @@ -21,7 +22,8 @@ "ioredis": "^5.2.4", "less-middleware": "~2.2.1", "morgan": "~1.9.1", - "node-forge": "^1.3.1" + "node-forge": "^1.3.1", + "uuid": "^9.0.0" }, "devDependencies": { "nodemon": "^2.0.20" @@ -32,6 +34,14 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, + "node_modules/@paypal/paypal-js": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-5.1.1.tgz", + "integrity": "sha512-MMQ8TA048gTB43pzEOMzod8WY8hfzy+ahd7w29LtMvXduqzp7/29WxrTlsy4k6ARG6WGJ/uGqpc4+la4UZEQgw==", + "dependencies": { + "promise-polyfill": "^8.2.3" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3226,6 +3236,11 @@ "asap": "~2.0.3" } }, + "node_modules/promise-polyfill": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3474,6 +3489,16 @@ "node": ">=0.6" } }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -4509,13 +4534,11 @@ } }, "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "optional": true, + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/validator": { @@ -4798,6 +4821,14 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, + "@paypal/paypal-js": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-5.1.1.tgz", + "integrity": "sha512-MMQ8TA048gTB43pzEOMzod8WY8hfzy+ahd7w29LtMvXduqzp7/29WxrTlsy4k6ARG6WGJ/uGqpc4+la4UZEQgw==", + "requires": { + "promise-polyfill": "^8.2.3" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -7354,6 +7385,11 @@ "asap": "~2.0.3" } }, + "promise-polyfill": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7561,6 +7597,12 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.1.tgz", "integrity": "sha512-LQy1Q1fcva/UsnP/6Iaa4lVeM49WiOitu2T4hZCyA/elLKu37L99qcBJk4VCCk+rdLvnMzfKyiN3SZTqdAZGSQ==", "optional": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "optional": true } } }, @@ -8391,10 +8433,9 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" }, "validator": { "version": "13.7.0", diff --git a/pass/package.json b/pass/package.json index 5196544..fac9384 100644 --- a/pass/package.json +++ b/pass/package.json @@ -7,6 +7,7 @@ "dev": "nodemon --exec node ./bin/www" }, "dependencies": { + "@paypal/paypal-js": "^5.1.1", "blind-signatures": "^1.0.7", "browserify-middleware": "^8.1.1", "config": "^3.3.8", @@ -20,7 +21,8 @@ "ioredis": "^5.2.4", "less-middleware": "~2.2.1", "morgan": "~1.9.1", - "node-forge": "^1.3.1" + "node-forge": "^1.3.1", + "uuid": "^9.0.0" }, "devDependencies": { "nodemon": "^2.0.20" diff --git a/pass/public/js/index.js b/pass/public/js/index.js index a62ba01..cf14611 100644 --- a/pass/public/js/index.js +++ b/pass/public/js/index.js @@ -1,19 +1,22 @@ -const default_searches = 250; -const default_price = 3; +const default_searches = 300; +const default_price = 4; const default_estimate_months = 1; -document.getElementById("multiplier").addEventListener("input", multiplierChanged); -multiplierChanged() +document.getElementById("amount").addEventListener("input", multiplierChanged); +multiplierChanged(); -function multiplierChanged(){ - let multiplier = document.getElementById("multiplier").value; - let searches_element = document.querySelector("#offers > .offer.default > h1"); - let estimate_element = document.querySelector("#offers > .offer.default > p.hint > span"); - let price_element = document.querySelector("#offers > .offer.default > button.select"); - let amount_form_element = document.querySelector("#offers > .offer.default > input[name=amount]"); - searches_element.textContent = default_searches * multiplier; - price_element.textContent = (default_price * multiplier) + " €"; - estimate_element.textContent = default_estimate_months * multiplier; - amount_form_element.value = default_searches * multiplier; - -} \ No newline at end of file +function multiplierChanged() { + let multiplier = document.getElementById("amount").value; + let searches_element = document.querySelector( + "#offers > .offer.default > h1" + ); + let estimate_element = document.querySelector( + "#offers > .offer.default > p.hint > span" + ); + let price_element = document.querySelector( + "#offers > .offer.default > button.select" + ); + searches_element.textContent = default_searches * multiplier; + price_element.textContent = default_price * multiplier + " €"; + estimate_element.textContent = default_estimate_months * multiplier; +} diff --git a/pass/public/styles/checkout.css b/pass/public/styles/checkout.css index f64239c..fa00378 100644 --- a/pass/public/styles/checkout.css +++ b/pass/public/styles/checkout.css @@ -1 +1 @@ -#payment-container{width:max-content;border:1px solid #777;border-radius:.2rem}#payment-container>#heading{border:1px solid #777;background-color:#eaeaea;padding:.5rem}#payment-container>#payment-provider-container{display:flex;align-items:center}#payment-container>#payment-provider-container>#payment-providers{border-right:1px solid #777;padding:.5rem}#payment-container>#payment-provider-container>#payment-providers>h1{margin:0;font-size:.7rem;font-weight:normal}#payment-container>#payment-provider-container>#payment-providers>ul{list-style-type:none;margin:0;padding:0}#payment-container>#payment-provider-container>#payment-providers>ul>li{cursor:pointer;padding:.5rem 1rem;background-color:#f5f5f5}#payment-container>#payment-provider-container>#payment-providers>ul>li[data-active="true"]{background-color:orange;color:white;font-weight:bold;-webkit-text-stroke:#777 .01rem}#payment-container>#payment-provider-container>#payment-providers>ul>li:not(:last-child){border-bottom:1px dashed #777}#payment-container>.step{border:1px solid #777;padding:1rem;color:rgba(120,120,120,0.314)}#payment-container>.step>.section-heading{display:flex;gap:.5rem}#payment-container>.step>.section-heading>.status{width:1rem}#payment-container>.step>.section-heading>.status>*{display:none}#payment-container>.step>.section-heading>.content{flex-grow:1}#payment-container>.step>.section-heading>.location{text-align:right;border-left:1px solid rgba(120,120,120,0.314);padding-left:.6rem}#payment-container>.step.current{color:inherit}#payment-container>.step.current .loading{display:block}#payment-container>.step.current .location{color:#777;border-color:#777}#payment-container>.step.finished .finished{display:block} \ No newline at end of file +#payment-container{width:max-content;border:1px solid #777;border-radius:.2rem}#payment-container>#heading{border:1px solid #777;background-color:#eaeaea;padding:.5rem}#payment-container #payment-provider-container{display:flex;align-items:center}#payment-container #payment-provider-container>#payment-providers{border-right:1px solid #777;padding:.5rem}#payment-container #payment-provider-container>#payment-providers>h1{margin:0;font-size:.7rem;font-weight:normal}#payment-container #payment-provider-container>#payment-providers>ul{list-style-type:none;margin:0;padding:0}#payment-container #payment-provider-container>#payment-providers>ul>li{cursor:pointer;padding:.5rem 1rem;background-color:#f5f5f5}#payment-container #payment-provider-container>#payment-providers>ul>li[data-active="true"]{background-color:orange;color:white;font-weight:bold;-webkit-text-stroke:#777 .01rem}#payment-container #payment-provider-container>#payment-providers>ul>li:not(:last-child){border-bottom:1px dashed #777}#payment-container>.step{border:1px solid #777;padding:1rem;color:rgba(120,120,120,0.314)}#payment-container>.step>.section-heading{display:flex;gap:.5rem}#payment-container>.step>.section-heading>.status{width:1rem}#payment-container>.step>.section-heading>.status>*{display:none}#payment-container>.step>.section-heading>.content{flex-grow:1}#payment-container>.step>.section-heading>.location{text-align:right;border-left:1px solid rgba(120,120,120,0.314);padding-left:.6rem}#payment-container>.step>.section-body{display:none}#payment-container>.step.current{color:inherit}#payment-container>.step.current .loading{display:block}#payment-container>.step.current .location{color:#777;border-color:#777}#payment-container>.step.current>.section-body{display:block}#payment-container>.step.finished .finished{display:block} \ No newline at end of file diff --git a/pass/public/styles/checkout.less b/pass/public/styles/checkout.less index 7a84bcc..ec62288 100644 --- a/pass/public/styles/checkout.less +++ b/pass/public/styles/checkout.less @@ -9,7 +9,7 @@ padding: 0.5rem; } - > #payment-provider-container { + #payment-provider-container { display: flex; align-items: center; @@ -65,6 +65,9 @@ padding-left: 0.6rem; } } + > .section-body { + display: none; + } &.current { color: inherit; .loading { @@ -74,6 +77,9 @@ color: #777; border-color: #777; } + > .section-body { + display: block; + } } &.finished .finished { display: block; diff --git a/pass/resources/js/checkout.js b/pass/resources/js/checkout.js index 241951a..88cec47 100644 --- a/pass/resources/js/checkout.js +++ b/pass/resources/js/checkout.js @@ -1,5 +1,70 @@ -one_generate_private_key(); +const uuid_generator = require("uuid"); +const BlindSignature = require("blind-signatures"); +const BigInteger = require("jsbn").BigInteger; +const paypal_client = require("@paypal/paypal-js"); -function one_generate_private_key() { - -} \ No newline at end of file +var encrypted_sales_receipt = []; +var encrypted_sales_receipt_r = []; + +one_generate_encrypted_sales_receipt(); + +function one_generate_encrypted_sales_receipt() { + let current_step_container = document.getElementById( + "generate-sales-receipt" + ); + let next_step_container = document.getElementById("execute-payment"); + + let amount = document.getElementById("payment-container").dataset.amount; + let N = document.getElementById("payment-container").dataset.public_n; + let E = document.getElementById("payment-container").dataset.public_e; + + for (let i = 0; i < amount; i++) { + let uuid = uuid_generator.v4(); + + let { blinded, r } = BlindSignature.blind({ + message: uuid, + N: N, + E: E, + }); + encrypted_sales_receipt.push(blinded.toString()); + encrypted_sales_receipt_r.push(r.toString()); + } + + current_step_container.classList.remove("current"); + current_step_container.classList.add("finished"); + next_step_container.classList.add("current"); +} + +function two_execute_payment(provider) {} + +function execute_payment_paypal(e) { + let paypal_payment_option_button = document.getElementById( + "payment_method_paypal" + ); + + paypal_payment_option_button.dataset.active = true; + let client_id = paypal_payment_option_button.dataset.client_id; + paypal_client + .loadScript({ + "client-id": client_id, + }) + .then((paypal) => { + paypal + .Buttons({ + createOrder: (data, actions) => { + return fetch("/payment/paypal/order", {}) + .then((response) => response.json()) + .then((order) => order.id); + }, + }) + .render("#payment-information"); + }) + .catch((err) => { + // ToDo Handle error + console.error("failed to load the PayPal JS SDK script", err); + }); +} + +document + .getElementById("payment_method_paypal") + .addEventListener("click", execute_payment_paypal); diff --git a/pass/routes/checkout/checkout.js b/pass/routes/checkout/checkout.js index 9e04a5c..0c59f53 100644 --- a/pass/routes/checkout/checkout.js +++ b/pass/routes/checkout/checkout.js @@ -1,8 +1,8 @@ const BlindSignature = require("blind-signatures"); const Crypto = require("../../app/Crypto.js"); +const config = require("config"); const price_for_100 = 1; -const price_for_250 = 3; var express = require("express"); var router = express.Router(); @@ -12,17 +12,8 @@ const { query, validationResult } = require("express-validator"); router.get( "/", query("amount") - .isInt({ min: 100, max: 1000 }) - .withMessage("Amount needs to be between 100 and 1000."), - query("amount") - .toInt() - .custom((value, { req }) => { - if (value !== 100 && value % 250 !== 0) { - throw new Error("Amount needs to be either 100 or divisble by 250."); - } - return true; - }) - .withMessage("Amount needs to be either 100 or divisble by 250."), + .isInt({ min: 1, max: 4 }) + .withMessage("Amount needs to be between 1 and 4."), function (req, res, next) { const errors = validationResult(req); if (!errors.isEmpty()) { @@ -31,22 +22,24 @@ router.get( let params = { amount: req.query.amount, - price: - req.query.amount === 100 - ? price_for_100 - : price_for_250 * (req.query.amount / 250), + unit_size: 100, + price: req.query.amount * price_for_100, + payments: { + paypal: { + client_id: config.get("payments.paypal.testing.client_id"), + }, + }, }; let crypto = new Crypto(); - crypto.private_key_get_current().then((keypair) => { - console.log(keypair); + crypto.private_key_get_current().then((private_key) => { + params.crypto = { + N: private_key.n, + E: private_key.e, + }; res.render("checkout/checkout", params); }); } ); -function get_current_private_key() { - let dayjs = require("dayjs"); -} - module.exports = router; diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js new file mode 100644 index 0000000..8bfd7a4 --- /dev/null +++ b/pass/routes/checkout/paypal.js @@ -0,0 +1,100 @@ +var express = require("express"); +var router = express.Router(); + +const config = require("config"); + +const CLIENT_ID = config.get("payments.paypal.testing.client_id"); +const APP_SECRET = config.get("payments.paypal.testing.secret"); +const base = "https://api-m.sandbox.paypal.com"; + +/* GET home page. */ +router.get("/order", async (req, res, next) => { + const order = await createOrder(); + res.json(order); +}); + +module.exports = router; + +////////////////////// + +// PayPal API helpers + +////////////////////// + +// use the orders api to create an order + +async function createOrder() { + const accessToken = await generateAccessToken(); + + const url = `${base}/v2/checkout/orders`; + + const response = await fetch(url, { + method: "post", + + headers: { + "Content-Type": "application/json", + + Authorization: `Bearer ${accessToken}`, + }, + + body: JSON.stringify({ + intent: "CAPTURE", + + purchase_units: [ + { + amount: { + currency_code: "USD", + + value: "100.00", + }, + }, + ], + }), + }); + + const data = await response.json(); + + return data; +} + +// use the orders api to capture payment for an order + +async function capturePayment(orderId) { + const accessToken = await generateAccessToken(); + + const url = `${base}/v2/checkout/orders/${orderId}/capture`; + + const response = await fetch(url, { + method: "post", + + headers: { + "Content-Type": "application/json", + + Authorization: `Bearer ${accessToken}`, + }, + }); + + const data = await response.json(); + + return data; +} + +// generate an access token using client id and app secret + +async function generateAccessToken() { + const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); + + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "post", + + body: "grant_type=client_credentials", + + headers: { + Authorization: `Basic ${auth}`, + }, + }); + + const data = await response.json(); + + return data.access_token; +} diff --git a/pass/views/checkout/checkout.ejs b/pass/views/checkout/checkout.ejs index 38fdbcd..21f2b8f 100644 --- a/pass/views/checkout/checkout.ejs +++ b/pass/views/checkout/checkout.ejs @@ -1,40 +1,46 @@ <%- include('../templates/page_header', {css: ['/styles/checkout.css'], js: ['/js/checkout.js']}); %> <main> - <div id="payment-container"> - <div id="heading">Ihr Einkauf: <%- amount %> Suchanfragen für <%- price %>€</div> - <div id="generate-private-key" class="step current"> + <div id="payment-container" data-amount="<%- amount _%>" data-public_e="<%- crypto.E _%>" data-public_n="<%- crypto.N %>"> + <div id="heading">Ihr Einkauf: <%- amount * unit_size %> Suchanfragen für <%- price %>€</div> + <div id="generate-sales-receipt" class="step current"> <div class="section-heading"> <div class="status"> <img src="/images/loader.gif" alt="Loading" class="loading"> <div class="finished">✓</div> </div> - <div class="content">Privaten Schlüssel generieren</div> + <div class="content">Verschlüsselten Kaufbeleg generieren</div> <div class="location">Lokal</div> </div> + <div class="section-body"> + + </div> </div> - <div id="generate-auth-keys" class="step"> + + <div id="execute-payment" class="step"> <div class="section-heading"> <div class="status"> <img src="/images/loader.gif" alt="Loading" class="loading"> <div class="finished">✓</div> </div> - <div class="content">Auth Codes generieren</div> - <div class="location">Lokal</div> + <div class="content">Zahlung durchführen</div> + <div class="location">Extern</div> </div> - </div> - <div id="payment-provider-container"> - <div id="payment-providers"> - <h1>Zahlungsmethode</h1> - <ul> - <li data-active="true">Paypal</li> - <li>Google Pay</li> - <li>Apple Pay</li> - <li>Paysafe Card</li> - </ul> - </div> - <div id="payment-information"> - <button>Bezahlen</button> + <div class="section-body"> + <div id="payment-provider-container"> + <div id="payment-providers"> + <h1>Zahlungsmethode</h1> + <ul> + <li id="payment_method_paypal" data-active="false" data-client_id="<%- payments.paypal.client_id %>">Paypal</li> + <li>Google Pay</li> + <li>Apple Pay</li> + <li>Paysafe Card</li> + </ul> + </div> + <div id="payment-information"> + <div class="no-provider">Bitte Zahlungsmethode auswählen</div> + </div> + </div> </div> </div> </div> diff --git a/pass/views/index.ejs b/pass/views/index.ejs index e6c9556..e3640ad 100644 --- a/pass/views/index.ejs +++ b/pass/views/index.ejs @@ -4,16 +4,15 @@ </p> <div id="offers"> <form class="offer" action="/checkout"> - <input type="hidden" name="amount" value="100"> + <input type="hidden" name="amount" value="1"> <h1>100</h1> <div class="spacer"></div> <button type="submit" class="select">1€</button> </form> <form class="offer default" action="/checkout"> - <input type="hidden" name="amount" value="0"> <h1>-</h1> <p class="hint">reicht üblicherweise für <span>-</span> Monat</p> - <input form="nosubmit" type="range" name="multiplier" id="multiplier" min="1" max="4" value="1"> + <input type="range" name="amount" id="amount" min="1" max="4" value="1"> <div class="spacer"></div> <button type="submit" class="select">- €</button> </form> -- GitLab