diff --git a/gratipay/main.py b/gratipay/main.py index 52017a86a8..02b65bfee3 100644 --- a/gratipay/main.py +++ b/gratipay/main.py @@ -33,9 +33,11 @@ # This is shared via class inheritance with jinja2_htmlescaped. 'b64encode': base64.b64encode, 'enumerate': enumerate, + 'filter': filter, 'filter_profile_subnav': utils.filter_profile_subnav, 'float': float, 'len': len, + 'map': map, 'range': range, 'str': str, 'to_javascript': utils.to_javascript, diff --git a/js/gratipay/forms.js b/js/gratipay/forms.js index aa991030ef..c8d557b3b5 100644 --- a/js/gratipay/forms.js +++ b/js/gratipay/forms.js @@ -3,57 +3,6 @@ Gratipay.forms = {}; -Gratipay.forms.clearFeedback = function() { - $('#feedback').empty(); -}; - -Gratipay.forms.showFeedback = function(msg, details) { - if (msg === null) - msg = "Failure"; - msg = '

' + msg + '

'; - msg += ''; - $('#feedback').html(msg); - if (details !== undefined) - for (var i=0; i < details.length; i++) - $('#feedback .details').append('
  • ' + details[i] + '
  • '); -}; - -Gratipay.forms.submit = function(url, data, success, error) { - if (success === undefined) { - success = function() { - Gratipay.forms.showFeedback("Success!"); - }; - } - - if (error === undefined) { - error = function(data) { - Gratipay.forms.showFeedback(data.problem); - }; - } - - function _success(data) { - if (data.problem === "" || data.problem === undefined) - success(data); - else - error(data); - } - - function _error(xhr, foo, bar) { - Gratipay.forms.showFeedback( "So sorry!!" - , ["There was a fairly drastic error with your request."] - ); - console.log("failed", xhr, foo, bar); - } - - jQuery.ajax({ url: url - , type: "POST" - , data: data - , dataType: "json" - , success: _success - , error: _error - }); -}; - Gratipay.forms.initCSRF = function() { // https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax jQuery(document).ajaxSend(function(event, xhr, settings) { function sameOrigin(url) { @@ -151,3 +100,15 @@ Gratipay.forms.jsEdit = function(params) { $form.on('submit', post); }; + +Gratipay.forms.clearInvalid = function($form) { + $form.find('.invalid').removeClass('invalid'); +}; + +Gratipay.forms.focusInvalid = function($form) { + $form.find('.invalid').eq(0).focus(); +}; + +Gratipay.forms.setInvalid = function($input, invalid) { + $input.toggleClass('invalid', invalid); +}; diff --git a/js/gratipay/payments.js b/js/gratipay/payments.js index 6108c8aadb..2ab001198f 100644 --- a/js/gratipay/payments.js +++ b/js/gratipay/payments.js @@ -15,7 +15,7 @@ Gratipay.payments = {}; Gratipay.payments.init = function(participantId) { Gratipay.participantId = participantId; - $('#delete form').submit(Gratipay.payments.submitDeleteForm); + $('#delete').submit(Gratipay.payments.submitDeleteForm); // Lazily depend on Balanced. var balanced_js = "https://js.balancedpayments.com/1.1/balanced.min.js"; @@ -25,22 +25,18 @@ Gratipay.payments.init = function(participantId) { } Gratipay.payments.submitDeleteForm = function(e) { - var item = $("#payout").length ? "bank account" : "credit card"; - var slug = $("#payout").length ? "bank-account" : "credit-card"; - var msg = "Really disconnect your " + item + "?"; - if (!confirm(msg)) { - e.stopPropagation(); - e.preventDefault(); + e.stopPropagation(); + e.preventDefault(); + + var $form = $(this); + if (!confirm($form.data('confirm'))) { return false; } - jQuery.ajax( - { url: '/' + slug + '.json' + { url: $form.attr('action') , data: {action: "delete"} , type: "POST" - , success: function() { - window.location.href = '/' + slug + '.html'; - } + , success: function() { window.location.reload(); } , error: Gratipay.error } ); @@ -48,24 +44,16 @@ Gratipay.payments.submitDeleteForm = function(e) { }; Gratipay.payments.onError = function(response) { - $('button#save').css('opacity', 1); + $('button#save').prop('disabled', false); var msg = response.status_code + ": " + $.map(response.errors, function(obj) { return obj.description }).join(', '); - Gratipay.forms.showFeedback(null, [msg]); + Gratipay.notification(msg, 'error', -1); return msg; }; Gratipay.payments.onSuccess = function(data) { - $('#status').text('working').addClass('highlight'); - setTimeout(function() { - $('#status').removeClass('highlight'); - }, 8000); - $('#delete').show(); - Gratipay.forms.clearFeedback(); - $('button#save').css('opacity', 1); - setTimeout(function() { - window.location.href = '/' + Gratipay.participantId + '/'; - }, 1000); + $('button#save').prop('disabled', false); + window.location.reload(); }; @@ -76,14 +64,14 @@ Gratipay.payments.ba = {}; Gratipay.payments.ba.init = function(participantId) { Gratipay.payments.init(participantId); - $('#payout').submit(Gratipay.payments.ba.submit); + $('form#bank-account').submit(Gratipay.payments.ba.submit); }; Gratipay.payments.ba.submit = function (e) { e.preventDefault(); - $('button#save').css('opacity', 0.5); - Gratipay.forms.clearFeedback(); + $('button#save').prop('disabled', true); + Gratipay.forms.clearInvalid($(this)); var bankAccount = { name: $('#account_name').val(), @@ -91,72 +79,20 @@ Gratipay.payments.ba.submit = function (e) { routing_number: $('#routing_number').val() }; - Gratipay.payments.ba.merchantData = { - //type: 'person', // Oooh, may need to vary this some day? - street_address: $('#address_1').val(), - postal_code: $('#zip').val(), - phone_number: $('#phone_number').val(), - region: $('#state').val(), - dob_month: $('#dob-month').val(), - dob_year: $('#dob-year').val(), - dob_day: $('#dob-day').val(), - name: $('#name').val() - }; - var errors = []; - - - // Require some fields. - // ==================== - // We only require fields that are actually on the page. Since we don't - // load the identity verification fields if they're already verified, not - // all of these will necessarily be present at all. - - var requiredFields = { - name: 'Your legal name is required.', - address_1: 'Your street address is required.', - zip: 'Your ZIP or postal code is required.', - phone_number: 'A phone number is required.', - account_name: 'The name on your bank account is required.', - account_number: 'Your bank account number is required.', - routing_number: 'A routing number is required.' - }; - for (var field in requiredFields) { - var $f = $('#' + field); - if (!$f.length) // Only validate if it's on the page. - continue; - var value = $f.val(); - - if (!value) { - $f.closest('div').addClass('error'); - errors.push(requiredFields[field]); - } else { - $f.closest('div').removeClass('error'); - } - } - - // Validate routing number. - // ======================== - - var $rn = $('#routing_number'); if (bankAccount.routing_number) { if (!balanced.bankAccount.validateRoutingNumber(bankAccount.routing_number)) { - $rn.closest('div').addClass('error'); - errors.push("That routing number is invalid."); - } else { - $rn.closest('div').removeClass('error'); + Gratipay.forms.setInvalid($('#routing_number')); + Gratipay.forms.focusInvalid($(this)); + $('button#save').prop('disabled', false); + return false } } - - if (errors.length) { - $('button#save').css('opacity', 1); - Gratipay.forms.showFeedback(null, errors); - } else { - balanced.bankAccount.create( bankAccount - , Gratipay.payments.ba.handleResponse - ); - } + // Okay, send the data to Balanced. + balanced.bankAccount.create( bankAccount + , Gratipay.payments.ba.handleResponse + ); }; Gratipay.payments.ba.handleResponse = function (response) { @@ -167,32 +103,21 @@ Gratipay.payments.ba.handleResponse = function (response) { } /* The request to tokenize the bank account succeeded. Now we need to - * validate the merchant information. We'll submit it to - * /bank-accounts.json and check the response code to see what's going - * on there. + * associate it to the Customer on Balanced and to the participant in + * our DB. */ - function detailedFeedback(data) { - $('#status').text('failing'); - $('#delete').show(); - var messages = [data.error]; - if (data.problem == 'More Info Needed') { - messages = [ "Sorry, we couldn't verify your identity. Please " - + "check, correct, and resubmit your details." - ]; - } - Gratipay.forms.showFeedback(data.problem, messages); - $('button#save').css('opacity', 1); - } - - var detailsToSubmit = Gratipay.payments.ba.merchantData; - detailsToSubmit.bank_account_uri = response.bank_accounts[0].href; - - Gratipay.forms.submit( "/bank-account.json" - , detailsToSubmit - , Gratipay.payments.onSuccess - , detailedFeedback - ); + jQuery.ajax({ + url: "/bank-account.json", + type: "POST", + data: {bank_account_uri: response.bank_accounts[0].href}, + dataType: "json", + success: Gratipay.payments.onSuccess, + error: [ + Gratipay.error, + function() { $('button#save').prop('disabled', false); }, + ], + }); }; @@ -203,7 +128,7 @@ Gratipay.payments.cc = {}; Gratipay.payments.cc.init = function(participantId) { Gratipay.payments.init(participantId); - $('form#payment').submit(Gratipay.payments.cc.submit); + $('form#credit-card').submit(Gratipay.payments.cc.submit); Gratipay.payments.cc.formatInputs( $('#card_number'), $('#expiration_month'), @@ -340,13 +265,13 @@ Gratipay.payments.cc.submit = function(e) { e.stopPropagation(); e.preventDefault(); - $('button#save').css('opacity', 0.5); - Gratipay.forms.clearFeedback(); + $('button#save').prop('disabled', true); + Gratipay.forms.clearInvalid($(this)); // Adapt our form lingo to balanced nomenclature. function val(field) { - return $('form#payment input[id="' + field + '"]').val(); + return $('form#credit-card #'+field).val(); } var credit_card = {}; // holds CC info @@ -378,23 +303,23 @@ Gratipay.payments.cc.submit = function(e) { var year = val('expiration_year'); credit_card.expiration_year = year.length == 2 ? '20' + year : year; - if (!balanced.card.isCardNumberValid(credit_card.number)) { - $('button#save').css('opacity', 1); - Gratipay.forms.showFeedback(null, ["Your card number is bad."]); - } else if (!balanced.card.isExpiryValid( credit_card.expiration_month - , credit_card.expiration_year - )) { - $('button#save').css('opacity', 1); - Gratipay.forms.showFeedback(null, ["Your expiration date is bad."]); - } else if (!balanced.card.isSecurityCodeValid( credit_card.number - , credit_card.cvv - )) { - $('button#save').css('opacity', 1); - Gratipay.forms.showFeedback(null, ["Your CVV is bad."]); - } else { - balanced.card.create(credit_card, Gratipay.payments.cc.handleResponse); + var is_card_number_invalid = !balanced.card.isCardNumberValid(credit_card.number); + var is_expiry_invalid = !balanced.card.isExpiryValid(credit_card.expiration_month, + credit_card.expiration_year); + var is_cvv_invalid = !balanced.card.isSecurityCodeValid(credit_card.number, + credit_card.cvv); + + Gratipay.forms.setInvalid($('#card_number'), is_card_number_invalid); + Gratipay.forms.setInvalid($('#expiration_month'), is_expiry_invalid); + Gratipay.forms.setInvalid($('#cvv'), is_cvv_invalid); + + if (is_card_number_invalid || is_expiry_invalid || is_cvv_invalid) { + $('button#save').prop('disabled', false); + Gratipay.forms.focusInvalid($(this)); + return false; } + balanced.card.create(credit_card, Gratipay.payments.cc.handleResponse); return false; }; @@ -413,17 +338,15 @@ Gratipay.payments.cc.handleResponse = function(response) { * card is good. */ - function detailedFeedback(data) { - $('#status').text('failing'); - $('#delete').show(); - var details = []; - Gratipay.forms.showFeedback(data.problem, [data.error]); - $('button#save').css('opacity', 1); - } - - Gratipay.forms.submit( "/credit-card.json" - , {card_uri: response.cards[0].href} - , Gratipay.payments.onSuccess - , detailedFeedback - ); + jQuery.ajax({ + url: "/credit-card.json", + type: "POST", + data: {card_uri: response.cards[0].href}, + dataType: "json", + success: Gratipay.payments.onSuccess, + error: [ + Gratipay.error, + function() { $('button#save').prop('disabled', false); }, + ], + }); }; diff --git a/scss/pages/cc-ba.scss b/scss/pages/cc-ba.scss index ae883b40cc..d660f38941 100644 --- a/scss/pages/cc-ba.scss +++ b/scss/pages/cc-ba.scss @@ -1,43 +1,35 @@ .cc-ba { - text-align: left; + width: 300px; - .constrain-width { - width: 300px; - margin-bottom: 20px; - } form { + margin-bottom: 1.2em; h2 { font: bold 14px $Ideal; text-transform: uppercase; } } - .half { - width: 150px; - } - .full { - clear: both; - text-align: right; - padding-top: 5px; - } .info { font: normal 11px/13px $Ideal; - padding-top:20px; - width: 300px; - } - .left { - float: left; - } - .right { - float: left; + padding-top: 20px; } label { display: block; font: normal 10px $Ideal; margin: 8px 0 0; padding: 0; + } + label[for], label > span:first-child { + display: block; text-transform: uppercase; } + .half { + display: inline-block; + width: 148px; + } + .half.right { + padding-left: 5px; + } input[type="radio"] + label { display: inline; margin: 0; @@ -47,46 +39,22 @@ } input:not([type]), input[type="text"] { font: normal 11pt/14pt $Ideal; - width: 292px; + width: 100%; margin: 0; padding: 3px; border: 1px solid $light-brown; outline: none; } - input.disabled { - color: $light-brown; - } input[type="radio"] { vertical-align: middle; } - .half input { - width: 137px; - } - .right.half label, - .right.half input { - margin-left: 5px; - } input:focus { border-color: $green; } - .float { - float: left; - } - .city input { width: 137px; } - .state input { width: 43px; } - .zip input { width: 75px; } - .card_number input { width: 292px; } - .cvv input { width: 50px; } - input.expiration_month { width: 35px;} - input.expiration_year { width: 35px; margin-left: 1px !important;} - .not-first label, - .not-first input { - margin-left: 10px; - } - .nav { - text-align: center; - } + input#cvv { width: 50px; } + input#expiration_month { width: 35px; } + input#expiration_year { width: 35px; margin-left: 1px !important; } #feedback { .details li { margin: 0; @@ -98,4 +66,41 @@ select { font-size: 12.15px; // hack for Chosen to get a 292px width } + #save { + margin-top: 1.2em; + } + + input.invalid { + box-shadow: rgb(255, 0, 0) 0 0 1.5px 1px; + } + input.invalid:focus { + box-shadow: rgba(255, 0, 0, 0.4) 0 0 2px 2px; + } + .invalid-msg { + background: rgba(240, 0, 0, 0.9); + border-radius: 5px; + color: #fff; + display: none; + font: normal 11px/13px $Ideal; + margin: 7px 0 0 -12px; + padding: 12px; + position: absolute; + + &::before { + background: transparent; + border-style: solid; + border-width: 8px; + border-color: transparent; + border-bottom-color: rgba(240, 0, 0, 0.9); + content: ''; + height: 0; + margin-left: -8px; + position: absolute; + top: -16px; + width: 0; + } + } + input.invalid:focus + .invalid-msg { + display: block; + } } diff --git a/templates/profile-subnav.html b/templates/profile-subnav.html index b1149f989a..bcd82bd434 100644 --- a/templates/profile-subnav.html +++ b/templates/profile-subnav.html @@ -11,6 +11,7 @@ , ('/'+u+'/giving/', _('Giving'), True, False) , ('/'+u+'/history/', _('History'), True, False) , ('/'+u+'/widgets/', _('Widgets'), True, False) + , ('/'+u+'/identity', _('Identity'), True, False) , ('/'+u+'/settings/', _('Settings'), True, False) , ('/'+u+'/events/', _('Events'), False, False) ] %} diff --git a/www/%username/identity.spt b/www/%username/identity.spt new file mode 100644 index 0000000000..4da53c09b3 --- /dev/null +++ b/www/%username/identity.spt @@ -0,0 +1,151 @@ +import balanced + +from gratipay.billing.exchanges import repr_exception +from gratipay.utils import get_participant + +[---] +request.allow('GET', 'POST') +participant = get_participant(state, restrict=True) +title = _("Identity Verification") +error = '' + +if request.method == 'POST': + account = participant.get_balanced_account() + + body = request.body + account.name = body.get('name') + account.address['line1'] = body.get('address_1') + account.address['line2'] = body.get('address_2') + account.address['postal_code'] = body.get('postal_code') + account.address['city'] = body.get('city') + account.address['state'] = body.get('region') + account.address['country_code'] = body.get('country') + account.phone = body.get('phone_number') + account.ssn_last4 = body.get('ssn_last4') or None + + dob = body.get('dob', '') + if dob: + try: + account.dob_year, account.dob_month, account.meta['dob_day'] = \ + map(int, dob.split('-')) + except ValueError: + error = _("Invalid date of birth.") + + if not error: + # This will possibly fail with 400 if formatted badly, or 300 if we + # cannot identify the merchant. + try: + account.save() + except balanced.exc.HTTPError as err: + error = repr_exception(err) + else: + if account.merchant_status != 'underwritten': + error = _("Unable to verify your identity") + +elif participant.balanced_customer_href: + account = participant.get_balanced_account() + +else: + account = balanced.Customer(address={}, meta={}) + +[---] text/html +{% extends "templates/profile.html" %} + +{% block scripts %} + {{ super() }} + +{% endblock %} + +{% block content %} + +{% if account.merchant_status == 'underwritten' %} +

    {{ _( "Your identity is verified, which means you may add a {0}bank account{1}." + , ''|safe + , ''|safe) }}

    +{% endif %} + +
    +
    + + + + + +
    + + + + + + + + + + + +
    + + + + + + + +
    + + + +
    +
    + + + +
    +
    + +{% endblock %} diff --git a/www/%username/settings/close.spt b/www/%username/settings/close.spt index fda54b065e..e63c492511 100644 --- a/www/%username/settings/close.spt +++ b/www/%username/settings/close.spt @@ -32,7 +32,7 @@ if request.method == 'POST':
    {% if payday_is_running %} -
    +

    Try Again Later

    @@ -44,7 +44,7 @@ if request.method == 'POST': {% else %} -
    +
    {% if participant.balance > 0 %}

    Balance

    diff --git a/www/bank-account.html.spt b/www/bank-account.html.spt index 15f2e9ec4b..adc93a11d5 100644 --- a/www/bank-account.html.spt +++ b/www/bank-account.html.spt @@ -1,11 +1,5 @@ -import traceback -from datetime import datetime, timedelta - -import balanced -from aspen import json, log, Response -from gratipay import billing, MONTHS +from gratipay import billing from gratipay.billing.exchanges import MINIMUM_CREDIT -from gratipay.elsewhere import github [-----------------------------------------------------------------------------] balanced_customer_href = None @@ -14,19 +8,18 @@ bank_account = None status = ". . ." if not user.ANON: - balanced_customer_href = user.participant.balanced_customer_href - last_ach_result = user.participant.last_ach_result + participant = user.participant + balanced_customer_href = participant.balanced_customer_href + last_ach_result = participant.last_ach_result status = _("Your bank account is {0}not connected{1}") if balanced_customer_href: if last_ach_result == "": status = _("Your bank account is {0}connected{1}") - account = user.participant.get_balanced_account() + account = participant.get_balanced_account() bank_account = billing.BalancedBankAccount(balanced_customer_href) - username = user.participant.username - title = _("Bank Account") [-----------------------------------------------------------------------------] {% extends "templates/base.html" %} @@ -62,151 +55,59 @@ title = _("Bank Account") {{ _("and then you'll be able to add or change your bank account.") }} {% else %} - {% if bank_account and bank_account.is_setup %} -

    Current: {{ bank_account['bank_name'] }} - ******{{ bank_account['account_number'][-4:] }}

    - {% endif %} -
    {% if last_ach_result %}

    {{ _("Failure") }}

    {{ last_ach_result }}

    {% endif %}
    - + {% if account and account.merchant_status == 'underwritten' %}
    - -
    - - {% if account and 'merchant' in account.roles %} -

    {{ _("Identity Verification") }}  

    -

    {{ _("Routing Information") }}

    - {% endif %} - - - {% if not account or 'merchant' not in account.roles %} -

    {{ _("Identity Verification") }}

    - -
    - - -
    - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - - -
    - -
    - - -
    - -
    - - - - -
    - -
    - -
    - - -
    - -
    - - -

    {{ _("Routing Information") }}

    - {% endif %} - - -
    - - -
    - -
    - - -
    - - - -
    -
    + + +

    {{ _("Routing Information") }}

    + + + + + + - + {% if bank_account and bank_account.is_setup %} -
    -
    - - -
    -
    +
    + +
    {% endif %} -

    Bank account information is stored and processed by Balanced Payments. Here - are their Terms of - Service and Privacy - Policy.

    +

    Bank account information is stored and processed by + Balanced Payments. Here + are their Terms of Service + and Privacy Policy. +

    - +
    + {% else %} +

    {{ + _("You need to verify your identity first.") + }}

    + {% endif %} {% endif %} {% endblock %} diff --git a/www/bank-account.json.spt b/www/bank-account.json.spt index ec48e4a246..8993553722 100644 --- a/www/bank-account.json.spt +++ b/www/bank-account.json.spt @@ -1,14 +1,8 @@ -""" -""" +from aspen import Response import balanced -import urllib -import gratipay -from aspen import Response from gratipay import billing -base_url = gratipay.canonical_scheme + "://" + gratipay.canonical_host - [-----------------------------------------------------------------------------] if user.ANON: @@ -37,55 +31,28 @@ else: balanced_account = participant.get_balanced_account() - # Ensure the user is a merchant. + # Ensure the user is identified. # ============================== - # This will possibly fail with 400 if formatted badly, or 300 if we cannot - # identify the merchant. - out = {} if balanced_account.merchant_status != 'underwritten': - - balanced_account.name = body.get('name') - balanced_account.address['line1'] = body.get('street_address') - balanced_account.address['postal_code'] = body.get('postal_code') - balanced_account.address['state'] = body.get('region') - balanced_account.phone = body.get('phone_number') - balanced_account.dob_month = body.get('dob_month') - balanced_account.dob_year = body.get('dob_year') - balanced_account.meta['dob_day'] = body.get('dob_day') - balanced_account.ssn_last4 = body.get('ssn_last4') - - try: - balanced_account.save() - except balanced.exc.HTTPError as err: - out = {"problem": "Problem", "error": err.message} - - if balanced_account.merchant_status != 'underwritten': - out = { 'problem': 'More Info Needed' - , 'error': 'Unable to verify your identity' - } + raise Response(400, _("You need to verify your identity first.")) # No errors? Great! Let's add the bank account. # ============================================= - if not out: - - # Clear out any old ones first. - billing.clear(website.db, u"bank account", participant, balanced_account) + # Clear out any old ones first. + billing.clear(website.db, u"bank account", participant, balanced_account) - bank_account_uri = body['bank_account_uri'] - try: - billing.associate( website.db + bank_account_uri = body['bank_account_uri'] + error = billing.associate( website.db , u"bank account" , participant , balanced_account , bank_account_uri ) - except balanced.exc.HTTPError as err: - out = {"problem": "Problem", "error": err.message} - else: - out = {"problem": ""} + if error: + raise Response(400, error) [---] application/json via json_dump out diff --git a/www/credit-card.html.spt b/www/credit-card.html.spt index 6c3d4b97b3..14b9c19fd1 100644 --- a/www/credit-card.html.spt +++ b/www/credit-card.html.spt @@ -1,9 +1,4 @@ -import traceback - -import balanced -from aspen import json, log, Response from gratipay import billing -from gratipay.elsewhere import github [-----------------------------------------------------------------------------] @@ -65,116 +60,103 @@ title = _("Credit Card") {{ _("and then you'll be able to add or change your credit card.") }} {% else %} - {% if status != "missing" %} -

    {{ _("Current:") }} {{ card['brand'] }} {{ card['number'][-4:] }}

    - {% endif %} -
    {% if last_bill_result %}

    {{ _("Failure") }}

    {{ last_bill_result }}

    {% endif %}
    - -
    -
    -
    - -

    {{ _("Required") }}

    - -
    - - -
    - -
    -
    - - - / - -
    - -
    - - -
    - -
    + + +

    {{ _("Required") }}

    + + + +
    + + + {{ _("This expiration date is invalid.") }} + / + +
    -
    - {{ _("To minimize processing fees, we charge your credit card at least $10 at a time \ - (anything extra stays in Gratipay to use in future weeks). {0}Read More{1}", - ""|safe, - ""|safe) }} -
    + + +
    + {{ _("To minimize processing fees, we charge your credit card at least $10 at a time " + "(anything extra stays in Gratipay to use in future weeks). {0}Read More{1}", + ""|safe, + ""|safe) }} +
    -

    Optional

    +

    Optional

    - + -
    - - + - - - + - + + + -
    - - -
    - -
    - - -
    - -
    - - - - {% for each in locale.countries.items() %} {% endfor %} - -
    +
    -
    -
    - - -
    -
    - -

    Credit card information is stored and processed by Balanced Payments. + {% if status != "missing" %} +

    + +
    + {% endif %} - Here are their Terms of - Service and Privacy Policy.

    +

    Credit card information is stored and processed by + Balanced Payments. Here + are their Terms of Service + and Privacy Policy. +

    diff --git a/www/credit-card.json.spt b/www/credit-card.json.spt index fcfc69fd28..563d6b7a56 100644 --- a/www/credit-card.json.spt +++ b/www/credit-card.json.spt @@ -44,11 +44,8 @@ else: , balanced_account , card_uri ) - if error: - out = {"problem": "Problem", "error": error} - else: - out = {"problem": ""} + raise Response(400, error) [---] application/json via json_dump out