diff --git a/pass/app/payment_processor/Paypal.js b/pass/app/payment_processor/Paypal.js index 7dbad29ff2c5d0e20ee3f8ba85042bbbecf7ebb9..364597c35e391668ffe44c6a43e341dc8e7562a4 100644 --- a/pass/app/payment_processor/Paypal.js +++ b/pass/app/payment_processor/Paypal.js @@ -51,11 +51,12 @@ class Paypal extends PaymentProcessor { /** * * @param {PaymentReference} payment_reference + * @param {boolean} creditcard_payment * @param {i18next.TFunction} t * * @returns {Promise<Object>} */ - async createOrder(payment_reference, t) { + async createOrder(payment_reference, creditcard_payment = false, t) { let payment = new Payment({ id: -1, price: payment_reference.price, @@ -114,6 +115,21 @@ class Paypal extends PaymentProcessor { }, }; + if (creditcard_payment) { + paypal_order["payment_source"] = { + "card": { + "attributes": { + "verification": { + "method": "SCA_ALWAYS" + } + }, + "experience_context": { + "shipping_preference": "NO_SHIPPING", + } + } + }; + } + return this.#generateAccessToken().then((access_token) => { return fetch(`${this.#base}/v2/checkout/orders`, { method: "post", @@ -153,10 +169,9 @@ class Paypal extends PaymentProcessor { if ( response_data.status !== "COMPLETED" || response_data.purchase_units[0].payments.captures[0].status !== - "COMPLETED" + "COMPLETED" ) { - console.error(JSON.stringify(response_data)); - throw "PAYMENT_NOT_COMPLETED_ERROR"; + throw response_data; } return response_data; }); @@ -383,14 +398,83 @@ class Paypal extends PaymentProcessor { async verify_3D() { return this.get_order_details().then((order_details) => { - if ( - order_details.payment_source.card.authentication_result - .liability_shift !== "POSSIBLE" - ) { - return Promise.reject("3D Authentication was not successful"); + if (order_details.payment_source.card.hasOwnProperty("authentication_result")) { + let authentication_result = order_details.payment_source.card.authentication_result; + let liability_shift = authentication_result.liability_shift; + let authentication_status = null; + if (authentication_result.three_d_secure.hasOwnProperty("authentication_status")) { + authentication_status = authentication_result.three_d_secure.authentication_status; + } + let enrollment_status = authentication_result.three_d_secure.enrollment_status; + if (enrollment_status == "Y") { + switch (authentication_status) { + case "Y": + if (["POSSIBLE", "YES"].includes(liability_shift)) { + return true; + } else { + return Promise.reject(order_details); + } + case "N": + if (liability_shift == "NO") { + return Promise.reject(order_details); + } else { + return true; + } + case "R": + if (liability_shift == "NO") { + return Promise.reject(order_details); + } else { + return true; + } + case "A": + if (liability_shift == "POSSIBLE") { + return true; + } else { + return Promise.reject(order_details); + } + case "U": + if (["UNKNOWN", "NO"].includes(liability_shift)) { + return Promise.reject(order_details); + } else { + return true; + } + case "C": + if (liability_shift == "UNKNOWN") { + return Promise.reject(order_details); + } else { + return true; + } + default: + return Promise.reject(order_details); + } + } else if (enrollment_status == "N") { + if (liability_shift == "NO") { + return true; + } else { + return Promise.reject(order_details); + } + } else if (enrollment_status == "U") { + switch (liability_shift) { + case "NO": + return true; + case "UNKNOWN": + return Promise.reject(order_details); + default: + return Promise.reject(order_details); + } + } else if (enrollment_status == "B") { + if (liability_shift == "NO") { + return true; + } else { + return Promise.reject(order_details); + } + } else { + return Promise.reject(order_details); + } } else { return true; } + }); } diff --git a/pass/config/default.json b/pass/config/default.json index 7391c1b38ff91ddc17df395f83b322730668dc16..fdd79f2a3af984e30c71ee736a6659690559074b 100644 --- a/pass/config/default.json +++ b/pass/config/default.json @@ -18,7 +18,7 @@ "app_secret": "<OPEN ID APP SECRET>" }, "osticket": { - "enabled": true, + "enabled": false, "url": "<OSTICKET_URL>", "api_key": "<OSTICKET_API_KEY>" }, diff --git a/pass/lang/en/checkout.json b/pass/lang/en/checkout.json index 99c696f919806a3c56c159b64a80fc96acbefc1a..4eba908a17810bc0209e9b0f828e66106585d91c 100644 --- a/pass/lang/en/checkout.json +++ b/pass/lang/en/checkout.json @@ -50,10 +50,24 @@ "3D": "3D authentication failed" }, "card": { + "name": "Cardholder Name (optional)", "number": "Card number", "expiration": "Valid until", "cvv": "CVV", - "label": "Credit / debit card" + "label": "Credit / debit card", + "error": { + "9500": "Credit card rejected as fraudulent", + "5100": "The credit card was declined by the credit institution", + "00N7": "Wrong CVV. Please check input", + "5400": "Credit card expired", + "5180": "Luhn check failed", + "5120": "Credit card declined due to insufficient funds.", + "9520": "Credit card rejected as lost/stolen", + "0500": "Credit card declined by credit institution", + "1330": "Credit card invalid. Please check your entry", + "3ds": "3DS Authentication failed", + "generic": "Credit card declined by credit institution" + } }, "submit": "Make payment", "loading": "Payment method is loaded" diff --git a/pass/package-lock.json b/pass/package-lock.json index 2aefae8790901623b9beb547de747b0a9846e03e..f903f5a762cf22e77d7616d38dc64f3f2629d8fb 100644 --- a/pass/package-lock.json +++ b/pass/package-lock.json @@ -8,7 +8,7 @@ "name": "pass", "version": "0.0.0", "dependencies": { - "@paypal/paypal-js": "^5.1.1", + "@paypal/paypal-js": "^8.0.5", "accept-language": "^3.0.18", "blind-signatures": "^1.0.7", "browserify-middleware": "^8.1.1", @@ -474,11 +474,11 @@ } }, "node_modules/@paypal/paypal-js": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-5.1.5.tgz", - "integrity": "sha512-303/cICvUfoq4OXIuAO4AJZNw1YNF6tCkHzTP7zGnXcCICPagkH7Ww13bOJVwgtuNkQMB44sJUEk5uAxFTAp8w==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-8.0.5.tgz", + "integrity": "sha512-yQNV7rOILeaVCNU4aVDRPqEnbIlzfxgQfFsxzsBuZW1ouqRD/4kYBWJDzczCiscSr2xOeA/Pkm7e3a9fRfnuMQ==", "dependencies": { - "promise-polyfill": "^8.2.3" + "promise-polyfill": "^8.3.0" } }, "node_modules/@sideway/address": { diff --git a/pass/package.json b/pass/package.json index 3d0c449b077ad7f367d14565f4e8accd9c3e0a31..3a68a10441672528ec7e25fdbd71ede67a09738d 100644 --- a/pass/package.json +++ b/pass/package.json @@ -7,7 +7,7 @@ "dev": "nodemon --ignore node_modules/ --verbose --exec node --inspect=0.0.0.0 ./bin/www" }, "dependencies": { - "@paypal/paypal-js": "^5.1.1", + "@paypal/paypal-js": "^8.0.5", "accept-language": "^3.0.18", "blind-signatures": "^1.0.7", "browserify-middleware": "^8.1.1", @@ -45,4 +45,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} +} \ No newline at end of file diff --git a/pass/public/styles/key/checkout-payment.less b/pass/public/styles/key/checkout-payment.less index 4959268e5c9bb0fd3de84d98454483ff15283bc8..c8dd658c92dc5eab04574f79b0406c14a639fe44 100644 --- a/pass/public/styles/key/checkout-payment.less +++ b/pass/public/styles/key/checkout-payment.less @@ -2,13 +2,14 @@ margin-bottom: 1rem; @payment-group-breakpoint-1: 930px; @payment-group-breakpoint-2: 470px; - > h2 { + + >h2 { margin: 0 0 1rem; text-align: center; border-bottom: 1px solid var(--color-main); } - > label { + >label { border: 1px solid #777; display: block; padding: 0.5rem; @@ -18,25 +19,30 @@ border-bottom-color: var(--color-main); border-bottom-width: 2px; } - > .payment-group-container { + + >.payment-group-container { border: 1px solid #777; margin: 1rem 0; border-radius: 5px; display: grid; gap: 1rem; - > * { + + >* { padding: 0 1rem; + &:last-child { margin-bottom: 1rem; } } - > label { + + >label { background-color: var(--color-secondary); display: block; color: var(--font-color); padding: 0.5rem 1rem; } - > .payment-group { + + >.payment-group { display: grid; grid-template-columns: 1fr 1fr 1fr; align-items: center; @@ -44,21 +50,24 @@ gap: 1rem; border-top: 0; place-items: stretch; - > div { + + >div { width: 14em; } + @media (max-width: @payment-group-breakpoint-1) { grid-template-columns: 1fr 1fr; } + @media (max-width: @payment-group-breakpoint-2) { grid-template-columns: 1fr; } - > div { + >div { min-width: 10em; } - > .funding_source { + >.funding_source { display: flex; border: 1px solid #777; padding: 1rem; @@ -70,6 +79,7 @@ color: inherit; text-decoration: none; box-sizing: content-box; + img { max-width: 100%; max-height: 100%; @@ -77,77 +87,88 @@ } } } - > #payment-group-paypal { + + >#payment-group-paypal { display: none; } } #paypal-checkout { - > h2 { + >h2 { margin: 0 0 1rem; text-align: center; border-bottom: 1px solid var(--color-main); } - > #loading_paypal_funding_source { + + >#loading_paypal_funding_source { display: flex; align-items: center; justify-content: center; gap: 1rem; color: #777; padding: 2rem 0; + img { width: 2rem; } } + #revocation-container { display: grid; gap: 1rem; margin: 1rem 0; } + div.revocation-required-error { display: none; } - #paypal-payment-card #paypal-card-form { + + #card-form { display: grid; - grid-template-columns: 1fr calc(7ch + 1.5rem) calc(4ch + 1.5rem); + grid-template-columns: 1fr calc(12ch) calc(8ch); grid-template-rows: 1fr auto auto; place-items: stretch; gap: 0.5rem; - > div { - &.input-group { - grid-column: span 3; - } - &:not(.input-group, .error) { - display: flex; - flex-direction: column; - gap: 0.5rem; - > label { - font-weight: bold; - font-size: 0.8rem; - white-space: nowrap; - &.error { - color: red; - line-height: 1; - &::before { - font-size: initial; - } + >div, + >button { + >label { + font-weight: bold; + font-size: 0.8rem; + white-space: nowrap; + + &.error { + color: red; + line-height: 1; + + &::before { + font-size: initial; } } - > div { - height: 2.3rem; - border: 1px solid #777; - border-radius: 5px; - } - &.card-holder-name { - grid-column: span 3; - input { - padding: 0.5rem; - } + } + + >div { + height: 2.3rem; + border: 1px solid #777; + border-radius: 5px; + } + + &.card-name-group, + &#card-errors, + &#agb, + &.checkbox, + &#card-submit { + grid-column: span 3; + } + + &.checkbox { + >label { + white-space: wrap; } } } - > button#submit-credit-card { + + >button#submit-credit-card { grid-column: span 3; justify-self: center; display: flex; @@ -156,12 +177,13 @@ line-height: 1; gap: 0.5rem; - > img { + >img { display: none; } - &.loading > img { + + &.loading>img { display: block; } } } -} +} \ No newline at end of file diff --git a/pass/resources/js/checkout_paypal.js b/pass/resources/js/checkout_paypal.js index 7d774721442ff14d8df1780255fc094754194688..d8da07902c3ce5991db34f0cb55b1b1fc494e8b1 100644 --- a/pass/resources/js/checkout_paypal.js +++ b/pass/resources/js/checkout_paypal.js @@ -9,8 +9,7 @@ function initialize_paypal_payments() { "buttons", "marks", "payment-fields", - "funding-eligibility", - "hosted-fields", + "funding-eligibility" ], "disable-funding": "sofort,ideal", currency: "EUR", @@ -33,6 +32,10 @@ function initialize_paypal_payments() { let checkout_data = get_paypal_checkout_data(funding_source); + if (funding_source == "card") { + script_data.components = ["card-fields", "funding-eligibility", "buttons"]; + } + paypal_client .loadScript(script_data) .then((paypal) => { @@ -202,6 +205,104 @@ function cancelPayment(payment_reference) { } function loadCardPayment(checkout_data) { + let container = document.getElementById("content-container"); + let card_form = document.getElementById("card-form-skeleton").cloneNode(true); + card_form.id = "card-form"; + card_form.classList.remove("hidden"); + document.getElementById("card-form-skeleton").remove(); + container.appendChild(card_form); + + // Initialize Card Fields + let cardFields_options = get_paypal_checkout_data("card"); + + cardFields_options.style = { + body: { + padding: 0 + }, + input: { + background: "red", + "font-size": "16px", + "padding": "0.4rem 0.75rem", + }, + }; + let cardFields = paypal.CardFields(cardFields_options); + document + .getElementById("loading_paypal_funding_source") + .classList.add("hidden"); + if (!cardFields.isEligible()) { + showError("generic"); + return; + } + + const nameField = cardFields.NameField({ placeholder: "John Doe" }); + nameField.render("#card-name"); + + const numberField = cardFields.NumberField({ placeholder: "4111 1111 1111 1111" }); + numberField.render("#card-number"); + + const expiryField = cardFields.ExpiryField({ placeholder: "123" }); + expiryField.render("#card-expiration"); + + const cvvField = cardFields.CVVField(); + cvvField.render("#card-cvv"); + + card_form.addEventListener("submit", (event) => { + if (!card_form.checkValidity()) { + return; + } + event.preventDefault(); + hideErrors(); + lockForm(true); + cardFields.getState().then((data) => { + // Submit only if the current + // state of the form is valid + if (!data.isFormValid) { + showError(`error-1330`); + } else { + return cardFields.submit().then(() => { + }).catch((error) => { + console.error(error); + + let error_code_shown = false; + try { + let processor_response_code = + error.purchase_units[0].payments.captures[0] + .processor_response.response_code; + showError(`error-${processor_response_code}`); + error_code_shown = true; + } catch (e) { } + + try { + let card_errors_container = document.querySelector("#card-errors"); + if (card_errors_container.classList.contains("hidden")) { + card_errors_container.classList.remove("hidden"); + } + for (let i = 0; i < error.details.length; i++) { + let error_container = document.createElement("div"); + error_container.classList.add("error"); + error_container.textContent = error.details[i].description; + card_errors_container.appendChild(error_container); + } + error_code_shown = true; + } catch (e) { } + if (!error_code_shown) { + if (error.toString != undefined) { + error = error.toString(); + } + if (error.includes != undefined && error.includes("3DS")) { + showError("error-3ds"); + } else { + showError(`error-1330`); + } + } + }); + } + }).finally(() => { + lockForm(false) + }); + + }); + return; // Show the Form document .getElementById("loading_paypal_funding_source") @@ -353,4 +454,49 @@ function validateRevocation() { } } +function showError(errorId) { + let error_container = document.querySelector("#card-errors"); + if (!error_container) { + return; + } + if (error_container.classList.contains("hidden")) { + error_container.classList.remove("hidden"); + } + let error_element = error_container.querySelector(`#${errorId}`); + if (!error_element) { + error_element = error_container.querySelector("#error-generic"); + } + if (error_element.classList.contains("hidden")) { + error_element.classList.remove("hidden"); + } +} + +function hideErrors() { + let error_container = document.querySelector("#card-errors"); + if (!error_container) { + return; + } + if (!error_container.classList.contains("hidden")) { + error_container.classList.add("hidden"); + } + error_container.querySelectorAll(".error").forEach(error_element => { + if (!error_element.classList.contains("hidden")) { + error_element.classList.add("hidden"); + } + }); +} + +/** + * Prevents multiple submissions of the card form + * + * @param {boolean} lock + */ +function lockForm(lock) { + let submit_button = document.querySelector("#card-form #card-submit"); + if (!submit_button) { + return; + } + submit_button.disabled = lock; +} + module.exports = initialize_paypal_payments; diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js index a337609acfa207527d4d1f5acc83bc758b0aa0a6..17dc3e4ad7def51b73431c368ab0b5b704c9a856 100644 --- a/pass/routes/checkout/paypal.js +++ b/pass/routes/checkout/paypal.js @@ -56,6 +56,7 @@ router.get("/:funding_source", async (req, res) => { }); router.post("/:funding_source/order/create", async (req, res) => { + let funding_source = req.params.funding_source; // Order data is validated: Create and store the PaymentReference let paypal = new Paypal(); return PaymentReference.CREATE_NEW_REQUEST( @@ -63,7 +64,7 @@ router.post("/:funding_source/order/create", async (req, res) => { req.data.key.key.get_key() ) .then((payment_reference) => { - return paypal.createOrder(payment_reference, req.t).then((paypal) => { + return paypal.createOrder(payment_reference, funding_source == "card", req.t).then((paypal) => { return __redis_client .setex( `payments:paypal:${payment_reference.public_id}`, @@ -180,14 +181,7 @@ router.post( }) .catch((reason) => { console.debug(reason); - res.status(400).json({ - errors: [ - { - type: "PAYMENT_NOT_COMPLETED_ERROR", - msg: "Couldn't capture the payment", - }, - ], - }); + res.status(400).json(reason); }); } ); diff --git a/pass/views/checkout/paypal.ejs b/pass/views/checkout/paypal.ejs index cb3119b2a737414c91e941c700ec5da0ca8e123e..81fa57f77c8d92911adf56dced77496327aa689e 100644 --- a/pass/views/checkout/paypal.ejs +++ b/pass/views/checkout/paypal.ejs @@ -1,101 +1,97 @@ <div id="paypal-checkout"> - <h2><%= req.t("paypal.heading", {ns: "checkout"}) _%></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 %>" - /> - <%_ } _%> <%_ if(typeof checkout.payment.paypal.funding_source !== undefined) - { _%> <%_ if(checkout.payment.paypal.funding_source === "card") { _%> - <div id="paypal-payment-card" class="hidden"> - <div id="paypal-card-errors"> - <p id="paypal-card-errors-generic" class="error hidden"> - <%= req.t("paypal.errors.failed", {ns: "checkout"}) _%> - </p> - <p id="paypal-card-errors-invalid-card" class="error hidden"> - <%= req.t("paypal.errors.invalid-card", {ns: "checkout"}) _%> - </p> - <p id="paypal-card-errors-expired" class="error hidden"> - <%= req.t("paypal.errors.expired-card", {ns: "checkout"}) _%> - </p> - <p id="paypal-card-errors-rejected" class="error hidden"> - <%= req.t("paypal.errors.rejected", {ns: "checkout"}) _%> - </p> - <p id="paypal-card-errors-3d" class="error hidden"> - <%= req.t("paypal.errors.3d", {ns: "checkout"}) _%> - </p> - </div> - <form id="paypal-card-form"> - <div> - <label for="card-number" - ><%= req.t("paypal.card.number", {ns: "checkout"}) _%></label - > - <div id="card-number" class="card_field"></div> - </div> - <div> - <label for="expiration-date" - ><%= req.t("paypal.card.expiration", {ns: "checkout"}) _%></label - > - <div id="expiration-date" class="card_field"></div> - </div> - <div> - <label for="cvv" - ><%= req.t("paypal.card.cvv", {ns: "checkout"}) _%></label - > - <div id="cvv" class="card_field"></div> - </div> - <%_ if(process.env.NODE_ENV === "development") { _%> - <div class="card-holder-name"> - <label for="card-holder-name" - >Name (Entfällt im Produktivbetrieb)</label - > - <input - type="text" - id="card-holder-name" - class="card_field" - autocomplete="off" - placeholder="Name" - /> - </div> - <%_ } _%> <%- include("../templates/agb") -%> <%- - include("../templates/revocation") -%> - <button type="submit" id="submit-credit-card" class="button"> - <img src="<%= baseDir _%>/images/loader.gif" alt="Loading symbol" /> - <span><%= req.t("paypal.submit", {ns: "checkout"}) _%></span> - </button> - </form> - </div> - <%_ }else { _%> - <div class="hidden" id="revocation-container"> - <%- include("../templates/agb") -%> <%- include("../templates/revocation") - -%> - </div> - <div id="paypal-payment-fields" class="hidden"></div> - <div id="paypal-payment-button" class="hidden"></div> - <%_ } _%> <%_ } _%> - <div id="loading_paypal_funding_source"> - <img src="<%= baseDir _%>/images/loader.gif" alt="Loading Icon" /> - <div><%= req.t("paypal.loading", {ns: "checkout"}) _%></div> - </div> -</div> + <h2> + <%= req.t("paypal.heading", {ns: "checkout" }) _%> + </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 %>" /> + <%_ } _%> + <%_ if(typeof checkout.payment.paypal.funding_source !==undefined) { _%> + <%_ if(checkout.payment.paypal.funding_source==="card" ) { _%> + <form id="card-form-skeleton" class="hidden"> + <div id="card-errors" class="hidden"> + <div id="error-9500" class="error hidden"> + <%= req.t("paypal.card.error.9500", {ns: "checkout" }) _%> + </div> + <div id="error-5100" class="error hidden"> + <%= req.t("paypal.card.error.5100", {ns: "checkout" }) _%> + </div> + <div id="error-00N7" class="error hidden"> + <%= req.t("paypal.card.error.00N7", {ns: "checkout" }) _%> + </div> + <div id="error-5110" class="error hidden"> + <%= req.t("paypal.card.error.00N7", {ns: "checkout" }) _%> + </div> + <div id="error-5400" class="error hidden"> + <%= req.t("paypal.card.error.5400", {ns: "checkout" }) _%> + </div> + <div id="error-5180" class="error hidden"> + <%= req.t("paypal.card.error.5180", {ns: "checkout" }) _%> + </div> + <div id="error-5120" class="error hidden"> + <%= req.t("paypal.card.error.5120", {ns: "checkout" }) _%> + </div> + <div id="error-9520" class="error hidden"> + <%= req.t("paypal.card.error.9520", {ns: "checkout" }) _%> + </div> + <div id="error-0500" class="error hidden"> + <%= req.t("paypal.card.error.0500", {ns: "checkout" }) _%> + </div> + <div id="error-1330" class="error hidden"> + <%= req.t("paypal.card.error.1330", {ns: "checkout" }) _%> + </div> + <div id="error-3ds" class="error hidden"> + <%= req.t("paypal.card.error.3ds", {ns: "checkout" }) _%> + </div> + <div id="error-generic" class="error hidden"> + <%= req.t("paypal.card.error.generic", {ns: "checkout" }) _%> + </div> + </div> + <div class="input-group card-name-group"> + <label for="card-name"> + <%= req.t("paypal.card.name", {ns: "checkout" }) _%> + </label> + <div id="card-name"></div> + </div> + <div class="input-group card-number-group"> + <label for="card-number"> + <%= req.t("paypal.card.number", {ns: "checkout" }) _%> + </label> + <div id="card-number"></div> + </div> + <div class="input-group card-expiration-group"> + <label for="card-expiration"> + <%= req.t("paypal.card.expiration", {ns: "checkout" }) _%> + </label> + <div id="card-expiration"></div> + </div> + <div class="input-group card-cvv-group"> + <label for="card-number"> + <%= req.t("paypal.card.cvv", {ns: "checkout" }) _%> + </label> + <div id="card-cvv"></div> + </div> + <%- include("../templates/agb") -%> <%- include("../templates/revocation") -%> + <button type="submit" id="card-submit" class="btn btn-default"> + <%= req.t("paypal.submit", {ns: "checkout" }) _%> + </button> + </form> + <div id="content-container"></div> + <%_ }else { _%> + <div class="hidden" id="revocation-container"> + <%- include("../templates/agb") -%> <%- include("../templates/revocation") -%> + </div> + <div id="paypal-payment-fields" class="hidden"></div> + <div id="paypal-payment-button" class="hidden"></div> + <%_ } _%> + <%_ } _%> + <div id="loading_paypal_funding_source"> + <img src="<%= baseDir _%>/images/loader.gif" alt="Loading Icon" /> + <div> + <%= req.t("paypal.loading", {ns: "checkout" }) _%> + </div> + </div> +</div> \ No newline at end of file