diff --git a/payment/forms.py b/payment/forms.py new file mode 100644 index 00000000..db65a947 --- /dev/null +++ b/payment/forms.py @@ -0,0 +1,7 @@ +from django import forms +import logging + +logger = logging.getLogger(__name__) + +class StripePledgeForm(forms.Form): + stripe_token = forms.CharField(required=False, widget=forms.HiddenInput()) diff --git a/payment/stripelib.py b/payment/stripelib.py new file mode 100644 index 00000000..db01753a --- /dev/null +++ b/payment/stripelib.py @@ -0,0 +1,268 @@ +# https://github.com/stripe/stripe-python +# https://stripe.com/docs/api?lang=python#top + +from datetime import datetime +from pytz import utc + +import stripe + +try: + import unittest + from unittest import TestCase +except: + from django.test import TestCase + from django.utils import unittest + +# if customer.id doesn't exist, create one and then charge the customer +# we probably should ask our users whether they are ok with our creating a customer id account -- or ask for credit +# card info each time.... + +# should load the keys for Stripe from db -- but for now just hardcode here +# moving towards not having the stripe api key for the non profit partner in the unglue.it code -- but in a logically +# distinct application + +try: + from regluit.core.models import Key + STRIPE_PK = Key.objects.get(name="STRIPE_PK").value + STRIPE_SK = Key.objects.get(name="STRIPE_SK").value + STRIPE_PARTNER_PK = Key.objects.get(name="STRIPE_PARTNER_PK").value + STRIPE_PARTNER_SK = Key.objects.get(name="STRIPE_PARTNER_SK").value + logger.info('Successful loading of STRIPE_*_KEYs') +except Exception, e: + # currently test keys for Gluejar and for raymond.yee@gmail.com as standin for non-profit + STRIPE_PK = 'pk_0EajXPn195ZdF7Gt7pCxsqRhNN5BF' + STRIPE_SK = 'sk_0EajIO4Dnh646KPIgLWGcO10f9qnH' + STRIPE_PARTNER_PK ='pk_0AnIkNu4WRiJYzxMKgruiUwxzXP2T' + STRIPE_PARTNER_SK = 'sk_0AnIvBrnrJoFpfD3YmQBVZuTUAbjs' + +# set default stripe api_key to that of unglue.it + +stripe.api_key = STRIPE_SK + +# https://stripe.com/docs/testing + +TEST_CARDS = ( + ('4242424242424242', 'Visa'), + ('4012888888881881', 'Visa'), + ('5555555555554444', 'MasterCard'), + ('5105105105105100', 'MasterCard'), + ('378282246310005', 'American Express'), + ('371449635398431', 'American Express'), + ('6011111111111117', 'Discover'), + ('6011000990139424', 'Discover'), + ('30569309025904', "Diner's Club"), + ('38520000023237', "Diner's Club"), + ('3530111333300000', 'JCB'), + ('3566002020360505','JCB') +) + +ERROR_TESTING = dict(( + ('ADDRESS1_ZIP_FAIL', ('4000000000000010', 'address_line1_check and address_zip_check will both fail')), + ('ADDRESS1_FAIL', ('4000000000000028', 'address_line1_check will fail.')), + ('ADDRESS_ZIP_FAIL', ('4000000000000036', 'address_zip_check will fail.')), + ('CVC_CHECK_FAIL', ('4000000000000101', 'cvc_check will fail.')), + ('BAD_ATTACHED_CARD', ('4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.')), + ('CHARGE_DECLINE', ('4000000000000002', 'Charges with this card will always be declined.')) +)) + +# types of errors / when they can be handled + +#card_declined: Use this special card number - 4000000000000002. +#incorrect_number: Use a number that fails the Luhn check, e.g. 4242424242424241. +#invalid_expiry_month: Use an invalid month e.g. 13. +#invalid_expiry_year: Use a year in the past e.g. 1970. +#invalid_cvc: Use a two digit number e.g. 99. + + + +def filter_none(d): + return dict([(k,v) for (k,v) in d.items() if v is not None]) + +# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token. + +# https://stripe.com/docs/tutorials/charges + +def card (number=TEST_CARDS[0][0], exp_month='01', exp_year='2020', cvc=None, name=None, + address_line1=None, address_line2=None, address_zip=None, address_state=None, address_country=None): + + card = { + "number": number, + "exp_month": str(exp_month), + "exp_year": str(exp_year), + "cvc": str(cvc) if cvc is not None else None, + "name": name, + "address_line1": address_line1, + "address_line2": address_line2, + "address_zip": address_zip, + "address_state": address_state, + "address_country": address_country + } + + return filter_none(card) + + +class StripeClient(object): + def __init__(self, api_key=STRIPE_SK): + self.api_key = api_key + + # key entities: Charge, Customer, Token, Event + + @property + def charge(self): + return stripe.Charge(api_key=self.api_key) + + @property + def customer(self): + return stripe.Customer(api_key=self.api_key) + + @property + def token(self): + return stripe.Token(api_key=self.api_key) + + @property + def transfer(self): + return stripe.Transfer(api_key=self.api_key) + + @property + def event(self): + return stripe.Event(api_key=self.api_key) + + + def create_token(self, card): + return stripe.Token(api_key=self.api_key).create(card=card) + + def create_customer (self, card=None, description=None, email=None, account_balance=None, plan=None, trial_end=None): + """card is a dictionary or a token""" + # https://stripe.com/docs/api?lang=python#create_customer + + customer = stripe.Customer(api_key=self.api_key).create( + card=card, + description=description, + email=email, + account_balance=account_balance, + plan=plan, + trial_end=trial_end + ) + + # customer.id is useful to save in db + return customer + + + def create_charge(self, amount, currency="usd", customer=None, card=None, description=None ): + # https://stripe.com/docs/api?lang=python#create_charge + # customer or card required but not both + # charge the Customer instead of the card + # amount in cents + charge = stripe.Charge(api_key=self.api_key).create( + amount=int(100*amount), # in cents + currency=currency, + customer=customer.id, + description=description + ) + + return charge + + def refund_charge(self, charge_id): + # https://stripe.com/docs/api?lang=python#refund_charge + ch = stripe.Charge(api_key=self.api_key).retrieve(charge_id) + ch.refund() + return ch + + def list_all_charges(self, count=None, offset=None, customer=None): + # https://stripe.com/docs/api?lang=python#list_charges + return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer) + +# what to work through? + +# can't test Transfer in test mode: "There are no transfers in test mode." + +#pledge scenario +# bad card -- what types of erros to handle? +# https://stripe.com/docs/api#errors + +# what errors are handled in the python library and how? +# + +# Account? + +# https://stripe.com/docs/api#event_types +# events of interest -- especially ones that do not directly arise immediately (synchronously) from something we do -- I think +# especially: charge.disputed +# I think following (charge.succeeded, charge.failed, charge.refunded) pretty much sychronous to our actions +# customer.created, customer.updated, customer.deleted + +# transfer +# I expect the ones related to transfers all happen asynchronously: transfer.created, transfer.updated, transfer.failed + +# When will the money I charge with Stripe end up in my bank account? +# Every day, we transfer the money that you charged seven days previously?that is, you receive the money for your March 1st charges on March 8th. + +# pending payments? +# how to tell whether money transferred to bank account yet +# best practices for calling Events -- not too often. + +class PledgeScenarioTest(TestCase): + @classmethod + def setUpClass(cls): + print "in setUp" + cls._sc = StripeClient(api_key=STRIPE_SK) + + # valid card + card0 = card() + cls._good_cust = cls._sc.create_customer(card=card0, description="test good customer", email="raymond.yee@gmail.com") + + # bad card + test_card_num_to_get_BAD_ATTACHED_CARD = ERROR_TESTING['BAD_ATTACHED_CARD'][0] + card1 = card(number=test_card_num_to_get_BAD_ATTACHED_CARD) + cls._cust_bad_card = cls._sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com") + + def test_charge_good_cust(self): + charge = self._sc.create_charge(10, customer=self._good_cust, description="$10 for good cust") + print charge.id + + + def test_error_creating_customer_with_declined_card(self): + # should get a CardError upon attempt to create Customer with this card + _card = card(number=card(ERROR_TESTING['CHARGE_DECLINE'][0])) + self.assertRaises(stripe.CardError, self._sc.create_customer, card=_card) + + def test_charge_bad_cust(self): + # expect the card to be declined -- and for us to get CardError + self.assertRaises(stripe.CardError, self._sc.create_charge, 10, + customer = self._cust_bad_card, description="$10 for bad cust") + @classmethod + def tearDownClass(cls): + # clean up stuff we create in test + print "in tearDown" + cls._good_cust.delete() + print "list of customers" + print [(i, c.id, c.description, datetime.fromtimestamp(c.created, tz=utc), c.account_balance) for(i, c) in enumerate(cls._sc.customer.all()["data"])] + + print "list of charges" + print [(i, c.id, c.amount, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid, c.fee, c.disputed, c.amount_refunded, c.failure_message, c.card.fingerprint, c.card.last4) for (i, c) in enumerate(cls._sc.charge.all()['data'])] + + # can retrieve events since a certain time + print "list of events", cls._sc.event.all() + # [(i, e.id, e.type, e.created, e.pending_webhooks, e.data) for (i,e) in enumerate(s.event.all()['data'])] + +def suite(): + + testcases = [PledgeScenarioTest] + #testcases = [] + suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases]) + #suites.addTest(LibraryThingTest('test_cache')) + #suites.addTest(SettingsTest('test_dev_me_alignment')) # give option to test this alignment + return suites + + +# IPNs/webhooks: https://stripe.com/docs/webhooks +# how to use pending_webhooks ? + +# all events +# https://stripe.com/docs/api?lang=python#list_events + +if __name__ == '__main__': + #unittest.main() + suites = suite() + #suites = unittest.defaultTestLoader.loadTestsFromModule(__import__('__main__')) + unittest.TextTestRunner().run(suites) diff --git a/payment/templates/balanced.html b/payment/templates/balanced.html new file mode 100644 index 00000000..009acdc5 --- /dev/null +++ b/payment/templates/balanced.html @@ -0,0 +1,119 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Balanced{% endblock %} + +{% block extra_extra_head %} + + + + + + + +{% endblock %} + +{% block doccontent %} + +

Balanced Sample - Collect Credit Card Information

+
+
+
+ {% csrf_token %} +
+ + +
+
+ + / +
+
+ + +
+ +
+
+
+
+ + + +{% endblock %} + diff --git a/payment/templates/stripe.html b/payment/templates/stripe.html new file mode 100644 index 00000000..c0dfbacb --- /dev/null +++ b/payment/templates/stripe.html @@ -0,0 +1,27 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Stripe{% endblock %} + +{% block extra_extra_head %} + + + + + + +{% endblock %} + +{% block doccontent %} +Stripe Test!: + + +
+ {% csrf_token %} + + +
+ + +{% endblock %} + diff --git a/payment/templates/wepay.html b/payment/templates/wepay.html new file mode 100644 index 00000000..67e73871 --- /dev/null +++ b/payment/templates/wepay.html @@ -0,0 +1,24 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}WePay{% endblock %} + +{% block extra_extra_head %} + + + +{% endblock %} + +{% block doccontent %} + +
+ + + + + + +{% endblock %} + diff --git a/payment/urls.py b/payment/urls.py index c91c6dd4..7528cb47 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -1,5 +1,6 @@ from django.conf.urls.defaults import * from django.conf import settings +from regluit.payment.views import StripeView urlpatterns = patterns( "regluit.payment.views", @@ -21,6 +22,7 @@ if settings.DEBUG: url(r"^testfinish", "testFinish"), url(r"^testrefund", "testRefund"), url(r"^testmodify", "testModify"), + url(r"^stripe/test", StripeView.as_view()) ) \ No newline at end of file diff --git a/payment/views.py b/payment/views.py index 60f2f449..c0950859 100644 --- a/payment/views.py +++ b/payment/views.py @@ -1,6 +1,11 @@ from regluit.payment.manager import PaymentManager from regluit.payment.models import Transaction from regluit.core.models import Campaign, Wishlist + +from regluit.payment.stripelib import STRIPE_PK + +from regluit.payment.forms import StripePledgeForm + from django.conf import settings from django.core.urlresolvers import reverse from django.shortcuts import render_to_response @@ -12,6 +17,9 @@ from django.views.decorators.csrf import csrf_exempt from django.test.utils import setup_test_environment from django.template import RequestContext +from django.views.generic.edit import FormView +from django.views.generic.base import TemplateView + from unittest import TestResult @@ -283,5 +291,25 @@ def checkStatus(request): def _render(request, template, template_vars={}): return render_to_response(template, template_vars, RequestContext(request)) +class StripeView(FormView): + template_name="stripe.html" + form_class = StripePledgeForm + + def get_context_data(self, **kwargs): + + context = super(StripeView, self).get_context_data(**kwargs) - \ No newline at end of file + context.update({ + 'STRIPE_PK':STRIPE_PK + }) + return context + + def form_valid(self, form): + stripe_token = form.cleaned_data["stripe_token"] + # e.g., tok_0C0k4jG5B2Oxox + # + return HttpResponse("stripe_token: {0}".format(stripe_token)) + + + + diff --git a/requirements.pip b/requirements.pip index c0d9f83e..1ecd2c91 100644 --- a/requirements.pip +++ b/requirements.pip @@ -28,3 +28,4 @@ pycrypto django-maintenancemode django-smtp-ssl django-ckeditor +stripe diff --git a/requirements_versioned.pip b/requirements_versioned.pip index dfd0d088..27e7b443 100644 --- a/requirements_versioned.pip +++ b/requirements_versioned.pip @@ -53,4 +53,5 @@ requests==0.9.1 selenium==2.24.0 South==0.7.3 ssh==1.7.13 +stripe==1.7.4 wsgiref==0.1.2 diff --git a/static/stripe/tag.css b/static/stripe/tag.css new file mode 100755 index 00000000..4476ec5a --- /dev/null +++ b/static/stripe/tag.css @@ -0,0 +1,58 @@ +payment, .payment { + position: relative; + display: block; + + padding: 15px 20px; + max-width: 300px; + overflow: hidden; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; +} + +payment label, .payment label { + display: block; + padding: 5px 0; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +payment input, .payment input { + padding: 5px 5px; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; +} + +payment .number input, .payment .number input { + padding: 7px 40px 7px 7px; + width: 100%; +} + +payment .expiry input, payment .cvc input { + width: 45px; +} + +payment .expiry em { + display: none; +} + +payment .cvc, +.payment .cvc { + float: right; + text-align: right; +} + +payment .expiry, +.payment .expiry { + float: left; +} + +payment .message, +.payment .message { + display: block; +} \ No newline at end of file diff --git a/static/stripe/tag.dev.js b/static/stripe/tag.dev.js new file mode 100755 index 00000000..1bbac091 --- /dev/null +++ b/static/stripe/tag.dev.js @@ -0,0 +1,405 @@ +(function() { + var $, global, script, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = [].slice; + + $ = this.jQuery || this.Zepto; + + if (!$) { + throw 'jQuery/Zepto required'; + } + + this.PaymentTag = (function() { + + PaymentTag.replaceTags = function(element) { + var _this = this; + if (element == null) { + element = document.body; + } + return $('payment, .payment-tag', element).each(function(i, tag) { + return new _this({ + el: tag + }).render(); + }); + }; + + PaymentTag.prototype.defaults = { + tokenName: 'stripe_token', + token: true, + cvc: true + }; + + function PaymentTag(options) { + var _ref, _ref1; + if (options == null) { + options = {}; + } + this.changeCardType = __bind(this.changeCardType, this); + + this.restrictNumeric = __bind(this.restrictNumeric, this); + + this.formatNumber = __bind(this.formatNumber, this); + + this.handleToken = __bind(this.handleToken, this); + + this.submit = __bind(this.submit, this); + + this.$el = options.el || ''; + this.$el = $(this.$el); + options.key || (options.key = this.$el.attr('key') || this.$el.attr('data-key')); + if ((_ref = options.cvc) == null) { + options.cvc = !((this.$el.attr('nocvc') != null) || (this.$el.attr('data-nocvc') != null)); + } + if ((_ref1 = options.token) == null) { + options.token = !((this.$el.attr('notoken') != null) || (this.$el.attr('data-notoken') != null)); + } + options.form || (options.form = this.$el.parents('form')); + this.options = $.extend({}, this.defaults, options); + if (this.options.key) { + this.setKey(this.options.key); + } + this.setForm(this.options.form); + this.$el.delegate('.number input', 'keydown', this.formatNumber); + this.$el.delegate('.number input', 'keyup', this.changeCardType); + this.$el.delegate('input[type=tel]', 'keypress', this.restrictNumeric); + } + + PaymentTag.prototype.render = function() { + this.$el.html(this.constructor.view(this)); + this.$number = this.$('.number input'); + this.$cvc = this.$('.cvc input'); + this.$expiryMonth = this.$('.expiry input.expiryMonth'); + this.$expiryYear = this.$('.expiry input.expiryYear'); + this.$message = this.$('.message'); + return this; + }; + + PaymentTag.prototype.renderToken = function(token) { + this.$token = $(''); + this.$token.attr('name', this.options.tokenName); + this.$token.val(token); + return this.$el.html(this.$token); + }; + + PaymentTag.prototype.setForm = function($form) { + this.$form = $($form); + return this.$form.bind('submit.payment', this.submit); + }; + + PaymentTag.prototype.setKey = function(key) { + this.key = key; + return Stripe.setPublishableKey(this.key); + }; + + PaymentTag.prototype.validate = function() { + var expiry, valid; + valid = true; + this.$('div').removeClass('invalid'); + this.$message.empty(); + if (!Stripe.validateCardNumber(this.$number.val())) { + valid = false; + this.handleError({ + code: 'invalid_number' + }); + } + expiry = this.expiryVal(); + if (!Stripe.validateExpiry(expiry.month, expiry.year)) { + valid = false; + this.handleError({ + code: 'expired_card' + }); + } + if (this.options.cvc && !Stripe.validateCVC(this.$cvc.val())) { + valid = false; + this.handleError({ + code: 'invalid_cvc' + }); + } + if (!valid) { + this.$('.invalid input:first').select(); + } + return valid; + }; + + PaymentTag.prototype.createToken = function(callback) { + var complete, expiry, + _this = this; + complete = function(status, response) { + if (response.error) { + return callback(response.error); + } else { + return callback(null, response); + } + }; + expiry = this.expiryVal(); + return Stripe.createToken({ + number: this.$number.val(), + cvc: this.$cvc.val() || null, + exp_month: expiry.month, + exp_year: expiry.year + }, complete); + }; + + PaymentTag.prototype.submit = function(e) { + if (e != null) { + e.preventDefault(); + } + if (e != null) { + e.stopImmediatePropagation(); + } + if (!this.validate()) { + return; + } + if (this.pending) { + return; + } + this.pending = true; + this.disableInputs(); + this.trigger('pending'); + this.$el.addClass('pending'); + return this.createToken(this.handleToken); + }; + + PaymentTag.prototype.handleToken = function(err, response) { + this.enableInputs(); + this.trigger('complete'); + this.$el.removeClass('pending'); + this.pending = false; + if (err) { + return this.handleError(err); + } else { + this.trigger('success', response); + this.$el.addClass('success'); + if (this.options.token) { + this.renderToken(response.id); + } + this.$form.unbind('submit.payment', this.submit); + return this.$form.submit(); + } + }; + + PaymentTag.prototype.formatNumber = function(e) { + var digit, lastDigits, value; + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + value = this.$number.val(); + if (Stripe.cardType(value) === 'American Express') { + lastDigits = value.match(/^(\d{4}|\d{4}\s\d{6})$/); + } else { + lastDigits = value.match(/(?:^|\s)(\d{4})$/); + } + if (lastDigits) { + return this.$number.val(value + ' '); + } + }; + + PaymentTag.prototype.restrictNumeric = function(e) { + var char; + if (e.shiftKey || e.metaKey) { + return true; + } + if (e.which === 0) { + return true; + } + char = String.fromCharCode(e.which); + return !/[A-Za-z]/.test(char); + }; + + PaymentTag.prototype.cardTypes = { + 'Visa': 'visa', + 'American Express': 'amex', + 'MasterCard': 'mastercard', + 'Discover': 'discover', + 'Unknown': 'unknown' + }; + + PaymentTag.prototype.changeCardType = function(e) { + var map, name, type, _ref; + type = Stripe.cardType(this.$number.val()); + if (!this.$number.hasClass(type)) { + _ref = this.cardTypes; + for (name in _ref) { + map = _ref[name]; + this.$number.removeClass(map); + } + return this.$number.addClass(this.cardTypes[type]); + } + }; + + PaymentTag.prototype.handleError = function(err) { + if (err.message) { + this.$message.text(err.message); + } + switch (err.code) { + case 'card_declined': + this.invalidInput(this.$number); + break; + case 'invalid_number': + case 'incorrect_number': + this.invalidInput(this.$number); + break; + case 'invalid_expiry_month': + this.invalidInput(this.$expiryMonth); + break; + case 'invalid_expiry_year': + case 'expired_card': + this.invalidInput(this.$expiryYear); + break; + case 'invalid_cvc': + this.invalidInput(this.$cvc); + } + this.$('label.invalid:first input').select(); + this.trigger('error', err); + return typeof console !== "undefined" && console !== null ? console.error('Stripe error:', err) : void 0; + }; + + PaymentTag.prototype.invalidInput = function(input) { + input.parent().addClass('invalid'); + return this.trigger('invalid', [input.attr('name'), input]); + }; + + PaymentTag.prototype.expiryVal = function() { + var month, prefix, trim, year; + trim = function(s) { + return s.replace(/^\s+|\s+$/g, ''); + }; + month = trim(this.$expiryMonth.val()); + year = trim(this.$expiryYear.val()); + if (year.length === 2) { + prefix = (new Date).getFullYear(); + prefix = prefix.toString().slice(0, 2); + year = prefix + year; + } + return { + month: month, + year: year + }; + }; + + PaymentTag.prototype.enableInputs = function() { + var $elements; + $elements = this.$el.add(this.$form).find(':input'); + return $elements.each(function() { + var $item, _ref; + $item = $(this); + return $elements.attr('disabled', (_ref = $item.data('olddisabled')) != null ? _ref : false); + }); + }; + + PaymentTag.prototype.disableInputs = function() { + var $elements; + $elements = this.$el.add(this.$form).find(':input'); + return $elements.each(function() { + var $item; + $item = $(this); + $item.data('olddisabled', $item.attr('disabled')); + return $item.attr('disabled', true); + }); + }; + + PaymentTag.prototype.trigger = function() { + var data, event, _ref; + event = arguments[0], data = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + return (_ref = this.$el).trigger.apply(_ref, ["" + event + ".payment"].concat(__slice.call(data))); + }; + + PaymentTag.prototype.$ = function(sel) { + return $(sel, this.$el); + }; + + return PaymentTag; + + })(); + + document.createElement('payment'); + + if (typeof module !== "undefined" && module !== null) { + module.exports = PaymentTag; + } + + global = this; + + if (global.Stripe) { + $(function() { + return typeof PaymentTag.replaceTags === "function" ? PaymentTag.replaceTags() : void 0; + }); + } else { + script = document.createElement('script'); + script.onload = script.onreadystatechange = function() { + if (!global.Stripe) { + return; + } + if (script.done) { + return; + } + script.done = true; + return typeof PaymentTag.replaceTags === "function" ? PaymentTag.replaceTags() : void 0; + }; + script.src = 'https://js.stripe.com/v1/'; + $(function() { + var sibling; + sibling = document.getElementsByTagName('script')[0]; + return sibling != null ? sibling.parentNode.insertBefore(script, sibling) : void 0; + }); + } + +}).call(this); +(function() { + this.PaymentTag || (this.PaymentTag = {}); + this.PaymentTag["view"] = function(__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + + __out.push('\n\n
\n \n\n \n
\n\n
\n \n\n \n \n
\n\n'); + + if (this.options.cvc) { + __out.push('\n
\n \n \n
\n'); + } + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); + }; +}).call(this); diff --git a/static/stripe/tag.js b/static/stripe/tag.js new file mode 100755 index 00000000..dcd82026 --- /dev/null +++ b/static/stripe/tag.js @@ -0,0 +1 @@ +(function(){var e,t,n,r=function(e,t){return function(){return e.apply(t,arguments)}},i=[].slice;e=this.jQuery||this.Zepto;if(!e)throw"jQuery/Zepto required";this.PaymentTag=function(){function t(t){var n,i;t==null&&(t={}),this.changeCardType=r(this.changeCardType,this),this.restrictNumeric=r(this.restrictNumeric,this),this.formatNumber=r(this.formatNumber,this),this.handleToken=r(this.handleToken,this),this.submit=r(this.submit,this),this.$el=t.el||"",this.$el=e(this.$el),t.key||(t.key=this.$el.attr("key")||this.$el.attr("data-key")),(n=t.cvc)==null&&(t.cvc=this.$el.attr("nocvc")==null&&this.$el.attr("data-nocvc")==null),(i=t.token)==null&&(t.token=this.$el.attr("notoken")==null&&this.$el.attr("data-notoken")==null),t.form||(t.form=this.$el.parents("form")),this.options=e.extend({},this.defaults,t),this.options.key&&this.setKey(this.options.key),this.setForm(this.options.form),this.$el.delegate(".number input","keydown",this.formatNumber),this.$el.delegate(".number input","keyup",this.changeCardType),this.$el.delegate("input[type=tel]","keypress",this.restrictNumeric)}return t.replaceTags=function(t){var n=this;return t==null&&(t=document.body),e("payment, .payment-tag",t).each(function(e,t){return(new n({el:t})).render()})},t.prototype.defaults={tokenName:"stripe_token",token:!0,cvc:!0},t.prototype.render=function(){return this.$el.html(this.constructor.view(this)),this.$number=this.$(".number input"),this.$cvc=this.$(".cvc input"),this.$expiryMonth=this.$(".expiry input.expiryMonth"),this.$expiryYear=this.$(".expiry input.expiryYear"),this.$message=this.$(".message"),this},t.prototype.renderToken=function(t){return this.$token=e(''),this.$token.attr("name",this.options.tokenName),this.$token.val(t),this.$el.html(this.$token)},t.prototype.setForm=function(t){return this.$form=e(t),this.$form.bind("submit.payment",this.submit)},t.prototype.setKey=function(e){return this.key=e,Stripe.setPublishableKey(this.key)},t.prototype.validate=function(){var e,t;return t=!0,this.$("div").removeClass("invalid"),this.$message.empty(),Stripe.validateCardNumber(this.$number.val())||(t=!1,this.handleError({code:"invalid_number"})),e=this.expiryVal(),Stripe.validateExpiry(e.month,e.year)||(t=!1,this.handleError({code:"expired_card"})),this.options.cvc&&!Stripe.validateCVC(this.$cvc.val())&&(t=!1,this.handleError({code:"invalid_cvc"})),t||this.$(".invalid input:first").select(),t},t.prototype.createToken=function(e){var t,n,r=this;return t=function(t,n){return n.error?e(n.error):e(null,n)},n=this.expiryVal(),Stripe.createToken({number:this.$number.val(),cvc:this.$cvc.val()||null,exp_month:n.month,exp_year:n.year},t)},t.prototype.submit=function(e){e!=null&&e.preventDefault(),e!=null&&e.stopImmediatePropagation();if(!this.validate())return;if(this.pending)return;return this.pending=!0,this.disableInputs(),this.trigger("pending"),this.$el.addClass("pending"),this.createToken(this.handleToken)},t.prototype.handleToken=function(e,t){return this.enableInputs(),this.trigger("complete"),this.$el.removeClass("pending"),this.pending=!1,e?this.handleError(e):(this.trigger("success",t),this.$el.addClass("success"),this.options.token&&this.renderToken(t.id),this.$form.unbind("submit.payment",this.submit),this.$form.submit())},t.prototype.formatNumber=function(e){var t,n,r;t=String.fromCharCode(e.which);if(!/^\d+$/.test(t))return;r=this.$number.val(),Stripe.cardType(r)==="American Express"?n=r.match(/^(\d{4}|\d{4}\s\d{6})$/):n=r.match(/(?:^|\s)(\d{4})$/);if(n)return this.$number.val(r+" ")},t.prototype.restrictNumeric=function(e){var t;return e.shiftKey||e.metaKey?!0:e.which===0?!0:(t=String.fromCharCode(e.which),!/[A-Za-z]/.test(t))},t.prototype.cardTypes={Visa:"visa","American Express":"amex",MasterCard:"mastercard",Discover:"discover",Unknown:"unknown"},t.prototype.changeCardType=function(e){var t,n,r,i;r=Stripe.cardType(this.$number.val());if(!this.$number.hasClass(r)){i=this.cardTypes;for(n in i)t=i[n],this.$number.removeClass(t);return this.$number.addClass(this.cardTypes[r])}},t.prototype.handleError=function(e){e.message&&this.$message.text(e.message);switch(e.code){case"card_declined":this.invalidInput(this.$number);break;case"invalid_number":case"incorrect_number":this.invalidInput(this.$number);break;case"invalid_expiry_month":this.invalidInput(this.$expiryMonth);break;case"invalid_expiry_year":case"expired_card":this.invalidInput(this.$expiryYear);break;case"invalid_cvc":this.invalidInput(this.$cvc)}return this.$("label.invalid:first input").select(),this.trigger("error",e),typeof console!="undefined"&&console!==null?console.error("Stripe error:",e):void 0},t.prototype.invalidInput=function(e){return e.parent().addClass("invalid"),this.trigger("invalid",[e.attr("name"),e])},t.prototype.expiryVal=function(){var e,t,n,r;return n=function(e){return e.replace(/^\s+|\s+$/g,"")},e=n(this.$expiryMonth.val()),r=n(this.$expiryYear.val()),r.length===2&&(t=(new Date).getFullYear(),t=t.toString().slice(0,2),r=t+r),{month:e,year:r}},t.prototype.enableInputs=function(){var t;return t=this.$el.add(this.$form).find(":input"),t.each(function(){var n,r;return n=e(this),t.attr("disabled",(r=n.data("olddisabled"))!=null?r:!1)})},t.prototype.disableInputs=function(){var t;return t=this.$el.add(this.$form).find(":input"),t.each(function(){var t;return t=e(this),t.data("olddisabled",t.attr("disabled")),t.attr("disabled",!0)})},t.prototype.trigger=function(){var e,t,n;return t=arguments[0],e=2<=arguments.length?i.call(arguments,1):[],(n=this.$el).trigger.apply(n,[""+t+".payment"].concat(i.call(e)))},t.prototype.$=function(t){return e(t,this.$el)},t}(),document.createElement("payment"),typeof module!="undefined"&&module!==null&&(module.exports=PaymentTag),t=this,t.Stripe?e(function(){return typeof PaymentTag.replaceTags=="function"?PaymentTag.replaceTags():void 0}):(n=document.createElement("script"),n.onload=n.onreadystatechange=function(){if(!t.Stripe)return;if(n.done)return;return n.done=!0,typeof PaymentTag.replaceTags=="function"?PaymentTag.replaceTags():void 0},n.src="https://js.stripe.com/v1/",e(function(){var e;return e=document.getElementsByTagName("script")[0],e!=null?e.parentNode.insertBefore(n,e):void 0}))}).call(this),function(){this.PaymentTag||(this.PaymentTag={}),this.PaymentTag.view=function(e){e||(e={});var t=[],n=function(e){var n=t,r;return t=[],e.call(this),r=t.join(""),t=n,i(r)},r=function(e){return e&&e.ecoSafe?e:typeof e!="undefined"&&e!=null?o(e):""},i,s=e.safe,o=e.escape;return i=e.safe=function(e){if(e&&e.ecoSafe)return e;if(typeof e=="undefined"||e==null)e="";var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('\n\n
\n \n\n \n
\n\n
\n \n\n \n \n
\n\n'),this.options.cvc&&t.push('\n
\n \n \n
\n'),t.push("\n")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")}}.call(this); \ No newline at end of file diff --git a/static/stripe/themes/amex.png b/static/stripe/themes/amex.png new file mode 100755 index 00000000..959159f3 Binary files /dev/null and b/static/stripe/themes/amex.png differ diff --git a/static/stripe/themes/discover.png b/static/stripe/themes/discover.png new file mode 100755 index 00000000..6d8860bd Binary files /dev/null and b/static/stripe/themes/discover.png differ diff --git a/static/stripe/themes/generic.png b/static/stripe/themes/generic.png new file mode 100755 index 00000000..19e4d008 Binary files /dev/null and b/static/stripe/themes/generic.png differ diff --git a/static/stripe/themes/mastercard.png b/static/stripe/themes/mastercard.png new file mode 100755 index 00000000..b8558403 Binary files /dev/null and b/static/stripe/themes/mastercard.png differ diff --git a/static/stripe/themes/spinner.gif b/static/stripe/themes/spinner.gif new file mode 100755 index 00000000..ca0285b1 Binary files /dev/null and b/static/stripe/themes/spinner.gif differ diff --git a/static/stripe/themes/stripe.css b/static/stripe/themes/stripe.css new file mode 100755 index 00000000..916751f2 --- /dev/null +++ b/static/stripe/themes/stripe.css @@ -0,0 +1,152 @@ +payment, .payment { + position: relative; + display: block; + + border-radius: 5px; + padding: 15px 20px; + max-width: 300px; + overflow: hidden; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; + + font-size: 12px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + font-family: 'Helvetica Neue', Helvetica, Arial Geneva, sans-serif; + + background: #FFF; + background-image: -o-linear-gradient(#FFF, #F9FAFA); + background-image: -ms-linear-gradient(#FFF, #F9FAFA); + background-image: -moz-linear-gradient(#FFF, #F9FAFA); + background-image: -webkit-linear-gradient(#FEFEFE, #F9FAFA); + background-image: linear-gradient(#FFF, #F9FAFA); + + -moz-box-shadow: 0 0 2px rgba(80,84,92,0.3), 0 1px 1px rgba(80,84,92,0.5); + -webkit-box-shadow: 0 0 2px rgba(80, 84, 92, 0.3), 0 1px 1px rgba(80, 84, 92, 0.5); + -ms-box-shadow: 0 0 2px rgba(80, 84, 92, 0.3), 0 1px 1px rgba(80, 84, 92, 0.5); + box-shadow: 0 0 2px rgba(80, 84, 92, 0.3), 0 1px 1px rgba(80, 84, 92, 0.5); +} + +payment ::-webkit-input-placeholder, +.payment ::-webkit-input-placeholder { + text-transform: uppercase; +} + +payment label, .payment label { + display: block; + color: #999; + font-size: 13px; + padding: 5px 0; + text-transform: uppercase; + text-shadow: 0 1px 0 #FFF; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +payment input, .payment input { + font-size: 13px; + padding: 5px 5px; + border: 1px solid #BBB; + border-top-color: #999; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + border-radius: 3px; + + -webkit-transition: -webkit-box-shadow 0.1s ease-in-out; + -moz-transition: -moz-box-shadow 0.1s ease-in-out; + transition: -moz-box-shadow 0.1s ease-in-out; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + font-family: 'Helvetica Neue', Helvetica, Arial Geneva, sans-serif; + font-size: 14px; +} + +payment input:focus, .payment input:focus { + border: 1px solid #5695DB; + outline: none; + -webkit-box-shadow: inset 0 1px 2px #DDD, 0px 0 5px #5695DB; + -moz-box-shadow: 0 0 5px #5695db; + box-shadow: inset 0 1px 2px #DDD, 0px 0 5px #5695DB; +} + +payment .invalid input, .payment .invalid input { + outline: none; + border-color: rgba(255, 0, 0, 0.5); + -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); + -ms-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); +} + +payment input:disabled, .payment input:disabled { + opacity: 0.5; +} + +payment .number, +.payment .number { + margin-bottom: 8px; +} + +payment .number input, .payment .number input { + padding: 7px 40px 7px 7px; + background: #FFF url(generic.png) 98.5% 20% no-repeat; + width: 100%; +} + +payment .number input.visa, .payment .number input.visa { + background-image: url(visa.png); +} + +payment .number input.mastercard, .payment .number input.mastercard { + background-image: url(mastercard.png); +} + +payment .number input.discover, .payment .number input.discover { + background-image: url(discover.png); +} + +payment .number input.amex, .payment .number input.amex { + background-image: url(amex.png); +} + +payment .expiry input, payment .cvc input { + width: 45px; +} + +payment .expiry em { + font-size: 10px; + font-style: normal; + display: none; +} + +payment .cvc, +.payment .cvc { + float: right; + text-align: right; +} + +payment .expiry, +.payment .expiry { + float: left; +} + +payment .message, +.payment .message { + display: block; +} + +payment.pending, +.payment.pending, +payment.success, +.payment.success { + background: #FFF url(spinner.gif) center center no-repeat; + min-height: 130px; +} \ No newline at end of file diff --git a/static/stripe/themes/visa.png b/static/stripe/themes/visa.png new file mode 100755 index 00000000..e39df455 Binary files /dev/null and b/static/stripe/themes/visa.png differ