diff --git a/pass/app.js b/pass/app.js index 2c31fe0cea901000c6d73064fbb05aa62f8000a1..dc812c4d50f6fec712b7be0d3db9c7a9e1039d50 100644 --- a/pass/app.js +++ b/pass/app.js @@ -36,6 +36,11 @@ app.get( browserify(path.join(__dirname, "resources", "js", "base.js")) ); +app.get( + "/js/funding_sources.js", + browserify(path.join(__dirname, "resources", "js", "funding_sources.js")) +); + app.get( "/js/checkout_paypal.js", browserify(path.join(__dirname, "resources", "js", "checkout_paypal.js")) diff --git a/pass/app/Order.js b/pass/app/Order.js index 6d7a62e29fe6f8af3862e6ec137492a88fdc7641..848fc34bacdaec08b34f046f3791701c460bc9a3 100644 --- a/pass/app/Order.js +++ b/pass/app/Order.js @@ -87,6 +87,10 @@ class Order { return this.#payment_method_link; } + setPaymentCompleted(payment_completed) { + this.#payment_completed = payment_completed; + } + isPaymentComplete() { return this.#payment_completed; } @@ -149,7 +153,17 @@ class Order { * Uncompleted Orders will be stored in Redis */ if (this.#payment_completed) { - expiration = expiration.add(Order.PURCHASE_TIME_LIMIT_MINUTES, "minute"); + let fs = require("fs"); + let order_file = path.join( + this.#order_path, + "orders", + this.#order_id.toString() + ".json" + ); + // Create directory if it does not exist + if (!fs.existsSync(path.dirname(order_file))) { + fs.mkdirSync(path.dirname(order_file), { recursive: true }); + } + return fs.writeFileSync(order_file, JSON.stringify(stored_data, null, 4)); } else { // Store Order in Redis let redis_key = Order.STORAGE_KEY_PREFIX + this.#order_id; diff --git a/pass/public/styles/base.less b/pass/public/styles/base.less index 13ba96de72279193cbdcd9a3b8cb1da617c2525b..aba02c9d9eb75d31a25e56785bc8c8ec43f74a98 100644 --- a/pass/public/styles/base.less +++ b/pass/public/styles/base.less @@ -48,7 +48,18 @@ html { } .hidden { - display: none; + display: none !important; +} + +.error { + color: #ff5858; + display: flex; + align-items: center; + &::before { + content: "!"; + font-size: 2rem; + padding: 0 0.5rem; + } } button { diff --git a/pass/public/styles/checkout.less b/pass/public/styles/checkout.less index b4ed57f8be77ce8dc5371f9e974c5cab5a9a64a8..57333ea4af5d4e783355d3ed8c8e4c785ad8234f 100644 --- a/pass/public/styles/checkout.less +++ b/pass/public/styles/checkout.less @@ -1,9 +1,3 @@ -h2 { - margin: 0 0 1rem; - text-align: center; - border-bottom: 1px solid @color-main; -} - #payment-container { width: max-content; max-width: 980px; diff --git a/pass/public/styles/key/checkout-payment.less b/pass/public/styles/key/checkout-payment.less index 2b42c2ae32b4498075ca8551346a895295d32918..28f5e31febe6f72887fea92680aa45e6a4e385ac 100644 --- a/pass/public/styles/key/checkout-payment.less +++ b/pass/public/styles/key/checkout-payment.less @@ -72,6 +72,17 @@ text-align: center; border-bottom: 1px solid @color-main; } + > #loading_paypal_funding_source { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + color: #777; + padding: 2rem 0; + img { + width: 2rem; + } + } #paypal-payment-card #paypal-card-form { @paypal-card-form-breakpoint-1: 920px; display: grid; diff --git a/pass/public/styles/key/key.css b/pass/public/styles/key/key.css index 6653a9591dd57cd32468971a14e39730fb8c6bc8..42d90aab92fd2177ab952e48e6b08ad8cd18e659 100644 --- a/pass/public/styles/key/key.css +++ b/pass/public/styles/key/key.css @@ -1 +1 @@ -#checkout-amount-template{text-decoration:none;color:inherit;display:grid;grid-template-columns:7em 3.5em;grid-template-rows:2.5em 2em;place-items:stretch}#checkout-amount-template>.checkout-amount{font-size:2rem;font-weight:bold;line-height:1;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border:1px solid #ff7f00;border-bottom:0;border-right:0}#checkout-amount-template>.checkout-duration{border-bottom-left-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #ff7f00;border-top:0;border-right:0}#checkout-amount-template>.checkout-cost{grid-row:span 2;justify-content:center;background-color:#ff7f00;color:white;font-size:1.7rem;align-items:center;border-top-right-radius:10px;border-bottom-right-radius:10px;border:1px solid #ff7f00;border-left:0;display:flex}#checkout #checkout-amount{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;justify-items:center;align-items:center}#checkout #checkout-amount.single{grid-template-columns:1fr}#checkout #checkout-amount>a{text-decoration:none;color:inherit;display:grid;grid-template-columns:7em 3.5em;grid-template-rows:2.5em 2em;place-items:stretch}#checkout #checkout-amount>a>.checkout-amount{font-size:2rem;font-weight:bold;line-height:1;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border:1px solid #ff7f00;border-bottom:0;border-right:0}#checkout #checkout-amount>a>.checkout-duration{border-bottom-left-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #ff7f00;border-top:0;border-right:0}#checkout #checkout-amount>a>.checkout-cost{grid-row:span 2;justify-content:center;background-color:#ff7f00;color:white;font-size:1.7rem;align-items:center;border-top-right-radius:10px;border-bottom-right-radius:10px;border:1px solid #ff7f00;border-left:0;display:flex}@media (max-width:800px){#checkout #checkout-amount{grid-template-columns:1fr 1fr}#checkout #checkout-amount.single{grid-template-columns:1fr}}@media (max-width:470px){#checkout #checkout-amount{grid-template-columns:1fr}#checkout #checkout-amount>a{width:100%;grid-template-columns:1fr 3em}}#payment{margin-bottom:1rem}#payment>h2{margin:0 0 1rem;text-align:center;border-bottom:1px solid #ff7f00}#payment>label[for="payment-group-paypal"]{border:1px solid #777;display:block;padding:.5rem;border-top-left-radius:5px;border-top-right-radius:5px;background-color:#f0f0f0;border-bottom-color:#ff7f00;border-bottom-width:2px}#payment>#payment-group-paypal{display:none}#payment>.payment-group{display:grid;grid-template-columns:1fr 1fr 1fr;align-items:center;justify-items:center;gap:1rem;border:1px solid #777;padding:1rem;border-top:0;place-items:stretch}#payment>.payment-group>div{width:14em}@media (max-width:930px){#payment>.payment-group{grid-template-columns:1fr 1fr}}@media (max-width:470px){#payment>.payment-group{grid-template-columns:1fr}}#payment>.payment-group>div{min-width:10em}#payment>.payment-group#paypal-payments>.funding_source{display:flex;border:1px solid #777;padding:1rem;align-items:center;justify-content:center;height:2em;border-radius:5px;gap:.5rem;color:inherit;text-decoration:none}#payment>.payment-group#paypal-payments>.funding_source img{max-width:100%;max-height:100%}#paypal-checkout>h2{margin:0 0 1rem;text-align:center;border-bottom:1px solid #ff7f00}#paypal-checkout #paypal-payment-card #paypal-card-form{display:grid;grid-template-columns:1fr 5em 9em;place-items:stretch;gap:.5rem}#paypal-checkout #paypal-payment-card #paypal-card-form>div{display:flex;flex-direction:column;gap:.5rem}#paypal-checkout #paypal-payment-card #paypal-card-form>div>label{font-weight:bold;font-size:.8rem;white-space:nowrap}#paypal-checkout #paypal-payment-card #paypal-card-form>div>div{height:2.3rem;border:1px solid #777;border-radius:5px}#paypal-checkout #paypal-payment-card #paypal-card-form>button#submit-credit-card{grid-column:span 3;padding:.5rem;justify-self:center}@media (max-width:920px){#paypal-checkout #paypal-payment-card #paypal-card-form{grid-template-columns:1fr}#paypal-checkout #paypal-payment-card #paypal-card-form>button#submit-credit-card{grid-column:auto}}main{max-width:980px;margin:0 auto;display:grid;row-gap:1rem;padding-right:1rem;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr auto auto;grid-template-areas:"qr key " "qr setting-url" "qr buttons" "amount charge"}main>#qr{grid-area:qr;justify-self:center}main #key{grid-area:key;font-size:clamp(.9rem, 4.8vw, 1.5rem);font-weight:bold;width:max-content;margin-top:11px;margin-right:1rem}main #setting-url{grid-area:setting-url;align-self:start;display:flex;padding:.5rem 1rem;border-radius:5px}main #setting-url>input{line-height:1.5;padding:.1rem .5rem;border-radius:5px;flex-grow:1}main>#buttons{grid-area:buttons;display:flex;gap:1rem;margin-bottom:17px;justify-content:flex-end}@media (max-width:435px){main>#buttons{display:grid;grid-template-columns:1fr}main>#buttons .button{width:auto}main>#buttons>*{display:grid;justify-items:center;text-align:center}}main>#amount{grid-area:amount;max-width:200px;justify-self:center;display:flex;flex-direction:column;align-items:center}main>#amount>h3{font-size:1.5rem;margin:0}main>#amount>div.amount{font-size:3rem}main>#charge{grid-area:charge}main>#charge>#store>p{line-height:1.5}@media (max-width:770px){main{padding:0 1rem;grid-template-rows:auto auto auto auto auto;grid-template-columns:auto auto;grid-template-areas:"amount qr " "key key" "setting-url setting-url" "buttons buttons " "charge charge "}main>#key{margin:0;justify-self:center}main>#setting-url{text-align:center}main>#buttons{justify-content:center;margin-bottom:0}main>#amount{align-self:center}main>#charge>#store{display:flex;flex-direction:column;align-items:center}}@media (max-width:430px){main{grid-template-rows:auto auto auto auto auto auto;grid-template-columns:auto;grid-template-areas:"qr" "key" "setting-url" "buttons" "amount" "charge"}}#summary>.checkout-amount{text-decoration:none;color:inherit;display:grid;grid-template-columns:7em 3.5em;grid-template-rows:2.5em 2em;place-items:stretch}#summary>.checkout-amount>.checkout-amount{font-size:2rem;font-weight:bold;line-height:1;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border:1px solid #ff7f00;border-bottom:0;border-right:0}#summary>.checkout-amount>.checkout-duration{border-bottom-left-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #ff7f00;border-top:0;border-right:0}#summary>.checkout-amount>.checkout-cost{grid-row:span 2;justify-content:center;background-color:#ff7f00;color:white;font-size:1.7rem;align-items:center;border-top-right-radius:10px;border-bottom-right-radius:10px;border:1px solid #ff7f00;border-left:0;display:flex} \ No newline at end of file +#checkout-amount-template{text-decoration:none;color:inherit;display:grid;grid-template-columns:7em 3.5em;grid-template-rows:2.5em 2em;place-items:stretch}#checkout-amount-template>.checkout-amount{font-size:2rem;font-weight:bold;line-height:1;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border:1px solid #ff7f00;border-bottom:0;border-right:0}#checkout-amount-template>.checkout-duration{border-bottom-left-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #ff7f00;border-top:0;border-right:0}#checkout-amount-template>.checkout-cost{grid-row:span 2;justify-content:center;background-color:#ff7f00;color:white;font-size:1.7rem;align-items:center;border-top-right-radius:10px;border-bottom-right-radius:10px;border:1px solid #ff7f00;border-left:0;display:flex}#checkout #checkout-amount{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;justify-items:center;align-items:center}#checkout #checkout-amount.single{grid-template-columns:1fr}#checkout #checkout-amount>a{text-decoration:none;color:inherit;display:grid;grid-template-columns:7em 3.5em;grid-template-rows:2.5em 2em;place-items:stretch}#checkout #checkout-amount>a>.checkout-amount{font-size:2rem;font-weight:bold;line-height:1;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border:1px solid #ff7f00;border-bottom:0;border-right:0}#checkout #checkout-amount>a>.checkout-duration{border-bottom-left-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #ff7f00;border-top:0;border-right:0}#checkout #checkout-amount>a>.checkout-cost{grid-row:span 2;justify-content:center;background-color:#ff7f00;color:white;font-size:1.7rem;align-items:center;border-top-right-radius:10px;border-bottom-right-radius:10px;border:1px solid #ff7f00;border-left:0;display:flex}@media (max-width:800px){#checkout #checkout-amount{grid-template-columns:1fr 1fr}#checkout #checkout-amount.single{grid-template-columns:1fr}}@media (max-width:470px){#checkout #checkout-amount{grid-template-columns:1fr}#checkout #checkout-amount>a{width:100%;grid-template-columns:1fr 3em}}#payment{margin-bottom:1rem}#payment>h2{margin:0 0 1rem;text-align:center;border-bottom:1px solid #ff7f00}#payment>label[for="payment-group-paypal"]{border:1px solid #777;display:block;padding:.5rem;border-top-left-radius:5px;border-top-right-radius:5px;background-color:#f0f0f0;border-bottom-color:#ff7f00;border-bottom-width:2px}#payment>#payment-group-paypal{display:none}#payment>.payment-group{display:grid;grid-template-columns:1fr 1fr 1fr;align-items:center;justify-items:center;gap:1rem;border:1px solid #777;padding:1rem;border-top:0;place-items:stretch}#payment>.payment-group>div{width:14em}@media (max-width:930px){#payment>.payment-group{grid-template-columns:1fr 1fr}}@media (max-width:470px){#payment>.payment-group{grid-template-columns:1fr}}#payment>.payment-group>div{min-width:10em}#payment>.payment-group#paypal-payments>.funding_source{display:flex;border:1px solid #777;padding:1rem;align-items:center;justify-content:center;height:2em;border-radius:5px;gap:.5rem;color:inherit;text-decoration:none}#payment>.payment-group#paypal-payments>.funding_source img{max-width:100%;max-height:100%}#paypal-checkout>h2{margin:0 0 1rem;text-align:center;border-bottom:1px solid #ff7f00}#paypal-checkout>#loading_paypal_funding_source{display:flex;align-items:center;justify-content:center;gap:1rem;color:#777;padding:2rem 0}#paypal-checkout>#loading_paypal_funding_source img{width:2rem}#paypal-checkout #paypal-payment-card #paypal-card-form{display:grid;grid-template-columns:1fr 5em 9em;place-items:stretch;gap:.5rem}#paypal-checkout #paypal-payment-card #paypal-card-form>div{display:flex;flex-direction:column;gap:.5rem}#paypal-checkout #paypal-payment-card #paypal-card-form>div>label{font-weight:bold;font-size:.8rem;white-space:nowrap}#paypal-checkout #paypal-payment-card #paypal-card-form>div>div{height:2.3rem;border:1px solid #777;border-radius:5px}#paypal-checkout #paypal-payment-card #paypal-card-form>button#submit-credit-card{grid-column:span 3;padding:.5rem;justify-self:center}@media (max-width:920px){#paypal-checkout #paypal-payment-card #paypal-card-form{grid-template-columns:1fr}#paypal-checkout #paypal-payment-card #paypal-card-form>button#submit-credit-card{grid-column:auto}}main{max-width:980px;margin:0 auto;display:grid;row-gap:1rem;padding-right:1rem;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr auto auto;grid-template-areas:"qr key " "qr setting-url" "qr buttons" "amount charge"}main>#qr{grid-area:qr;justify-self:center}main #key{grid-area:key;font-size:clamp(.9rem, 4.8vw, 1.5rem);font-weight:bold;width:max-content;margin-top:11px;margin-right:1rem}main #setting-url{grid-area:setting-url;align-self:start;display:flex;padding:.5rem 1rem;border-radius:5px}main #setting-url>input{line-height:1.5;padding:.1rem .5rem;border-radius:5px;flex-grow:1}main>#buttons{grid-area:buttons;display:flex;gap:1rem;margin-bottom:17px;justify-content:flex-end}@media (max-width:435px){main>#buttons{display:grid;grid-template-columns:1fr}main>#buttons .button{width:auto}main>#buttons>*{display:grid;justify-items:center;text-align:center}}main>#amount{grid-area:amount;max-width:200px;justify-self:center;display:flex;flex-direction:column;align-items:center}main>#amount>h3{font-size:1.5rem;margin:0}main>#amount>div.amount{font-size:3rem}main>#charge{grid-area:charge}main>#charge>#store>p{line-height:1.5}@media (max-width:770px){main{padding:0 1rem;grid-template-rows:auto auto auto auto auto;grid-template-columns:auto auto;grid-template-areas:"amount qr " "key key" "setting-url setting-url" "buttons buttons " "charge charge "}main>#key{margin:0;justify-self:center}main>#setting-url{text-align:center}main>#buttons{justify-content:center;margin-bottom:0}main>#amount{align-self:center}main>#charge>#store{display:flex;flex-direction:column;align-items:center}}@media (max-width:430px){main{grid-template-rows:auto auto auto auto auto auto;grid-template-columns:auto;grid-template-areas:"qr" "key" "setting-url" "buttons" "amount" "charge"}}h2{margin:0 0 1rem;text-align:center;border-bottom:1px solid #ff7f00}#summary{display:flex;align-items:center;gap:1rem;justify-content:center;margin-bottom:3rem}#summary>.checkout-amount{text-decoration:none;color:inherit;display:grid;grid-template-columns:7em 3.5em;grid-template-rows:2.5em 2em;place-items:stretch}#summary>.checkout-amount>.checkout-amount{font-size:2rem;font-weight:bold;line-height:1;display:flex;align-items:center;justify-content:center;border-top-left-radius:10px;border:1px solid #ff7f00;border-bottom:0;border-right:0}#summary>.checkout-amount>.checkout-duration{border-bottom-left-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #ff7f00;border-top:0;border-right:0}#summary>.checkout-amount>.checkout-cost{grid-row:span 2;justify-content:center;background-color:#ff7f00;color:white;font-size:1.7rem;align-items:center;border-top-right-radius:10px;border-bottom-right-radius:10px;border:1px solid #ff7f00;border-left:0;display:flex}#summary>.funding_source{height:4.5rem;width:11rem;display:grid;place-content:center;place-items:center;border:1px solid #777;border-radius:10px;color:inherit;text-decoration:none;gap:.5rem}#summary>.funding_source img{max-width:70%;max-height:100%}@media (max-width:430px){#summary{flex-direction:column}} \ No newline at end of file diff --git a/pass/public/styles/key/key.less b/pass/public/styles/key/key.less index cfe950cf78ef9e5690eca8a418d18ff325a785c5..ff0ecbd26d6b0fd7ea02661d6322eaa475c9d4de 100644 --- a/pass/public/styles/key/key.less +++ b/pass/public/styles/key/key.less @@ -136,8 +136,39 @@ main { } } +h2 { + margin: 0 0 1rem; + text-align: center; + border-bottom: 1px solid @color-main; +} + #summary { + display: flex; + align-items: center; + gap: 1rem; + justify-content: center; + margin-bottom: 3rem; > .checkout-amount { #checkout-amount-template; } + > .funding_source { + height: 4.5rem; + width: 11rem; + display: grid; + place-content: center; + place-items: center; + border: 1px solid #777; + border-radius: 10px; + color: inherit; + text-decoration: none; + gap: 0.5rem; + img { + max-width: 70%; + max-height: 100%; + } + } + + @media (max-width: 430px) { + flex-direction: column; + } } diff --git a/pass/resources/js/checkout_paypal.js b/pass/resources/js/checkout_paypal.js index c4c5a2c9f528d9c856899745b64a8847b2dec107..3ef98fd5175e013266029540c72cda8f4c89782a 100644 --- a/pass/resources/js/checkout_paypal.js +++ b/pass/resources/js/checkout_paypal.js @@ -35,8 +35,32 @@ function initialize_paypal_payments() { paypal_client .loadScript(script_data) .then((paypal) => { - console.log(paypal.HostedFields.isEligible() + "test"); + if (!paypal.isFundingEligible(funding_source)) { + // Funding with this source is not available to the user + let disabled_funding = localStorage.getItem("funding_not_eligible"); + if (disabled_funding === null) { + disabled_funding = []; + } else { + disabled_funding = JSON.parse(disabled_funding); + } + disabled_funding.push(funding_source); + localStorage.setItem( + "funding_not_eligible", + JSON.stringify(disabled_funding) + ); + document.location.href = document.querySelector( + "input[name=funding-source-not-eligible-url]" + ).value; + return; + } if (funding_source === "card") { + // Show the Form + document + .getElementById("loading_paypal_funding_source") + .classList.add("hidden"); + document + .getElementById("paypal-payment-card") + .classList.remove("hidden"); paypal.HostedFields.render({ styles: { input: { @@ -60,8 +84,19 @@ function initialize_paypal_payments() { }, createOrder: checkout_data.createOrder, onApprove: checkout_data.onApprove, + }).then((cardFields) => { + document + .getElementById("paypal-card-form") + .addEventListener("submit", (e) => { + e.preventDefault(); + cardFields.submit(); + }); }); console.log("Hosted Fields"); + } else if (funding_source === "paypal") { + paypal + .Buttons(get_paypal_checkout_data(null)) + .render("#paypal-payment-button"); } else { paypal .PaymentFields({ @@ -70,9 +105,8 @@ function initialize_paypal_payments() { fields: {}, }) .render("#paypal-payment-fields"); - paypal - .Buttons(get_paypal_checkout_data(funding_source)) - .render("#paypal-payment-button"); + let button_data = get_paypal_checkout_data(funding_source); + paypal.Buttons(button_data).render("#paypal-payment-button"); } return; console.log(paypal.FUNDING); @@ -123,7 +157,7 @@ function initialize_paypal_payments() { } function get_paypal_checkout_data(funding_source) { - return { + let checkout_data = { style: { color: "white", height: 50, @@ -131,7 +165,9 @@ function get_paypal_checkout_data(funding_source) { fundingSource: funding_source, createOrder: (data, actions) => { let checkout_paypal_create_order_url = - document.URL.replace(/#.*$/, "").replace("//+$", "") + "/order/create"; + document.querySelector( + "#paypal-checkout input[name=paypal-order-base-url]" + ).value + "/create"; return fetch(checkout_paypal_create_order_url, { method: "POST", @@ -142,40 +178,33 @@ function get_paypal_checkout_data(funding_source) { }) .then((response) => response.json()) .then((order) => { - document.getElementById("paypal-payments").dataset.order_id = + document.getElementById("paypal-checkout").dataset.order_id = order.order_id; return order.id; }); }, onCancel: () => cancelPayment( - document.getElementById("paypal-payments").dataset.order_id + document.getElementById("paypal-checkout").dataset.order_id ), onError: (err) => { console.error(err); return cancelPayment( - document.getElementById("paypal-payments").dataset.order_id + document.getElementById("paypal-checkout").dataset.order_id ); }, onApprove: (data, actions) => { - return fetch("/checkout/payment/order/paypal/capture", { + let checkout_paypal_capture_order_url = + document.querySelector( + "#paypal-checkout input[name=paypal-order-base-url]" + ).value + "/capture"; + return fetch(checkout_paypal_capture_order_url, { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", }, body: JSON.stringify({ - order_id: document.querySelector("input[name=order_id]").value, - expires_at: document.querySelector("input[name=expires_at]").value, - amount: document.querySelector("input[name=amount]").value, - unit_size: document.querySelector("input[name=unit_size]").value, - price_per_unit: document.querySelector("input[name=price_per_unit]") - .value, - public_key_n: document.querySelector("input[name=public_key_n]") - .value, - public_key_e: document.querySelector("input[name=public_key_e]") - .value, - integrity: document.querySelector("input[name=integrity]").value, - encrypted_sales_receipts: encrypted_sales_receipts, + order_id: document.getElementById("paypal-checkout").dataset.order_id, }), }) .then((response) => response.json()) @@ -193,13 +222,29 @@ function get_paypal_checkout_data(funding_source) { paypal_payment_option_button.dispatchEvent(paymentEvent); }); }, + onInit: () => { + document + .getElementById("loading_paypal_funding_source") + .classList.add("hidden"); + document + .getElementById("paypal-payment-fields") + .classList.remove("hidden"); + document + .getElementById("paypal-payment-button") + .classList.remove("hidden"); + }, }; + if (funding_source) { + checkout_data.fundingSource = funding_source; + } + return checkout_data; } function cancelPayment(order_id) { - let checkout_paypal_create_order_url = - document.URL.replace(/#.*$/, "").replace("//+$", "") + "/order/cancel"; - return fetch(checkout_paypal_create_order_url, { + let checkout_paypal_cancel_order_url = + document.querySelector("#paypal-checkout input[name=paypal-order-base-url]") + .value + "/cancel"; + return fetch(checkout_paypal_cancel_order_url, { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", diff --git a/pass/resources/js/funding_sources.js b/pass/resources/js/funding_sources.js new file mode 100644 index 0000000000000000000000000000000000000000..cd2b39e79ec5881f156a6fb32767c3ac8cf5c5d3 --- /dev/null +++ b/pass/resources/js/funding_sources.js @@ -0,0 +1,12 @@ +let funding_sources = document.querySelectorAll("#payment .funding_source"); + +let disabled_sources = localStorage.getItem("funding_not_eligible"); +if (disabled_sources !== null) { + disabled_sources = JSON.parse(disabled_sources); + disabled_sources.forEach((source) => { + let source_element = document.querySelector("#payment #" + source); + if (source_element) { + source_element.classList.add("hidden"); + } + }); +} diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js index 52bc712e0f741d4f5337665e887553bfdcb6ff5f..12498b36fa02565d5adf0ee3496af143aa32a2d2 100644 --- a/pass/routes/checkout/paypal.js +++ b/pass/routes/checkout/paypal.js @@ -27,12 +27,40 @@ router.use("/", (req, res, next) => { }); router.get("/:funding_source", async (req, res) => { + res.cookie("paypal_enabled_by_user", true, { + httpOnly: true, + sameSite: true, + }); req.data.checkout.payment.paypal.funding_source = req.params.funding_source; if (req.params.funding_source === "card") { let client_token = await generateClientToken(); req.data.checkout.payment.paypal.client_token = client_token; } + + req.data.change_url.funding_source = + "/key/" + + encodeURIComponent(req.data.key.key) + + "/checkout/" + + encodeURIComponent(req.data.checkout.amount) + + "#payment"; + + req.data.change_url.funding_source_not_eligible = + "/key/" + + encodeURIComponent(req.data.key.key) + + "/checkout/" + + encodeURIComponent(req.data.checkout.amount) + + "?error=funding_source_not_eligible"; + + req.data.change_url.order_base_url = + "/key/" + + encodeURIComponent(req.data.key.key) + + "/checkout/" + + encodeURIComponent(req.data.checkout.amount) + + "/paypal/" + + encodeURIComponent(req.data.checkout.payment.paypal.funding_source) + + "/order"; + req.data.js.push("/js/checkout_paypal.js"); res.render("key", req.data); }); @@ -65,7 +93,7 @@ router.post("/:funding_source/order/create", async (req, res) => { }); }); -router.post("/order/cancel", async (req, res) => { +router.post("/:funding_source/order/cancel", async (req, res) => { Order.LOAD_ORDER_FROM_ID(req.body.order_id) .then((order) => { return order.delete(); @@ -83,119 +111,29 @@ router.post("/order/cancel", async (req, res) => { }); // capture payment & store order information or fullfill order -router.post("/capture", async (req, res) => { +router.post("/:funding_source/order/capture", async (req, res) => { Order.LOAD_ORDER_FROM_ID(req.body.order_id).then((loaded_order) => { - loaded_order - .signOrder() - .then(() => { - let paypal_order_id = loaded_order.getPaymentMethodLink().id; - capturePayment(paypal_order_id) - .then((captureData) => { - loaded_order - .save() - .then(() => { - captureData.signatures = loaded_order.getSignatures(); - captureData.order_id = req.body.order_id; - captureData.expires_at = req.body.expires_at; - res.json(captureData); - }) - .catch((error) => { - res.status(400).json({ errors: [{ msg: error }] }); - }); + loaded_order.setPaymentCompleted(true); + let paypal_order_id = loaded_order.getPaymentMethodLink().id; + capturePayment(paypal_order_id) + .then((captureData) => { + loaded_order + .save() + .then(() => { + res.json(captureData); }) .catch((error) => { - res.status(400).json({ errors: [{ msg: error }] }); + res.status(400).json({ errors: [{ msg: error.toString() }] }); }); }) .catch((error) => { - res.status(400).json({ errors: [{ msg: error }] }); + res.status(400).json({ errors: [{ msg: error.toString() }] }); }); - //res.json(captureData); // TODO: store payment information such as the transaction ID }); }); -router.use( - "/sdk.js", - createLocaleMiddleware({ - priority: ["custom", "accept-language", "map", "default"], - default: "en-US", - lookups: { - custom: (req) => { - let ip_info = ipLocale(req.ip); - if (ip_info && ip_info.countryCode) { - let locale = clm.getLocaleByAlpha2(ip_info.countryCode); - if (locale) { - console.log(locale); - } - } - return undefined; - }, - }, - }) -); -router.get("/sdk.js", async (req, res) => { - if ( - !req.query["client-id"] || - !req.query.locale || - !req.query.currency || - !req.query.components || - !req.query["enable-funding"] - ) { - console.log("redirecting"); - let new_url = req.baseUrl + req.path + "?"; - new_url += new URLSearchParams({ - "client-id": config.get( - `payments.paypal.${process.env.NODE_ENV}.client_id` - ), - locale: req.locale.toString().replace(/-/, "_"), - currency: "EUR", - components: ["buttons", "marks", "funding-eligibility"], - "enable-funding": [ - "card", - "credit", - "venmo", - "bancontact", - "blik", - "eps", - "giropay", - "ideal", - "mercadopago", - "mybank", - "p24", - "sepa", - "sofort", - ], - }).toString(); - res.redirect(new_url); - return; - } - let sdk_url = "https://www.paypal.com/sdk/js?"; - sdk_url += new URLSearchParams({ - "client-id": req.query["client-id"], - locale: req.query.locale, - currency: req.query.currency, - components: req.query.components, - "enable-funding": req.query["enable-funding"], - }).toString(); - console.log(sdk_url); - - fetch(sdk_url) - .then((response) => response.text()) - .then((paypal_sdk) => { - res.set({ - "Content-Type": "application/javascript; charset=utf-8", - "Content-Length": paypal_sdk.length, - }); - res.status(200); - res.send(paypal_sdk); - }) - .catch((reason) => { - res.status(400).json({ errors: [{ msg: reason }] }); - }); -}); - module.exports = router; ////////////////////// diff --git a/pass/routes/key.js b/pass/routes/key.js index 5143230796dfe77e1d28daefb5ac2a40f12782e3..a30ebc6be39115a620cdd14b661ebfd52f056ddf 100644 --- a/pass/routes/key.js +++ b/pass/routes/key.js @@ -80,7 +80,11 @@ router.use( } ); -router.get("/:key/checkout/:amount?", (req, res) => { +router.get("/:key/checkout/:amount", (req, res) => { + if (req.query.error) { + req.data.checkout.error = req.query.error; + } + req.data.js.push("/js/funding_sources.js"); res.render("key", req.data); }); diff --git a/pass/views/key.ejs b/pass/views/key.ejs index 236ea09759a8cdfec0d45c5a9741f015f5eecf39..825349d4246a32ef45adb171179e9eade89160c2 100644 --- a/pass/views/key.ejs +++ b/pass/views/key.ejs @@ -38,15 +38,24 @@ </div> <%_ }else { _%> <h2>Suchanfragen auffüllen</h2> + <%_ if (typeof checkout !== "undefined" && typeof checkout.amount !== "undefined") { _%> <div id="summary"> - <%_ if (typeof checkout !== "undefined" && typeof checkout.amount !== "undefined") { _%> <a href="<%= change_url.amount %>" class="checkout-amount" title="Menge ändern"> <span class="checkout-amount"><%= checkout.amount %></span> <span class="checkout-cost"><%= checkout.amount / 300 * 5%>€</span> <span class="checkout-duration">inkl. 7% USt.</span> </a> + + <%_ if (typeof checkout !== "undefined" && typeof checkout.payment !== "undefined" && typeof checkout.payment.provider !== "undefined" && checkout.payment.provider === "paypal") { _%> + <a href="<%= change_url.funding_source %>" class="funding_source"> + <img src="/images/paypal/<%= checkout.payment.paypal.funding_source %>.svg" alt="<%= checkout.payment.paypal.funding_source %> Logo"> + <%_ if(checkout.payment.paypal.funding_source === "card") { _%> + <span>Kreditkarte</span> + <%_ } _%> + </a> <%_ } _%> </div> + <%_ } _%> <div id="checkout"> <%_ if (typeof checkout === "undefined" || typeof checkout.amount === "undefined") { _%> <div id="checkout-amount"> @@ -84,15 +93,21 @@ <p>* Die angegebenen Zeiträume sind Schätzungen, die auf unseren Erfahrungswerten basieren und sollen einen Anhaltspunkt geben, wie viele Suchanfragen benötigt werden.</p> <%_ } _%> </div> - <%_ if (typeof checkout !== "undefined" && typeof checkout.amount !== "undefined") { _%> + <%_ if (typeof checkout !== "undefined" && typeof checkout.amount !== "undefined" && (typeof checkout.payment === "undefined" || typeof checkout.payment.provider === "undefined")) { _%> <div id="payment"> + <%_ if(typeof checkout.paypal_client_id !== "undefined") { _%> + <input type="hidden" name="paypal-client-id" value="<%= checkout.paypal_client_id %>"> + <%_ } %> <h2>Zahlungsart wählen</h2> + <%_ if (checkout.error === "funding_source_not_eligible") { _%> + <p class="error">Die gewählte Zahlungsart ist in deiner Region leider nicht verfügbar.</p> + <%_ } _%> <label for="payment-group-paypal"> <div class="info">Zahlungsdienstleister</div> </label> <input type="radio" name="payment-group" id="payment-group-paypal" selected> <div id="paypal-payments" class="payment-group"> - <a id="paypal-payment" class="funding_source"> + <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/paypal#paypal-checkout" id="paypal-payment" class="funding_source"> <img src="/images/paypal/paypal.svg" alt="PayPal Logo"> </a> <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/card#paypal-checkout" id="card" class="funding_source"> @@ -111,19 +126,19 @@ <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/p24#paypal-checkout" id="p24" class="funding_source"> <img src="/images/paypal/p24.svg" alt="P24 Logo"> </a> - <a href="" id="bancontact" class="funding_source"> + <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/bancontact#paypal-checkout" id="bancontact" class="funding_source"> <img src="/images/paypal/bancontact.svg" alt="Bancontact Logo"> </a> - <a href="" id="blik" class="funding_source"> + <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/blik#paypal-checkout" id="blik" class="funding_source"> <img src="/images/paypal/blik.svg" alt="BLIK Logo"> </a> - <a href="" id="eps" class="funding_source"> + <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/eps#paypal-checkout" id="eps" class="funding_source"> <img src="/images/paypal/eps.svg" alt="EPS Logo"> </a> - <a href="" id="ideal" class="funding_source"> + <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/ideal#paypal-checkout" id="ideal" class="funding_source"> <img src="/images/paypal/ideal.svg" alt="iDeal Logo"> </a> - <a href="" id="mybank" class="funding_source"> + <a href="/key/<%= key.key %>/checkout/<%= checkout.amount %>/paypal/mybank#paypal-checkout" id="mybank" class="funding_source"> <img src="/images/paypal/mybank.svg" alt="Mybank Logo"> </a> </div> @@ -132,15 +147,20 @@ <%_ if(typeof checkout !== "undefined" && typeof checkout.amount !== "undefined" && typeof checkout.payment !== "undefined" && typeof checkout.payment.provider !== "undefined" && checkout.payment.provider === "paypal") { _%> <div id="paypal-checkout"> <h2>Zahlung durchführen</h2> + <input type="hidden" name="funding-source-not-eligible-url" value="<%= change_url.funding_source_not_eligible %>"> + <input type="hidden" name="paypal-order-base-url" value="<%= change_url.order_base_url %>"> <input type="hidden" name="paypal-client-id" value="<%= checkout.payment.paypal.client_id %>"> <input type="hidden" name="paypal-funding-source" value="<%= checkout.payment.paypal.funding_source %>"> <%_ if(typeof checkout.payment.paypal.client_token !== "undefined") { _%> <input type="hidden" name="paypal-client-token" value="<%= checkout.payment.paypal.client_token %>"> <%_ } _%> - <p class="hidden">Die gewählte Zahlungsart ist in Ihrer Region nicht verfügbar.</p> + <div id="loading_paypal_funding_source"> + <img src="/images/loader.gif" alt="Loading Icon"> + <div>Zahlungsmethode wird geladen</div> + </div> <%_ if(typeof checkout.payment.paypal.funding_source !== undefined) { _%> <%_ if(checkout.payment.paypal.funding_source === "card") { _%> - <div id="paypal-payment-card"> + <div id="paypal-payment-card" class="hidden"> <form id="paypal-card-form"> <div> <label for="card-number">Kartennummer</label> @@ -158,8 +178,8 @@ </form> </div> <%_ }else { _%> - <div id="paypal-payment-fields"></div> - <div id="paypal-payment-button"></div> + <div id="paypal-payment-fields" class="hidden"></div> + <div id="paypal-payment-button" class="hidden"></div> <%_ } _%> <%_ } _%> </div>