diff --git a/pip-packages.txt b/pip-packages.txt index e0807192c82ff1cb4905e1651fc1efb0a47a8301..ce6254516f9f814849844a3708121e4a89006338 100644 --- a/pip-packages.txt +++ b/pip-packages.txt @@ -1,16 +1,30 @@ +apimatic-core==0.2.2 +apimatic-core-interfaces==0.1.3 +apimatic-requests-client-adapter==0.1.3 +CacheControl==0.12.11 certifi==2019.3.9 +charset-normalizer==3.1.0 +deprecation==2.1.0 Django==2.2 django-auth-ldap==1.7.0 django-formtools==2.1 django-sslserver==0.20 +enum34==1.1.10 +idna==3.4 +jsonpickle==3.0.1 +jsonpointer==2.3 ldap3==2.6 +msgpack==1.0.5 +packaging==23.1 psycopg2-binary==2.8.2 pyasn1==0.4.5 pyasn1-modules==0.2.4 -python-dateutil==2.8.0 +python-dateutil==2.8.2 python-ldap==3.2.0 pytz==2019.1 +requests==2.28.2 six==1.12.0 sqlparse==0.3.0 squareconnect==2.20190410.0 -urllib3==1.25 +squareup==26.0.0.20230419 +urllib3==1.26.15 diff --git a/src/squarepay/payments.py b/src/squarepay/payments.py index 74c15cfeab1de39a5c9d68e0f6d5cfb63c870493..c46d3cc7ef794e6694822bf9a282e1d0194d3336 100644 --- a/src/squarepay/payments.py +++ b/src/squarepay/payments.py @@ -5,10 +5,7 @@ import logging from django.conf import settings from django.core.exceptions import ImproperlyConfigured - -import squareconnect -from squareconnect.rest import ApiException -from squareconnect.apis.transactions_api import TransactionsApi +from square.client import Client log = logging.getLogger('squarepay') @@ -21,37 +18,37 @@ access_key = getattr(settings, 'SQUARE_ACCESS_TOKEN', None) if (app_id is None) or (loc_id is None) or (access_key is None): raise ImproperlyConfigured("Please define SQUARE_APP_ID, SQUARE_LOCATION and SQUARE_ACCESS_TOKEN in settings.py") -# instantiate the global squareconnect ApiClient instance (only needs to be done once) -_sqapi_inst = squareconnect.ApiClient() -_sqapi_inst.configuration.access_token = access_key - -def get_transactions_api(): - return TransactionsApi(_sqapi_inst) +client = Client(access_token=access_key, environment='production') -def try_capture_payment(card_payment, nonce): +def try_capture_payment(card_payment, source_id, verification_token): """ attempt to charge the customer associated with the given card nonce (created by the PaymentForm in JS) Note: this can be called multiple times with the same CardPayment instance but the customer will not be charged multiple times (using the Square idempotency key feature) Returns either True on success or False on failure. """ - api_inst = get_transactions_api() - request_body = { 'idempotency_key': card_payment.idempotency_key, - 'card_nonce': nonce, + 'source_id': source_id, + 'verification_token': verification_token, 'amount_money': { 'amount': card_payment.amount, 'currency': 'AUD' } } - try: - api_response = api_inst.charge(loc_id, request_body) + result = client.payments.create_payment(body=request_body) + + if result.is_success(): card_payment.set_paid() - log.info("TransactionApi response without error, charge $%1.2f" % (float(card_payment.amount) / 100.0)) - return True - except ApiException as e: - log.error("Exception while calling TransactionApi::charge: %s" % e) - return False + return { + "success": True, + "receipt_url": result.body["payment"]["receipt_url"], + } + # Call the error method to see if the call failed + elif result.is_error(): + return { + "success": False, + "errors": result.errors, + } diff --git a/src/squarepay/views.py b/src/squarepay/views.py index c80825f531d6848bf3eff53cb27033cfbb58d9e6..fd8ae945a7dc4a21a2cde7e2818a6b75e16129a3 100644 --- a/src/squarepay/views.py +++ b/src/squarepay/views.py @@ -1,3 +1,4 @@ +import json import uuid from django.views.generic.base import RedirectView, View from django.views.generic.detail import DetailView @@ -34,23 +35,32 @@ class PaymentFormMixin: payment.set_paid() messages.success(self.request, "Your payment of $%1.2f was successful." % (payment.amount / 100.0)) - def payment_error(self, payment): + def payment_error(self, payment, error): messages.error(self.request, "Your payment of $%1.2f was unsuccessful. Please try again later." % (payment.amount / 100.0)) + messages.error(self.request, error) payment.delete() def post(self, request, *args, **kwargs): - nonce = request.POST.get('nonce', None) + data = json.loads(request.body.decode('utf-8')) + source_id = data['sourceId'] + verification_token = data['verificationToken'] + card_payment = self.get_object() amount_aud = card_payment.amount / 100.0 - - if (nonce is None or nonce == ""): + + if (source_id is None or source_id == ""): messages.error(request, "Failed to collect card details. Please reload the page and submit again.") return self.get(request) - if try_capture_payment(card_payment, nonce): + payment = try_capture_payment(card_payment, source_id, verification_token) + if payment["success"]: self.payment_success(card_payment) + messages.success(request, "Receipt: %s" % payment["receipt_url"]) + return HttpResponse(json.dumps(payment)) else: - self.payment_error(card_payment) + error = payment['errors'][0]['detail'] + self.payment_error(card_payment, error) + return HttpResponse(json.dumps(payment), status=403) # redirect to success URL, or redisplay the form with a success message if none is given return HttpResponseRedirect(self.get_completed_url()) @@ -110,7 +120,8 @@ class MembershipPaymentView(MemberAccessMixin, PaymentFormMixin, DetailView): if (self.object is None): # the membership is already marked as paid and no CardPayment exists # so we add an error and redirect to member home - messages.error(request, "Your membership is already paid. Check the cokelog (/home/other/coke/cokelog) for more details.") + if "/payment/" not in self.request.META['HTTP_REFERER']: + messages.error(request, "Your membership is already paid. Check the cokelog (/home/other/coke/cokelog) for more details.") return HttpResponseRedirect(self.get_completed_url()) else: return super().dispatch(request, *args, **kwargs) diff --git a/src/static/squarepay.css b/src/static/squarepay.css index 91776e920b9cb75bf8848afdb097b613b29b2c02..234267b71647b22809731e2b0845aeae05946134 100644 --- a/src/static/squarepay.css +++ b/src/static/squarepay.css @@ -9,117 +9,8 @@ button { border: 0; } -hr { - height: 1px; - border: 0; - background-color: #CCC; -} - -fieldset { - margin: 0; - padding: 0; - border: 0; -} - -.label { - font-size: 14px; - font-weight: 500; - line-height: 24px; - letter-spacing: 0.5; - text-transform: uppercase; -} - -.half { - float: left; - width: calc((100% - 16px) / 2); - padding: 0; - margin-right: 16px; -} - -.half:last-of-type { - margin-right: 0; -} - -/* Define how SqPaymentForm iframes should look */ -.sq-input, -#sq-expiration-date, #sq-card-number, #sq-cvv { - box-sizing: border-box; - border: 1px solid #E0E2E3; - border-radius: 4px; - outline-offset: -2px; - /*display: inline-block;*/ - height: 56px; - -webkit-transition: border-color .2s ease-in-out, background .2s ease-in-out; - -moz-transition: border-color .2s ease-in-out, background .2s ease-in-out; - -ms-transition: border-color .2s ease-in-out, background .2s ease-in-out; - transition: border-color .2s ease-in-out, background .2s ease-in-out; -} - -div#sq-expiration-date, div#sq-card-number, div#sq-cvv { - background-color: #f2f2f2; -} - -/* Define how SqPaymentForm iframes should look when they have focus */ -.sq-input--focus { - border: 1px solid #4A90E2; - background-color: rgba(74,144,226,0.02); -} - - -/* Define how SqPaymentForm iframes should look when they contain invalid values */ -.sq-input--error { - border: 1px solid #E02F2F; - background-color: rgba(244,47,47,0.02); -} - -#sq-card-number { - margin-bottom: 16px; -} - -/* Customize the "Pay with Credit Card" button */ -.button-credit-card { - width: 100%; - height: 56px; - margin-top: 10px; - background: #ccf; - border-radius: 4px; - cursor: default; - display: block; - color: #FFFFFF; - font-size: 16px; - line-height: 24px; - font-weight: 700; - letter-spacing: 0; - text-align: center; - -} -.button-credit-card:enabled { - width: 100%; - height: 56px; - margin-top: 10px; - background: #4A90E2; - border-radius: 4px; - cursor: pointer; - display: block; - color: #FFFFFF; - font-size: 16px; - line-height: 24px; - font-weight: 700; - letter-spacing: 0; - text-align: center; - -webkit-transition: background .2s ease-in-out; - -moz-transition: background .2s ease-in-out; - -ms-transition: background .2s ease-in-out; - transition: background .2s ease-in-out; -} - -.button-credit-card:hover:enabled { - background-color: #4281CB; -} - .payment-info { float: left; - width: 380px; margin: 20px auto; box-sizing: content-box; border: 2px solid #eee; @@ -134,73 +25,6 @@ div#sq-expiration-date, div#sq-card-number, div#sq-cvv { margin: 0; } -/*#content, #container, body { - height: 100%; -} -*/ -/* Customize the Apple Pay on the Web button */ -#sq-apple-pay { - width: 100%; - height: 48px; - padding: 0; - margin: 24px 0 16px 0; - background-image: -webkit-named-image(apple-pay-logo-white); - background-color: black; - background-size: 100% 60%; - background-repeat: no-repeat; - background-position: 50% 50%; - border-radius: 4px; - cursor: pointer; - display: none; -} - -/* Customize the Masterpass button */ -#sq-masterpass { - width: 100%; - height: 48px; - padding: 0; - margin: 24px 0 24px; - background-image: url("https://masterpass.com/dyn/img/acc/global/mp_mark_hor_wht.svg"); - background-color: black; - background-size: 60% 60%; - background-repeat: no-repeat; - background-position: calc((100% - 32px) / 2) 50%; - border-radius: 4px; - cursor: pointer; - display: none; -} - -/*#sq-masterpass::after { - box-sizing: border-box; - float: right; - width: 32px; - height: 48px; - padding-top: 12px; - content: url("data:image/svg+xml; utf8, <svg width='14' height='24' viewBox='0 0 14 24' xmlns='http://www.w3.org/2000/svg'><path d='M1.891 23.485c-.389 0-.778-.144-1.075-.436a1.46 1.46 0 0 1 0-2.102l9.141-8.944L.817 3.06a1.463 1.463 0 0 1 0-2.104 1.544 1.544 0 0 1 2.15 0l10.217 9.994a1.464 1.464 0 0 1 0 2.105L2.966 23.049a1.525 1.525 0 0 1-1.075.436' fill='#FFF' fill-rule='evenodd'/></svg>"); - background-color: #E6761F; - border-radius: 0 4px 4px 0; -}*/ - -/* Customize the Google Pay button */ -.button-google-pay { - min-width: 200px; - min-height: 40px; - padding: 11px 24px; - margin: 10px; - background-color: #000; - background-image: url(data:image/svg+xml,%3Csvg%20width%3D%22103%22%20height%3D%2217%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M.148%202.976h3.766c.532%200%201.024.117%201.477.35.453.233.814.555%201.085.966.27.41.406.863.406%201.358%200%20.495-.124.924-.371%201.288s-.572.64-.973.826v.084c.504.177.912.471%201.225.882.313.41.469.891.469%201.442a2.6%202.6%200%200%201-.427%201.47c-.285.43-.667.763-1.148%201.001A3.5%203.5%200%200%201%204.082%2013H.148V2.976zm3.696%204.2c.448%200%20.81-.14%201.085-.42.275-.28.413-.602.413-.966s-.133-.684-.399-.959c-.266-.275-.614-.413-1.043-.413H1.716v2.758h2.128zm.238%204.368c.476%200%20.856-.15%201.141-.448.285-.299.427-.644.427-1.036%200-.401-.147-.749-.441-1.043-.294-.294-.688-.441-1.183-.441h-2.31v2.968h2.366zm5.379.903c-.453-.518-.679-1.239-.679-2.163V5.86h1.54v4.214c0%20.579.138%201.013.413%201.302.275.29.637.434%201.085.434.364%200%20.686-.096.966-.287.28-.191.495-.446.644-.763a2.37%202.37%200%200%200%20.224-1.022V5.86h1.54V13h-1.456v-.924h-.084c-.196.336-.5.611-.91.826-.41.215-.845.322-1.302.322-.868%200-1.528-.259-1.981-.777zm9.859.161L16.352%205.86h1.722l2.016%204.858h.056l1.96-4.858H23.8l-4.41%2010.164h-1.624l1.554-3.416zm8.266-6.748h1.666l1.442%205.11h.056l1.61-5.11h1.582l1.596%205.11h.056l1.442-5.11h1.638L36.392%2013h-1.624L33.13%207.876h-.042L31.464%2013h-1.596l-2.282-7.14zm12.379-1.337a1%201%200%200%201-.301-.735%201%201%200%200%201%20.301-.735%201%201%200%200%201%20.735-.301%201%201%200%200%201%20.735.301%201%201%200%200%201%20.301.735%201%201%200%200%201-.301.735%201%201%200%200%201-.735.301%201%201%200%200%201-.735-.301zM39.93%205.86h1.54V13h-1.54V5.86zm5.568%207.098a1.967%201.967%200%200%201-.686-.406c-.401-.401-.602-.947-.602-1.638V7.218h-1.246V5.86h1.246V3.844h1.54V5.86h1.736v1.358H45.75v3.36c0%20.383.075.653.224.812.14.187.383.28.728.28.159%200%20.299-.021.42-.063.121-.042.252-.11.392-.203v1.498c-.308.14-.681.21-1.12.21-.317%200-.616-.051-.896-.154zm3.678-9.982h1.54v2.73l-.07%201.092h.07c.205-.336.511-.614.917-.833.406-.22.842-.329%201.309-.329.868%200%201.53.254%201.988.763.457.509.686%201.202.686%202.079V13h-1.54V8.688c0-.541-.142-.947-.427-1.218-.285-.27-.656-.406-1.113-.406-.345%200-.656.098-.931.294a2.042%202.042%200%200%200-.651.777%202.297%202.297%200%200%200-.238%201.029V13h-1.54V2.976zm32.35-.341v4.083h2.518c.6%200%201.096-.202%201.488-.605.403-.402.605-.882.605-1.437%200-.544-.202-1.018-.605-1.422-.392-.413-.888-.62-1.488-.62h-2.518zm0%205.52v4.736h-1.504V1.198h3.99c1.013%200%201.873.337%202.582%201.012.72.675%201.08%201.497%201.08%202.466%200%20.991-.36%201.819-1.08%202.482-.697.665-1.559.996-2.583.996h-2.485v.001zm7.668%202.287c0%20.392.166.718.499.98.332.26.722.391%201.168.391.633%200%201.196-.234%201.692-.701.497-.469.744-1.019.744-1.65-.469-.37-1.123-.555-1.962-.555-.61%200-1.12.148-1.528.442-.409.294-.613.657-.613%201.093m1.946-5.815c1.112%200%201.989.297%202.633.89.642.594.964%201.408.964%202.442v4.932h-1.439v-1.11h-.065c-.622.914-1.45%201.372-2.486%201.372-.882%200-1.621-.262-2.215-.784-.594-.523-.891-1.176-.891-1.96%200-.828.313-1.486.94-1.976s1.463-.735%202.51-.735c.892%200%201.629.163%202.206.49v-.344c0-.522-.207-.966-.621-1.33a2.132%202.132%200%200%200-1.455-.547c-.84%200-1.504.353-1.995%201.062l-1.324-.834c.73-1.045%201.81-1.568%203.238-1.568m11.853.262l-5.02%2011.53H96.42l1.864-4.034-3.302-7.496h1.635l2.387%205.749h.032l2.322-5.75z%22%20fill%3D%22%23FFF%22%2F%3E%3Cpath%20d%3D%22M75.448%207.134c0-.473-.04-.93-.116-1.366h-6.344v2.588h3.634a3.11%203.11%200%200%201-1.344%202.042v1.68h2.169c1.27-1.17%202.001-2.9%202.001-4.944%22%20fill%3D%22%234285F4%22%2F%3E%3Cpath%20d%3D%22M68.988%2013.7c1.816%200%203.344-.595%204.459-1.621l-2.169-1.681c-.603.406-1.38.643-2.29.643-1.754%200-3.244-1.182-3.776-2.774h-2.234v1.731a6.728%206.728%200%200%200%206.01%203.703%22%20fill%3D%22%2334A853%22%2F%3E%3Cpath%20d%3D%22M65.212%208.267a4.034%204.034%200%200%201%200-2.572V3.964h-2.234a6.678%206.678%200%200%200-.717%203.017c0%201.085.26%202.11.717%203.017l2.234-1.731z%22%20fill%3D%22%23FABB05%22%2F%3E%3Cpath%20d%3D%22M68.988%202.921c.992%200%201.88.34%202.58%201.008v.001l1.92-1.918c-1.165-1.084-2.685-1.75-4.5-1.75a6.728%206.728%200%200%200-6.01%203.702l2.234%201.731c.532-1.592%202.022-2.774%203.776-2.774%22%20fill%3D%22%23E94235%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E); - background-origin: content-box; - background-position: center; - background-repeat: no-repeat; - background-size: contain; - border: 0; - border-radius: 4px; - box-shadow: 0 1px 1px 0 rgba(60, 64, 67, 0.30), 0 1px 3px 1px rgba(60, 64, 67, 0.15); - outline: 0; - cursor: pointer; - display: none; -} - #error { width: 100%; margin-top: 16px; @@ -211,3 +35,138 @@ div#sq-expiration-date, div#sq-card-number, div#sq-cvv { opacity: 0.8; display: none; } + +#payment-form { + max-width: 550px; + min-width: 300px; + } + + + #card-container { + /* this height depends on the size of the container element */ + /* We transition from a single row to double row at 485px */ + /* Settting this min-height minimizes the impact of the card form loading */ + min-height: 90px; + } + + + @media screen and (max-width: 500px) { + #card-container { + min-height: 140px; + } + } + + p { + line-height: 24px; + } + + label { + font-size: 12px; + width: 100%; + } + + input { + padding: 12px; + width: 100%; + border-radius: 5px; + border-width: 1px; + margin-top: 20px; + font-size: 16px; + border: 1px solid rgba(0, 0, 0, 0.15); + } + + input:focus { + border: 1px solid #006aff; + } + + button { + color: #ffffff; + background-color: #006aff; + border-radius: 5px; + cursor: pointer; + border-style: none; + user-select: none; + outline: none; + font-size: 16px; + font-weight: 500; + line-height: 24px; + padding: 12px; + width: 100%; + box-shadow: 1px; + } + + button:active { + background-color: rgb(0, 85, 204); + } + + button:disabled { + background-color: rgba(0, 0, 0, 0.05); + color: rgba(0, 0, 0, 0.3); + } + + button.success { + color: #FFFFFF; + background-color:rgb(82, 165, 0) + } + + button.failure { + color: #FFFFFF; + background-color: #E02F2F; + } + + .lds-ellipsis { + display: inline-block; + position: relative; + width: 80px; + height: 80px; + } + .lds-ellipsis div { + position: absolute; + top: 33px; + width: 13px; + height: 13px; + border-radius: 50%; + background: #3e3e3e; + animation-timing-function: cubic-bezier(0, 1, 1, 0); + } + .lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; + } + .lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; + } + .lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; + } + .lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; + } + @keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } + } + @keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + } + } + @keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(24px, 0); + } + } + \ No newline at end of file diff --git a/src/templates/payment_form.html b/src/templates/payment_form.html index bd2f08bb66ff362ff02742d8ed6f848bd45ce971..c4348c5e31b1e35b0a6e82f0a76b018a4b099029 100644 --- a/src/templates/payment_form.html +++ b/src/templates/payment_form.html @@ -4,18 +4,163 @@ {% block title %}UCC Payment Gateway{% endblock %} {% block extrahead %} -<!-- link to the SqPaymentForm library --> -<script type="text/javascript" src="https://js.squareup.com/v2/paymentform"></script> +<script type="text/javascript" src="web.squarecdn.com/v1/square.js"></script> {# bring the location IDs into javascript so the next bits know about them #} -<script type="text/javascript"> -var applicationId = "{{ app_id }}"; -var locationId = "{{ loc_id }}"; -var amount = {{ payment.amount }}; +<script type="text/javascript"> + + var appId = "{{ app_id }}"; + var locationId = "{{ loc_id }}"; + var amount = "{{ payment.amount }}"; + + async function initializeCard(payments) { + const card = await payments.card(); + await card.attach('#card-container'); + return card; + } + + // Call this function to send a payment token, buyer name, and other details + // to the project server code so that a payment can be created with + // Payments API + async function createPayment(token, verificationToken) { + const csrfToken = document.querySelector('input[name=csrfmiddlewaretoken]').value; + const body = JSON.stringify({ + locationId, + sourceId: token, + verificationToken: verificationToken, + }); + + const paymentResponse = await fetch(location.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body, + credentials: 'same-origin', + }); + + return paymentResponse.json(); + } + + // This function tokenizes a payment method. + // The ‘error’ thrown from this async function denotes a failed tokenization, + // which is due to buyer error (such as an expired card). It is up to the + // developer to handle the error and provide the buyer the chance to fix + // their mistakes. + async function tokenize(paymentMethod) { + const tokenResult = await paymentMethod.tokenize(); + if (tokenResult.status === 'OK') { + return tokenResult.token; + } else { + let errorMessage = `Tokenization failed-status: ${tokenResult.status}`; + if (tokenResult.errors) { + errorMessage += ` and errors: ${JSON.stringify( + tokenResult.errors + )}`; + } + throw new Error(errorMessage); + } + } + + async function verifyBuyer(payments, token) { + const verificationDetails = { + amount: '{{ payment.amount }}', + /* collected from the buyer */ + billingContact: {}, + currencyCode: 'AUD', + intent: 'CHARGE', + }; + + const verificationResults = await payments.verifyBuyer( + token, + verificationDetails + ); + + return verificationResults.token; + } + + // Helper method for displaying the Payment Status on the screen. + // status is either SUCCESS or FAILURE; + function displayPaymentResults(status) { + const cardButton = document.getElementById( + 'card-button' + ); + if (status === 'SUCCESS') { + cardButton.classList.remove('failure'); + cardButton.classList.add('success'); + cardButton.innerText = 'Payment successful! Redirecting...'; + } else { + cardButton.classList.remove('success'); + cardButton.classList.add('failure'); + cardButton.innerText = 'Payment failed :( Refreshing...'; + } + } + + + document.addEventListener('DOMContentLoaded', async function () { + const loader = document.getElementById('spinner'); + const cardButton = document.getElementById( + 'card-button' + ); + + cardButton.disabled = true; + if (!window.Square) { + throw new Error('Square.js failed to load properly'); + } + const payments = window.Square.payments(appId, locationId); + let card; + try { + card = await initializeCard(payments); + loader.style.display = 'none'; + cardButton.disabled = false; + } catch (e) { + console.error('Initializing Card failed', e); + return; + } + + async function handlePaymentMethodSubmission(event, paymentMethod, shouldVerify = true) { + event.preventDefault(); + const token = await tokenize(paymentMethod); + + cardButton.disabled = true; + cardButton.innerText = "Processing..."; + + let verificationToken; + if (shouldVerify) { + verificationToken = await verifyBuyer( + payments, + token + ); + } + + const paymentResults = await createPayment(token, verificationToken); + if (paymentResults.success) { + displayPaymentResults('SUCCESS'); + setTimeout( + function () { window.location.href = "{{ payment.completed_url }}"; }, 3000 + ); + } else { + cardButton.disabled = false; + displayPaymentResults('FAILURE'); + console.error(paymentResults.errors); + setTimeout( + function () { window.location.href = "{{ payment.completed_url }}"; }, 3000 + ); + } + console.debug('Payment Success', paymentResults); + + } + + cardButton.addEventListener('click', async function (event) { + await handlePaymentMethodSubmission(event, card, true); + }); + + }); </script> -<script type="text/javascript" src="{% static 'squarepay.js' %}"></script> <link rel="stylesheet" type="text/css" href="{% static 'squarepay.css' %}"> + {% endblock %} @@ -24,15 +169,17 @@ var amount = {{ payment.amount }}; <div class="form-container text-center"> <div class="form-header"> <span class="tips {% if payment.is_paid %}error{% endif %}">{% block tips %} - {% if payment.is_paid %} + {% if payment.is_paid %} <p><b>It appears you have already successfully attempted this payment.</b> - {% if payment.completed_url %} + {% if payment.completed_url %} <br><br>Perhaps you were meaning to find <a href="{{ payment.completed_url }}">this page</a>. - {% endif %}</p> - {% else %} + {% endif %} + </p> + {% else %} <b><i>Please fill in your card details below.</i></b> - {% endif %} - {% endblock %}</span> + {% endif %} + </span> + {% endblock %} <noscript> <span class="tips error">Please enable javascript to use the payment form.</span> @@ -51,37 +198,21 @@ var amount = {{ payment.amount }}; </div> <!-- the element #form-container is used to create the Square payment form (hardcoded) --> <div id="form-container" class="payment-info"> - <div id="sq-ccbox"> - <form id="nonce-form" novalidate action="#" method="post"> + <div class="float-container"> {% csrf_token %} - <div id="error"></div> - - <fieldset> - <div class="form-row"> - <span class="label">Card Number</span> - <div id="sq-card-number"></div> - </div> - - <div class="form-row"> - <div class="half"> - <span class="label">Expiration</span> - <div id="sq-expiration-date"></div> - </div> - - <div class="half"> - <span class="label">CVV</span> - <div id="sq-cvv"></div> - </div> - </div> - </fieldset> - <button id="sq-creditcard" class="button-credit-card" onclick="requestCardNonce(event)" disabled>Pay {{ amount }}</button> - - <!-- after a nonce is generated it will be assigned to this hidden input field --> - <input type="hidden" id="card-nonce" name="nonce"> + <div id=spinner class="lds-ellipsis" id="payment-form"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + <form id="payment-form"> + <div id="card-container"></div> + <button id="card-button" type="button">Pay {{ amount }}</button> </form> - </div> <!-- end #sq-ccbox --> + </div> <!-- end #float-container --> </div> <!-- end #form-container --> </div> </div> </div> -{% endblock %} +{% endblock %} \ No newline at end of file