├── version ├── .gitignore ├── examples ├── confirmation.html ├── images │ ├── noise.png │ ├── pulp.jpeg │ └── grid_demo.png ├── simple.css ├── update_billing_info.html ├── index.html ├── one_time_transaction.html ├── subscribe.html ├── gridsystem.html ├── gridsystem.css └── examples.css ├── bin ├── yuicompressor-2.4.6.jar └── compile.js ├── themes └── default │ ├── images │ ├── dash.png │ ├── check.png │ ├── due_now.png │ ├── error.png │ ├── loading.gif │ ├── uncheck.png │ ├── coupon_check.png │ ├── coupon_valid.png │ ├── submitting.gif │ ├── coupon_invalid.png │ ├── coupon_checking.gif │ └── credit_cards │ │ ├── amex.png │ │ ├── visa.png │ │ ├── discover.png │ │ └── mastercard.png │ ├── recurly.styl │ └── recurly.css ├── src ├── dom │ ├── update_billing_info_form.jade │ ├── one_time_transaction_form.jade │ ├── terms_of_service.jade │ ├── months.jade │ ├── contact_info_fields.jade │ ├── billing_info_fields.jade │ ├── subscribe_form.jade │ └── countries.jade └── js │ ├── account.js │ ├── transaction.js │ ├── header.js │ ├── locale.js │ ├── polyfills.js │ ├── billing_info.js │ ├── states.js │ ├── plan.js │ ├── validators.js │ ├── core.js │ ├── subscription.js │ ├── utils.js │ └── ui.js ├── package.json ├── LICENSE ├── README.md ├── Makefile ├── changelog.md └── test └── all.html /version: -------------------------------------------------------------------------------- 1 | 2.1.3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /examples/confirmation.html: -------------------------------------------------------------------------------- 1 | 2 |

Success!

3 | -------------------------------------------------------------------------------- /examples/images/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/examples/images/noise.png -------------------------------------------------------------------------------- /examples/images/pulp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/examples/images/pulp.jpeg -------------------------------------------------------------------------------- /bin/yuicompressor-2.4.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/bin/yuicompressor-2.4.6.jar -------------------------------------------------------------------------------- /examples/images/grid_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/examples/images/grid_demo.png -------------------------------------------------------------------------------- /themes/default/images/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/dash.png -------------------------------------------------------------------------------- /themes/default/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/check.png -------------------------------------------------------------------------------- /themes/default/images/due_now.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/due_now.png -------------------------------------------------------------------------------- /themes/default/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/error.png -------------------------------------------------------------------------------- /themes/default/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/loading.gif -------------------------------------------------------------------------------- /themes/default/images/uncheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/uncheck.png -------------------------------------------------------------------------------- /themes/default/images/coupon_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/coupon_check.png -------------------------------------------------------------------------------- /themes/default/images/coupon_valid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/coupon_valid.png -------------------------------------------------------------------------------- /themes/default/images/submitting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/submitting.gif -------------------------------------------------------------------------------- /themes/default/images/coupon_invalid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/coupon_invalid.png -------------------------------------------------------------------------------- /themes/default/images/coupon_checking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/coupon_checking.gif -------------------------------------------------------------------------------- /themes/default/images/credit_cards/amex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/credit_cards/amex.png -------------------------------------------------------------------------------- /themes/default/images/credit_cards/visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/credit_cards/visa.png -------------------------------------------------------------------------------- /themes/default/images/credit_cards/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/credit_cards/discover.png -------------------------------------------------------------------------------- /themes/default/images/credit_cards/mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/recurly-js/master/themes/default/images/credit_cards/mastercard.png -------------------------------------------------------------------------------- /src/dom/update_billing_info_form.jade: -------------------------------------------------------------------------------- 1 | form.recurly.update_billing_info 2 | 3 | .server_errors.none 4 | 5 | .billing_info 6 | 7 | .footer 8 | button.submit(type='submit') Update 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recurly-js" 3 | , "version": "1.1.8" 4 | , "dependencies": { 5 | "stylus": "0.19.x" 6 | , "jade": "0.17.x" 7 | , "async": "0.1.x" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/dom/one_time_transaction_form.jade: -------------------------------------------------------------------------------- 1 | form.recurly.update_billing_info 2 | 3 | .server_errors.none 4 | 5 | .contact_info 6 | .billing_info 7 | .accept_tos 8 | 9 | .footer 10 | button.submit(type='submit') Pay 11 | -------------------------------------------------------------------------------- /src/dom/terms_of_service.jade: -------------------------------------------------------------------------------- 1 | input#tos_check(type="checkbox") 2 | label#accept_tos(for="tos_check") I accept the 3 | a.tos_link(target="_blank") Terms of Service 4 | span.and\ and\ 5 | a.pp_link(target="_blank") Privacy Policy 6 | -------------------------------------------------------------------------------- /src/js/account.js: -------------------------------------------------------------------------------- 1 | R.Account = { 2 | create: createObject 3 | , toJSON: function() { 4 | return { 5 | first_name: this.firstName 6 | , last_name: this.lastName 7 | , company_name: this.companyName 8 | , account_code: this.code 9 | , email: this.email 10 | }; 11 | } 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /src/dom/months.jade: -------------------------------------------------------------------------------- 1 | option(value="1") 01 - January 2 | option(value="2") 02 - February 3 | option(value="3") 03 - March 4 | option(value="4") 04 - April 5 | option(value="5") 05 - May 6 | option(value="6") 06 - June 7 | option(value="7") 07 - July 8 | option(value="8") 08 - August 9 | option(value="9") 09 - September 10 | option(value="10") 10 - October 11 | option(value="11") 11 - November 12 | option(value="12") 12 - December 13 | -------------------------------------------------------------------------------- /src/dom/contact_info_fields.jade: -------------------------------------------------------------------------------- 1 | .title Contact Info 2 | 3 | .full_name 4 | .field.first_name 5 | .placeholder First Name 6 | input(type="text") 7 | 8 | 9 | .field.last_name 10 | .placeholder Last Name 11 | input(type="text") 12 | 13 | .field.email 14 | .placeholder Email 15 | input(type="text") 16 | 17 | .field.phone 18 | .placeholder Phone Number 19 | input(type="text") 20 | 21 | .field.company_name 22 | .placeholder Company/Organization Name 23 | input(type="text") 24 | 25 | -------------------------------------------------------------------------------- /examples/simple.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: "helvetica neue", helvetica, arial; 3 | margin: 0; 4 | padding: 0; 5 | background: #c0c1c3 url("../images/noise.png") no-repeat center top; 6 | } 7 | #recurly-subscribe { 8 | width: 500px; 9 | margin: 40px auto; 10 | background: #fff; 11 | margin: 20px auto; 12 | padding: 0px; 13 | border: 1px solid #999; 14 | border-radius: 8px; 15 | background: #fcfcfc url("../images/hydranoise.jpeg") repeat; 16 | box-shadow: 0px 1px 2px rgba(0,0,0,0.2), inset 0 1px 0 #fff; 17 | } 18 | -------------------------------------------------------------------------------- /src/js/transaction.js: -------------------------------------------------------------------------------- 1 | 2 | R.Transaction = { 3 | // Note - No toJSON function for this object, all parameters must be signed. 4 | create: createObject 5 | , save: function(options) { 6 | var json = { 7 | account: this.account ? this.account.toJSON() : undefined 8 | , billing_info: this.billingInfo.toJSON() 9 | , signature: options.signature 10 | }; 11 | 12 | R.ajax({ 13 | url: R.settings.baseURL+'transactions/create' 14 | , data: json 15 | , dataType: 'jsonp' 16 | , jsonp: 'callback' 17 | , timeout: 60000 18 | , success: function(data) { 19 | if(data.success && options.success) { 20 | options.success(data.success); 21 | } 22 | else if(data.errors && options.error) { 23 | options.error( R.flattenErrors(data.errors) ); 24 | } 25 | } 26 | , error: function() { 27 | if(options.error) { 28 | options.error(['Unknown error processing transaction. Please try again later.']); 29 | } 30 | } 31 | , complete: options.complete || $.noop 32 | }); 33 | } 34 | }; 35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (MIT License) 2 | 3 | Copyright (C) 2011 by Recurly, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recurly.js 2 | 3 | Recurly.js is an open-source Javascript library for creating great looking credit card forms to securely create subscriptions, one-time transactions, and update billing information using Recurly. The library is designed to create fully customizable order forms while minimizing your PCI compliance scope. 4 | 5 | This library depends on jQuery 1.5.2+. 6 | 7 | Please refer to our full documentation at: http://docs.recurly.com/recurlyjs 8 | 9 | ## Building / Contributing 10 | 11 | The build/ directory contains the compiled library. You might want to build it yourself if you are contributing or have an unusual use case that isn't appropriate for the official library. 12 | 13 | * Install [node](http://nodejs.org/) and [npm](http://npmjs.org/) 14 | * Run 'make' 15 | 16 | Never edit build/recurly.js. The sources under src/ compile to build/recurly.js. 17 | 18 | To create a new theme, just add a directory to 'themes' containing a recurly.css. 19 | You can use any meta-language that compiles down to css and include that as well, 20 | but the compiled .css should be under version control. 21 | Put any images under 'images' and use relative paths in the css. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/sh 2 | COMPILER = ./bin/compile.js 3 | STYLUS = ./node_modules/stylus/bin/stylus 4 | YUI_COMPRESSOR = java -jar ./bin/yuicompressor-2.4.6.jar 5 | 6 | JS_SOURCES = $(addprefix src/js/, \ 7 | core.js \ 8 | locale.js \ 9 | utils.js \ 10 | validators.js \ 11 | plan.js \ 12 | account.js \ 13 | billing_info.js \ 14 | subscription.js \ 15 | transaction.js \ 16 | ui.js\ 17 | states.js\ 18 | ) 19 | 20 | DOM_SOURCES = $(addprefix src/dom/, \ 21 | contact_info_fields.jade \ 22 | billing_info_fields.jade \ 23 | subscribe_form.jade \ 24 | update_billing_info_form.jade \ 25 | one_time_transaction_form.jade \ 26 | terms_of_service.jade \ 27 | ) 28 | 29 | all: node_modules build build/recurly.min.js 30 | 31 | build: 32 | mkdir -p build 33 | 34 | build/recurly.js: $(JS_SOURCES) $(DOM_SOURCES) 35 | $(COMPILER) $^ > $@ 36 | 37 | build/recurly.min.js: build/recurly.js 38 | rm -f build/recurly.min.js 39 | $(YUI_COMPRESSOR) build/recurly.js -o build/recurly.min.js 40 | 41 | themes/default/recurly.css: themes/default/recurly.styl 42 | $(STYLUS) $^ 43 | 44 | clean: 45 | rm -rf build 46 | 47 | node_modules: package.json 48 | npm install 49 | touch node_modules 50 | 51 | .PHONY: clean 52 | -------------------------------------------------------------------------------- /examples/update_billing_info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RecurlyJS Update Billing Info Example 6 | 7 | 8 | 9 | 10 | 11 | 29 | 30 | 31 | 32 |

Update Billing Info

33 |

Account Code: testaccount

34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /src/dom/billing_info_fields.jade: -------------------------------------------------------------------------------- 1 | .title Billing Info 2 | 3 | .accepted_cards 4 | 5 | .credit_card 6 | .field.first_name 7 | .placeholder First Name 8 | input(type="text") 9 | 10 | .field.last_name 11 | .placeholder Last Name 12 | input(type="text") 13 | 14 | .card_cvv 15 | .field.card_number 16 | .placeholder Credit Card Number 17 | input(type="text") 18 | 19 | .field.cvv 20 | .placeholder CVV 21 | input(type="text") 22 | 23 | .field.expires 24 | .title Expires 25 | .month 26 | select 27 | include months.jade 28 | .year 29 | select 30 | 31 | .address 32 | .field.address1 33 | .placeholder Address 34 | input(type="text") 35 | 36 | .field.address2 37 | .placeholder Apt/Suite 38 | input(type="text") 39 | 40 | .field.city 41 | .placeholder City 42 | input(type="text") 43 | 44 | .state_zip 45 | .field.state 46 | .placeholder State/Province 47 | input(type="text") 48 | 49 | .field.zip 50 | .placeholder Zip/Postal 51 | input(type="text") 52 | 53 | .field.country 54 | select 55 | include countries.jade 56 | 57 | .field.vat_number 58 | .placeholder VAT Number 59 | input(type="text") 60 | 61 | -------------------------------------------------------------------------------- /src/dom/subscribe_form.jade: -------------------------------------------------------------------------------- 1 | form.recurly.subscribe 2 | //if lt IE 7 3 | div.iefail 4 | div.chromeframe 5 | p.blast Your browser is not supported by Recurly.js. 6 | p 7 | a(href="http://browsehappy.com/") Upgrade to a different browser 8 | p or 9 | p 10 | a(href="http://www.google.com/chromeframe/?redirect=true") install Google Chrome Frame 11 | p to use this site. 12 | .subscription 13 | .plan 14 | .name 15 | .field.quantity 16 | .placeholder Qty 17 | input(type="text") 18 | 19 | .recurring_cost 20 | .cost 21 | .interval 22 | 23 | .free_trial 24 | 25 | .setup_fee 26 | .title Setup Fee 27 | .cost 28 | 29 | 30 | .add_ons.none 31 | 32 | .coupon 33 | .coupon_code.field 34 | .placeholder Coupon Code 35 | input.coupon_code(type="text") 36 | .check 37 | .description 38 | .discount 39 | 40 | .vat 41 | .title VAT 42 | .cost 43 | 44 | .due_now 45 | .title Order Total 46 | .cost 47 | 48 | .server_errors.none 49 | 50 | .contact_info 51 | 52 | .billing_info 53 | 54 | .accept_tos 55 | 56 | .footer 57 | button.submit(type='submit') Subscribe 58 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Examples 6 | 7 | 8 | 9 |

Recurly.js

10 |

Examples

11 |
12 | 13 |
14 | Subscribe to Plan 15 | Update Billing Info 16 | One Time Transaction 17 | Grid System Design 18 |
19 |

You can test with these fake credit card numbers.

20 | 21 |

All examples point to a company we created specifically for this demo, called recurlyjsdemo. Naturally you'll want to replace all occurances with your real company subdomain.

22 |

The demo company defines two plans: simpleplan, and complexplan, and 23 | a forever 30% off coupon, named test.

24 | 25 |

Update Billing Info, and One-time Transactions both require signatures, which must be generated server-side and rendered to an option. For demonstration purposes we pre‑computed the signatures and hardcoded them in.

26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /src/js/header.js: -------------------------------------------------------------------------------- 1 | // Recurly.js - v{VERSION} 2 | // 3 | // Communicates with Recurly via a JSONP API, 4 | // generates UI, handles user error, and passes control to the client 5 | // to handle the successful events such as subscription creation. 6 | // 7 | // Example Site: https://js.recurly.com 8 | // 9 | // (MIT License) 10 | // 11 | // Copyright (C) 2012 by Recurly, Inc. 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | -------------------------------------------------------------------------------- /examples/one_time_transaction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RecurlyJS One-time Transaction Example 6 | 7 | 8 | 9 | 10 | 11 | 45 | 46 | 47 | 48 |

One-time Transaction

49 |

Dollar Amount: $50

50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/js/locale.js: -------------------------------------------------------------------------------- 1 | R.locale = {}; 2 | 3 | R.locale.errors = { 4 | emptyField: 'Required field' 5 | , missingFullAddress: 'Please enter your full address.' 6 | , invalidEmail: 'Invalid' 7 | , invalidCC: 'Invalid' 8 | , invalidCVV: 'Invalid' 9 | , invalidCoupon: 'Invalid' 10 | , cardDeclined: 'Transaction declined' 11 | , acceptTOS: 'Please accept the Terms of Service.' 12 | , invalidQuantity: 'Invalid quantity' 13 | }; 14 | 15 | R.locale.currencies = {}; 16 | 17 | R.locale.currency = { 18 | format: "%u%n" 19 | , separator: "." 20 | , delimiter: "," 21 | , precision: 2 22 | }; 23 | 24 | function C(key, def) { 25 | var c = R.locale.currencies[key] = createObject(R.locale.currency); 26 | for(var p in def) { 27 | c[p] = def[p]; 28 | } 29 | }; 30 | 31 | C('USD', { 32 | symbol: '$' 33 | }); 34 | 35 | C('AUD', { 36 | symbol: '$' 37 | }); 38 | 39 | C('CAD', { 40 | symbol: '$' 41 | }); 42 | 43 | C('EUR', { 44 | symbol: '\u20ac' 45 | }); 46 | 47 | C('GBP', { 48 | symbol: '\u00a3' 49 | }); 50 | 51 | C('CZK', { 52 | symbol: '\u004b' 53 | }); 54 | 55 | C('DKK', { 56 | symbol: '\u006b\u0072' 57 | }); 58 | 59 | C('HUF', { 60 | symbol: 'Ft' 61 | }); 62 | 63 | C('JPY', { 64 | symbol: '\u00a5' 65 | }); 66 | 67 | C('NOK', { 68 | symbol: 'kr' 69 | }); 70 | 71 | C('NZD', { 72 | symbol: '$' 73 | }); 74 | 75 | C('PLN', { 76 | symbol: '\u007a' 77 | }); 78 | 79 | C('SGD', { 80 | symbol: '$' 81 | }); 82 | 83 | C('SEK', { 84 | symbol: 'kr' 85 | }); 86 | 87 | C('CHF', { 88 | symbol: 'Fr' 89 | }); 90 | 91 | C('ZAR', { 92 | symbol: 'R' 93 | }); 94 | 95 | 96 | 97 | R.settings.locale = R.locale; 98 | -------------------------------------------------------------------------------- /examples/subscribe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RecurlyJS Subscribe Example 6 | 7 | 8 | 9 | 10 | 11 | 48 | 49 | 50 |

Subscribe to Plan

51 |

Plan Code: simpleplan

52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /src/js/polyfills.js: -------------------------------------------------------------------------------- 1 | // Object.create polyfill; true prototypal programming 2 | if (typeof Object.create !== 'function') { 3 | Object.create = function(o, props) { 4 | function F() {} 5 | F.prototype = o; 6 | 7 | if (typeof(props) === "object") { 8 | for (prop in props) { 9 | if (props.hasOwnProperty((prop))) { 10 | F[prop] = props[prop]; 11 | } 12 | } 13 | } 14 | return new F(); 15 | }; 16 | } 17 | 18 | // Array.map 19 | if (!Array.prototype.map) 20 | { 21 | Array.prototype.map = function(fun /*, thisp */) 22 | { 23 | "use strict"; 24 | 25 | if (this === void 0 || this === null) 26 | throw new TypeError(); 27 | 28 | var t = Object(this); 29 | var len = t.length >>> 0; 30 | if (typeof fun !== "function") 31 | throw new TypeError(); 32 | 33 | var res = new Array(len); 34 | var thisp = arguments[1]; 35 | for (var i = 0; i < len; i++) 36 | { 37 | if (i in t) 38 | res[i] = fun.call(thisp, t[i], i, t); 39 | } 40 | 41 | return res; 42 | }; 43 | } 44 | 45 | // Array.forEach 46 | if (!Array.prototype.forEach) { 47 | 48 | Array.prototype.forEach = function( callbackfn, thisArg ) { 49 | 50 | var T, 51 | O = Object(this), 52 | len = O.length >>> 0, 53 | k = 0; 54 | 55 | if ( !callbackfn || !callbackfn.call ) { 56 | throw new TypeError(); 57 | } 58 | 59 | if ( thisArg ) { 60 | T = thisArg; 61 | } 62 | 63 | while( k < len ) { 64 | 65 | var Pk = String( k ), 66 | kPresent = O.hasOwnProperty( Pk ), 67 | kValue; 68 | 69 | if ( kPresent ) { 70 | kValue = O[ Pk ]; 71 | 72 | callbackfn.call( T, kValue, k, O ); 73 | } 74 | 75 | k++; 76 | } 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/js/billing_info.js: -------------------------------------------------------------------------------- 1 | R.BillingInfo = { 2 | create: createObject 3 | , toJSON: function() { 4 | return { 5 | first_name: this.firstName 6 | , last_name: this.lastName 7 | , month: this.month 8 | , year: this.year 9 | , number: this.number 10 | , verification_value: this.cvv 11 | , address1: this.address1 12 | , address2: this.address2 13 | , city: this.city 14 | , state: this.state 15 | , zip: this.zip 16 | , country: this.country 17 | , phone: this.phone 18 | , vat_number: this.vatNumber 19 | }; 20 | } 21 | , save: function(options) { 22 | var json = { 23 | billing_info: this.toJSON() 24 | , signature: options.signature 25 | }; 26 | 27 | // Save first/last name on the account 28 | // if not distinguished 29 | if(!options.distinguishContactFromBillingInfo) { 30 | json.account = { 31 | account_code: options.accountCode 32 | , first_name: this.firstName 33 | , last_name: this.lastName 34 | }; 35 | } 36 | 37 | R.ajax({ 38 | url: R.settings.baseURL+'accounts/'+options.accountCode+'/billing_info/update' 39 | , data: json 40 | , dataType: 'jsonp' 41 | , jsonp: 'callback' 42 | , timeout: 60000 43 | , success: function(data) { 44 | if(data.success && options.success) { 45 | options.success(data.success); 46 | } 47 | else if(data.errors && options.error) { 48 | options.error( R.flattenErrors(data.errors) ); 49 | } 50 | } 51 | , error: function() { 52 | if(options.error) { 53 | options.error(['Unknown error processing transaction. Please try again later.']); 54 | } 55 | } 56 | , complete: options.complete || $.noop 57 | }); 58 | } 59 | }; 60 | 61 | -------------------------------------------------------------------------------- /examples/gridsystem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RecurlyJS Grid System Example 5 | 6 | 7 | 8 | 9 | 23 | 24 | 25 | 26 | 27 |
28 | 33 | 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 | 59 | 64 |
65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/gridsystem.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 18px "helvetica neue", helvetica, arial 3 | } 4 | 5 | #container 6 | { 7 | margin: 0 auto; 8 | width: 980px; 9 | background: #fff; 10 | } 11 | 12 | #header 13 | { 14 | background: #ccc; 15 | padding: 20px; 16 | } 17 | 18 | #header h1 { margin: 0; } 19 | 20 | #navigation 21 | { 22 | float: left; 23 | width: 980px; 24 | background: #333; 25 | } 26 | 27 | #navigation ul 28 | { 29 | margin: 0; 30 | padding: 0; 31 | } 32 | 33 | #navigation ul li 34 | { 35 | list-style-type: none; 36 | display: inline; 37 | } 38 | 39 | #navigation li a 40 | { 41 | display: block; 42 | float: left; 43 | padding: 5px 10px; 44 | color: #fff; 45 | text-decoration: none; 46 | border-right: 1px solid #fff; 47 | } 48 | 49 | #navigation li a:hover { background: #383; } 50 | 51 | #content-container 52 | { 53 | float: left; 54 | width: 980px; 55 | background: #f0f0f0 url(images/grid_demo.png) repeat 20px 0; 56 | } 57 | 58 | #content 59 | { 60 | clear: left; 61 | float: left; 62 | width: 520px; 63 | margin: 0 0 0 0px; 64 | display: inline; 65 | } 66 | 67 | #sidebar 68 | { 69 | float: right; 70 | font-weight: 100; 71 | letter-spacing: 1px; 72 | width: 400px; 73 | padding: 20px 0; 74 | margin: 0 0 0 0; 75 | display: inline; 76 | background: #333; 77 | color: white; 78 | } 79 | 80 | #sidebar p { margin: 20px; } 81 | 82 | #footer 83 | { 84 | clear: both; 85 | background: #ccc; 86 | text-align: right; 87 | padding: 20px; 88 | height: 1%; 89 | } 90 | 91 | 92 | #demo480 { 93 | background: skyblue; 94 | width: 480px; 95 | padding: 5px 0; 96 | } 97 | 98 | 99 | #demo960 { 100 | background: seagreen; 101 | width: 960px; 102 | padding: 5px 0; 103 | } 104 | 105 | 106 | #recurly-subscribe { 107 | padding 40px; 108 | height 100px; 109 | } 110 | 111 | -------------------------------------------------------------------------------- /examples/examples.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: "helvetica neue", helvetica, arial; 3 | margin: 0; 4 | padding: 0; 5 | background: #c0c1c3 url("images/noise.png") no-repeat center top; 6 | } 7 | h1 { 8 | width: 500px; 9 | margin: 30px auto; 10 | text-align: center; 11 | color: #fff; 12 | text-shadow: 0 1px 1px #111; 13 | } 14 | h2 { 15 | width: 500px; 16 | margin: -20px auto 0 auto; 17 | text-align: center; 18 | font-size: 18px; 19 | text-shadow: 0 1px 0 #fff; 20 | } 21 | p { 22 | margin: 20px; 23 | text-shadow: 0 1px 0 #eee; 24 | font-size: 16px; 25 | font-weight: 300; 26 | line-height: 26px; 27 | text-align: justify; 28 | } 29 | pre { 30 | margin: 20px; 31 | padding: 20px; 32 | background: #000; 33 | color: #fff; 34 | } 35 | code { 36 | font-family: menlo, monaco, "Lucida Console", monospace; 37 | padding: 2px 4px; 38 | font-size: 13px; 39 | background: rgba(55,100,150,0.30); 40 | color: #fff; 41 | letter-space: 1px; 42 | text-shadow: 0 1px 0 #467; 43 | border-radius: 4px; 44 | } 45 | a { 46 | color: inherit; 47 | } 48 | #index, 49 | #result { 50 | width: 480px; 51 | margin: 0 auto; 52 | } 53 | #examples { 54 | margin: 20px; 55 | font-weight: inherit; 56 | } 57 | #examples a { 58 | display: block; 59 | overflow: hidden; 60 | padding: 10px 15px; 61 | margin-bottom: 10px; 62 | border-radius: 18px; 63 | color: #fff; 64 | text-shadow: 0 1px 0 #000; 65 | font-weight: inherit; 66 | text-decoration: none; 67 | position: relative; 68 | background: rgba(0,10,0,0.40); 69 | box-shadow: 0 2px 1px rgba(0,0,0,0.10); 70 | } 71 | #examples a:hover { 72 | color: #fff; 73 | background: rgba(0,0,0,0.85); 74 | } 75 | #examples a:hover:after { 76 | content: '\25B6'; 77 | position: absolute; 78 | right: 15px; 79 | } 80 | #recurly-subscribe, 81 | #recurly-update-billing-info, 82 | #recurly-transaction { 83 | width: 500px; 84 | margin: 30px auto; 85 | padding: 0px; 86 | border: 1px solid #999; 87 | border-radius: 8px; 88 | background: #fcfcfc url("images/pulp.jpeg") repeat; 89 | box-shadow: 0px 1px 2px rgba(0,0,0,0.20), inset 0 1px 0 #fff; 90 | } 91 | -------------------------------------------------------------------------------- /src/js/states.js: -------------------------------------------------------------------------------- 1 | R.states = {}; 2 | R.states.US = { 3 | "-": "Select State" 4 | , "--": "------------" 5 | , "AK": "Alaska" 6 | , "AL": "Alabama" 7 | , "AP": "Armed Forces Pacific" 8 | , "AR": "Arkansas" 9 | , "AS": "American Samoa" 10 | , "AZ": "Arizona" 11 | , "CA": "California" 12 | , "CO": "Colorado" 13 | , "CT": "Connecticut" 14 | , "DC": "District of Columbia" 15 | , "DE": "Delaware" 16 | , "FL": "Florida" 17 | , "FM": "Federated States of Micronesia" 18 | , "GA": "Georgia" 19 | , "GU": "Guam" 20 | , "HI": "Hawaii" 21 | , "IA": "Iowa" 22 | , "ID": "Idaho" 23 | , "IL": "Illinois" 24 | , "IN": "Indiana" 25 | , "KS": "Kansas" 26 | , "KY": "Kentucky" 27 | , "LA": "Louisiana" 28 | , "MA": "Massachusetts" 29 | , "MD": "Maryland" 30 | , "ME": "Maine" 31 | , "MH": "Marshall Islands" 32 | , "MI": "Michigan" 33 | , "MN": "Minnesota" 34 | , "MO": "Missouri" 35 | , "MP": "Northern Mariana Islands" 36 | , "MS": "Mississippi" 37 | , "MT": "Montana" 38 | , "NC": "North Carolina" 39 | , "ND": "North Dakota" 40 | , "NE": "Nebraska" 41 | , "NH": "New Hampshire" 42 | , "NJ": "New Jersey" 43 | , "NM": "New Mexico" 44 | , "NV": "Nevada" 45 | , "NY": "New York" 46 | , "OH": "Ohio" 47 | , "OK": "Oklahoma" 48 | , "OR": "Oregon" 49 | , "PA": "Pennsylvania" 50 | , "PR": "Puerto Rico" 51 | , "PW": "Palau" 52 | , "RI": "Rhode Island" 53 | , "SC": "South Carolina" 54 | , "SD": "South Dakota" 55 | , "TN": "Tennessee" 56 | , "TX": "Texas" 57 | , "UT": "Utah" 58 | , "VA": "Virginia" 59 | , "VI": "Virgin Islands" 60 | , "VT": "Vermont" 61 | , "WA": "Washington" 62 | , "WV": "West Virginia" 63 | , "WI": "Wisconsin" 64 | , "WY": "Wyoming" 65 | }; 66 | 67 | R.states.CA = { 68 | "-": "Select State" 69 | , "--": "------------" 70 | , "AB": "Alberta" 71 | , "BC": "British Columbia" 72 | , "MB": "Manitoba" 73 | , "NB": "New Brunswick" 74 | , "NL": "Newfoundland" 75 | , "NS": "Nova Scotia" 76 | , "NU": "Nunavut" 77 | , "ON": "Ontario" 78 | , "PE": "Prince Edward Island" 79 | , "QC": "Quebec" 80 | , "SK": "Saskatchewan" 81 | , "NT": "Northwest Territories" 82 | , "YT": "Yukon Territory" 83 | , "AA": "Armed Forces Americas" 84 | , "AE": "Armed Forces Europe, Middle East, & Canada" 85 | }; 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/js/plan.js: -------------------------------------------------------------------------------- 1 | R.Plan = { 2 | create: createObject 3 | , fromJSON: function(json) { 4 | var p = this.create(); 5 | 6 | p.name = json.name; 7 | p.code = json.plan_code; 8 | p.currency = json.currency; 9 | p.cost = new R.Cost(json.unit_amount_in_cents); 10 | 11 | p.displayQuantity = json.display_quantity; 12 | 13 | p.interval = new R.TimePeriod( 14 | json.plan_interval_length, 15 | json.plan_interval_unit 16 | ); 17 | 18 | if(json.trial_interval_length) { 19 | p.trial = new R.TimePeriod( 20 | json.trial_interval_length, 21 | json.trial_interval_unit 22 | ); 23 | } 24 | 25 | if(json.setup_fee_in_cents) { 26 | p.setupFee = new R.Cost(json.setup_fee_in_cents); 27 | } 28 | 29 | if (json.vat_percentage) { 30 | R.settings.VATPercent = parseFloat(json.vat_percentage); 31 | } 32 | 33 | if (json.merchant_country) { 34 | R.settings.country = json.merchant_country; 35 | } 36 | 37 | p.addOns = []; 38 | if(json.add_ons) { 39 | for(var l=json.add_ons.length, i=0; i < l; ++i) { 40 | var a = json.add_ons[i]; 41 | p.addOns.push(R.AddOn.fromJSON(a)); 42 | } 43 | } 44 | 45 | return p; 46 | } 47 | , get: function(plan_code, currency, callback) { 48 | $.ajax({ 49 | url: R.settings.baseURL+'plans/'+plan_code+"?currency="+currency, 50 | // data: params, 51 | dataType: "jsonp", 52 | jsonp: "callback", 53 | timeout: 10000, 54 | success: function(data) { 55 | var plan = R.Plan.fromJSON(data); 56 | callback(plan); 57 | } 58 | }); 59 | } 60 | , createSubscription: function() { 61 | var s = createObject(R.Subscription); 62 | s.plan = createObject(this); 63 | s.plan.quantity = 1; 64 | s.addOns = []; 65 | return s; 66 | } 67 | }; 68 | 69 | R.AddOn = { 70 | fromJSON: function(json) { 71 | var a = createObject(R.AddOn); 72 | a.name = json.name; 73 | a.code = json.add_on_code; 74 | a.cost = new R.Cost(json.default_unit_amount_in_cents); 75 | a.displayQuantity = json.display_quantity; 76 | return a; 77 | } 78 | 79 | , toJSON: function() { 80 | return { 81 | name: this.name 82 | , add_on_code: this.code 83 | , default_unit_amount_in_cents: this.default_unit_amount_in_cents 84 | }; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /bin/compile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This script joins js and jade files together and outputs to stdout. 4 | // .js files are passed straight in with a little "compiled from" leader. 5 | // .jade files are compiled into HTML and wrapped in a JS string literal 6 | // assignment, like this: R.dom['jadefilename'] = 'compiledhtml'; 7 | 8 | var fs = require('fs') 9 | , jade = require('jade') 10 | , async = require('async'); 11 | 12 | var argParts = process.argv.slice(2).map(function(file) { 13 | if(file.match(/.*\.jade$/)) 14 | return jadePart(file); 15 | else 16 | return jsPart(file); 17 | }); 18 | 19 | var VERSION = '0'; 20 | 21 | async.series([prepare].concat(headerPart).concat(argParts).concat(footerPart)); 22 | 23 | function prepare(done) { 24 | VERSION = fs.readFileSync('version').toString().trim(); 25 | done(); 26 | } 27 | 28 | function headerPart(done) { 29 | 30 | var header = fs.readFileSync('src/js/header.js') + ''; 31 | header = header.replace(/\{VERSION\}/,VERSION); 32 | 33 | process.stdout.write( 34 | header 35 | + '\n(function($) {' 36 | + '\n"use strict";' 37 | ); 38 | 39 | done(); 40 | }; 41 | 42 | function footerPart(done) { 43 | process.stdout.write( 44 | '\nwindow.Recurly = R;' 45 | + '\n})(jQuery);' 46 | ); 47 | done(); 48 | }; 49 | 50 | function jsPart(jsfile) { 51 | return function(done) { 52 | fs.readFile(jsfile, function(err, data){ 53 | data = ('' + data).replace(/\{VERSION\}/,VERSION); 54 | process.stdout.write(leader(jsfile) + data); 55 | done(); 56 | }); 57 | }; 58 | } 59 | 60 | function jadePart(jadefile) { 61 | return function(done) { 62 | var key = jadefile.match('.*/(.+)\.jade$')[1]; 63 | var jadestr = fs.readFileSync(jadefile); 64 | 65 | jade.render(jadestr, {filename: jadefile}, function(err,html) { 66 | html = html.replace(/\n/g,''); 67 | var jsstr = leader(jadefile); 68 | jsstr += 'R.dom[\''+key+'\'] = \'' + html.replace(/\'/g,'\\\'') + '\';' 69 | process.stdout.write(jsstr); 70 | done(); 71 | 72 | }); 73 | 74 | }; 75 | } 76 | 77 | function leader(file) { 78 | var jsstr = ''; 79 | jsstr += "\n\n//////////////////////////////////////////////////\n"; 80 | jsstr += "// Compiled from " + file + "\n"; 81 | jsstr += "//////////////////////////////////////////////////\n\n"; 82 | 83 | return jsstr; 84 | } 85 | -------------------------------------------------------------------------------- /src/js/validators.js: -------------------------------------------------------------------------------- 1 | 2 | (R.isValidCC = function($input) { 3 | var v = $input.val(); 4 | 5 | // Strip out all non digits 6 | v = v.replace(/\D/g, ""); 7 | 8 | if(v == "") return false; 9 | 10 | var nCheck = 0, 11 | nDigit = 0, 12 | bEven = false; 13 | 14 | 15 | for (var n = v.length - 1; n >= 0; n--) { 16 | var cDigit = v.charAt(n); 17 | var nDigit = parseInt(cDigit, 10); 18 | if (bEven) { 19 | if ((nDigit *= 2) > 9) 20 | nDigit -= 9; 21 | } 22 | nCheck += nDigit; 23 | bEven = !bEven; 24 | } 25 | 26 | return (nCheck % 10) == 0; 27 | }).defaultErrorKey = 'invalidCC'; 28 | 29 | (R.isValidEmail = function($input) { 30 | var v = $input.val(); 31 | return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(v); 32 | }).defaultErrorKey = 'invalidEmail'; 33 | 34 | function wholeNumber(val) { 35 | return /^[0-9]+$/.test(val); 36 | } 37 | 38 | (R.isValidCVV = function($input) { 39 | var v = $input.val(); 40 | return (v.length == 3 || v.length == 4) && wholeNumber(v); 41 | }).defaultErrorKey = 'invalidCVV'; 42 | 43 | (R.isNotEmpty = function($input) { 44 | var v = $input.val(); 45 | if($input.is('select')) { 46 | if(v == '-' || v == '--') return false; 47 | } 48 | return !!v; 49 | }).defaultErrorKey = 'emptyField'; 50 | // State is required if its a dropdown, it is not required if it is an input box 51 | (R.isNotEmptyState = function($input) { 52 | var v = $input.val(); 53 | if($input.is('select')) { 54 | if(v == '-' || v == '--') return false; 55 | } 56 | return true; 57 | }).defaultErrorKey = 'emptyField'; 58 | 59 | (R.isChecked = function($input) { 60 | return $input.is(':checked'); 61 | }).defaultErrorKey = 'acceptTOS'; 62 | 63 | (R.isValidQuantity = function($input) { 64 | return /^[0-9]*$/.test($input.val()); 65 | }).defaultErrorKey = 'invalidQuantity'; 66 | 67 | -------------------------------------------------------------------------------- /src/js/core.js: -------------------------------------------------------------------------------- 1 | // Non-intrusive Object.create 2 | function createObject(o) { 3 | function F() {} 4 | F.prototype = o || this; 5 | return new F(); 6 | }; 7 | 8 | var R = {}; 9 | R.settings = { 10 | enableGeoIP: true 11 | , acceptedCards: ['american_express', 'discover', 'mastercard', 'visa'] 12 | , oneErrorPerField: true 13 | }; 14 | 15 | R.version = '{VERSION}'; 16 | 17 | R.dom = {}; 18 | 19 | R.Error = { 20 | toString: function() { 21 | return 'RecurlyJS Error: ' + this.message; 22 | } 23 | }; 24 | 25 | R.raiseError = function(message) { 26 | var e = createObject(R.Error); 27 | e.message = message; 28 | throw e; 29 | }; 30 | 31 | 32 | R.config = function(settings) { 33 | $.extend(true, R.settings, settings); 34 | 35 | if(!settings.baseURL) { 36 | R.settings.baseURL = 'https://api.recurly.com/jsonp/'; 37 | var subdomain = R.settings.subdomain || R.raiseError('company subdomain not configured'); 38 | R.settings.baseURL += subdomain + '/'; 39 | } 40 | }; 41 | 42 | 43 | function pluralize(count, term) { 44 | if(count == 1) { 45 | return term.substr(0,term.length-1); 46 | } 47 | 48 | return '' + count + ' ' + term; 49 | } 50 | 51 | // Immutable currency-amount object 52 | // This will eventually handle multi-currency 53 | // where it will store a list of costs per currency 54 | // and accessors will return the appropriate one 55 | // based on the current currency 56 | // 57 | 58 | (R.Cost = function(cents) { 59 | this._cents = cents || 0; 60 | }).prototype = { 61 | toString: function() { 62 | return R.formatCurrency(this.dollars()); 63 | } 64 | , cents: function(val) { 65 | if(val === undefined) 66 | return this._cents; 67 | 68 | return new Cost(val); 69 | } 70 | , dollars: function(val) { 71 | if(val === undefined) 72 | return this._cents/100; 73 | 74 | return new R.Cost(val*100); 75 | } 76 | , mult: function(n) { 77 | return new R.Cost(this._cents * n); 78 | } 79 | , add: function(n) { 80 | if(n.cents) n = n.cents(); 81 | return new R.Cost(this._cents + n); 82 | } 83 | , sub: function(n) { 84 | if(n.cents) n = n.cents(); 85 | return new R.Cost(this._cents - n); 86 | } 87 | }; 88 | 89 | R.Cost.FREE = new R.Cost(0); 90 | 91 | (R.TimePeriod = function(length,unit) { 92 | this.length = length; 93 | this.unit = unit; 94 | }).prototype = { 95 | toString: function() { 96 | return '' + pluralize(this.length, this.unit); 97 | } 98 | , toDate: function() { 99 | var d = new Date(); 100 | switch(this.unit) { 101 | case 'month': 102 | d.setMonth( d.getMonth() + this.length ); 103 | break; 104 | case 'day': 105 | d.setDay( d.getDay() + this.length ); 106 | break; 107 | } 108 | return d; 109 | } 110 | , clone: function() { 111 | return new R.TimePeriod(this.length,this.unit); 112 | } 113 | }; 114 | 115 | (R.RecurringCost = function(cost,interval) { 116 | this.cost = cost; 117 | this.interval = interval; 118 | }).prototype = { 119 | toString: function() { 120 | return '' + this.cost + ' every ' + this.interval; 121 | } 122 | , clone: function() { 123 | return new R.TimePeriod(this.length,this.unit); 124 | } 125 | }; 126 | 127 | R.RecurringCost.FREE = new R.RecurringCost(0,null); 128 | 129 | (R.RecurringCostStage = function(recurringCost, duration) { 130 | this.recurringCost = recurringCost; 131 | this.duration = duration; 132 | }).prototype = { 133 | toString: function() { 134 | this.recurringCost.toString() + ' for ' + this.duration.toString(); 135 | } 136 | }; 137 | 138 | 139 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | #Recurly.js CHANGELOG 2 | 3 | ##Version 2.1.3 (May 22, 2012) 4 | 5 | - Automatically set VAT variables for subscription forms using information from Recurly. 6 | - Fixed IE7 bug with the text field and added conditional IE6 logic 7 | 8 | ##Version 2.1.2 (March 26, 2012) 9 | 10 | - Include currency when validating a coupon 11 | 12 | ##Version 2.1.1 (March 13, 2012) 13 | 14 | - Fixed issue with postResult that would have caused results not to be POSTed 15 | 16 | ##Version 2.1.0 (March 12, 2012) 17 | 18 | - Result token is now POSTed to your successURL or JS callback handler 19 | - Updated to work with latest signature generation format (requires Client library upgrade) 20 | 21 | ##Version 2.0.6 (March 5, 2012) 22 | 23 | - Removed non-functional parameters from one time transactions. 24 | - Updated example HTML files. 25 | 26 | ##Version 2.0.5 (March 1, 2012) 27 | 28 | - Fixed issue where an empty plan quantity would cause totals to display as 0. 29 | 30 | ##Version 2.0.4 (February 9, 2012) 31 | 32 | - Fix: Now fetches and sends currency code of a plan during new subscriptions. 33 | 34 | ##Version 2.0.3 (February 9, 2012) 35 | 36 | - Fixed IE8 bug related to String.trim() support. 37 | 38 | ##Version 2.0.2 (February 8, 2012) 39 | 40 | - Added support for pre-populating coupon code via subscription.couponCode option. 41 | 42 | ##Version 2.0.1 (January 31, 2012) 43 | 44 | - No longer embedding jQuery 45 | - Added validation that checks that quantity is numeric 46 | - Added new locale.errors error, 'invalidQuantity' 47 | - Bugfix: Proper bind/unbind behavior w.r.t. input validation 48 | 49 | ##Version 2.0.0 (January 17, 2012) 50 | 51 | - buildSubscriptionForm() now requires a signature (breaks backwards compatibility) 52 | - Replaced 'preFill' option with 'account' and 'billingInfo'options. 53 | - 'preFill' options are moved down to the base options as general predefined object models with multiple intents. 54 | - Added 'oneErrorPerField' and 'oneErrorPerForm' options for validation. 55 | 56 | ##Version 1.2.0 (December 17, 2011) 57 | 58 | - Nice UI for switching state input to a select for US and Canada. 59 | - GeoIP country detection that makes the aforementioned UI so nice. 60 | - Added 'acceptedCards' option to form building functions. 61 | - Updated jQuery version used by tests and examples. 62 | 63 | ##Version 1.1.7 (December 17, 2011) 64 | 65 | - Big project refactor, with very minor changes to the resulting build. 66 | 67 | ##Version 1.1.6 (November 17, 2011) 68 | 69 | - Allow any characters to separate credit card parts. 70 | It forced only digits and dashes before, but 71 | now it allows anything and strips out the non-digits 72 | before LUHN validation. 73 | 74 | ##Version 1.1.5 (November 17, 2011) 75 | 76 | - Minor UI cornercase with the expiration date selector. 77 | 78 | ##Version 1.1.4 (November 10, 2011) 79 | 80 | - Made resultNamespace default to 'recurly_result'. 81 | 82 | ##Version 1.1.3 (October 31, 2011) 83 | 84 | - Made percent coupons discount only recurring amounts, not setup fee. 85 | 86 | ##Version 1.1.2 (September 9, 2011) 87 | 88 | - Fix issue with expiration dates, stop trying to use the browser Date object. 89 | 90 | ##Version 1.1.1 (August 30, 2011) 91 | 92 | - Added resultNamespace option 93 | 94 | - Minor UI improvement in year/month expiration select. 95 | 96 | ##Version 1.1.0 (August 29, 2011) 97 | 98 | - Added Company, and Phone fields 99 | with associated options collectCompany/collectPhone 100 | 101 | - When distinguishContactFromBilling == false on buildBillingInfoUpdateForm 102 | it will update the parent account first/last name. 103 | 104 | - Added preFill option for pre-populating fields with known values. 105 | 106 | - Added privacyPolicyURL to accompany termsOfServiceURL 107 | 108 | ##Version 1.0.4 (August 24, 2011) 109 | 110 | - Add VAT instead of subtracting it 111 | 112 | ##Version 1.0.3 (August 24, 2011) 113 | 114 | - Added termsOfService acceptance check 115 | 116 | ##Version 1.0.2 (August 24, 2011) 117 | 118 | - Add before/afterInject options to buildTransactionForm and buildBillingInfoUpdateForm 119 | 120 | ##Version 1.0.1 (August 24, 2011) 121 | 122 | - Fix a bug with VAT not applying when buyer and seller are in the same country 123 | 124 | ##Version 1.0.0 (August 23, 2011) 125 | 126 | - Initial public release 127 | -------------------------------------------------------------------------------- /src/js/subscription.js: -------------------------------------------------------------------------------- 1 | // Base Subscription prototype 2 | R.Subscription = { 3 | create: createObject 4 | , plan: R.Plan 5 | , addOns: [] 6 | 7 | , calculateTotals: function() { 8 | var totals = { 9 | stages: {} 10 | }; 11 | 12 | // PLAN 13 | totals.plan = this.plan.cost.mult(this.plan.quantity); 14 | 15 | // ADD-ONS 16 | totals.allAddOns = new R.Cost(0); 17 | totals.addOns = {}; 18 | for(var l=this.addOns.length, i=0; i < l; ++i) { 19 | var a = this.addOns[i], 20 | c = a.cost.mult(a.quantity); 21 | totals.addOns[a.code] = c; 22 | totals.allAddOns = totals.allAddOns.add(c); 23 | } 24 | 25 | totals.stages.recurring = totals.plan.add(totals.allAddOns); 26 | 27 | totals.stages.now = totals.plan.add(totals.allAddOns); 28 | 29 | // FREE TRIAL 30 | if(this.plan.trial) { 31 | totals.stages.now = R.Cost.FREE; 32 | } 33 | 34 | // COUPON 35 | if(this.coupon) { 36 | var beforeDiscount = totals.stages.now; 37 | var afterDiscount = totals.stages.now.discount(this.coupon); 38 | totals.coupon = afterDiscount.sub(beforeDiscount); 39 | totals.stages.now = afterDiscount; 40 | } 41 | 42 | // SETUP FEE 43 | if(this.plan.setupFee) { 44 | totals.stages.now = totals.stages.now.add(this.plan.setupFee); 45 | } 46 | 47 | // VAT 48 | if(this.billingInfo && R.isVATChargeApplicable(this.billingInfo.country,this.billingInfo.vatNumber)) { 49 | totals.vat = totals.stages.now.mult( (R.settings.VATPercent/100) ); 50 | totals.stages.now = totals.stages.now.add(totals.vat); 51 | } 52 | 53 | return totals; 54 | } 55 | , redeemAddOn: function(addOn) { 56 | var redemption = addOn.createRedemption(); 57 | this.addOns.push(redemption); 58 | return redemption; 59 | } 60 | 61 | , removeAddOn: function(code) { 62 | for(var a=this.addOns, l=a.length, i=0; i < l; ++i) { 63 | if(a[i].code == code) { 64 | return a.splice(i,1); 65 | } 66 | } 67 | } 68 | 69 | , findAddOnByCode: function(code) { 70 | for(var l=this.addOns.length, i=0; i < l; ++i) { 71 | if(this.addOns[i].code == code) { 72 | return this.addOns[i]; 73 | } 74 | } 75 | return false; 76 | } 77 | 78 | , toJSON: function() { 79 | var json = { 80 | plan_code: this.plan.code 81 | , quantity: this.plan.quantity 82 | , currency: this.plan.currency 83 | , coupon_code: this.coupon ? this.coupon.code : undefined 84 | , add_ons: [] 85 | }; 86 | 87 | for(var i=0, l=this.addOns.length, a=json.add_ons, b=this.addOns; i < l; ++i) { 88 | a.push({ 89 | add_on_code: b[i].code 90 | , quantity: b[i].quantity 91 | }); 92 | } 93 | 94 | return json; 95 | } 96 | 97 | , save: function(options) { 98 | var json = { 99 | subscription: this.toJSON() 100 | , account: this.account.toJSON() 101 | , billing_info: this.billingInfo.toJSON() 102 | , signature: options.signature 103 | }; 104 | 105 | R.ajax({ 106 | url: R.settings.baseURL+'subscribe', 107 | data: json, 108 | dataType: "jsonp", 109 | jsonp: "callback", 110 | timeout: 60000, 111 | success: function(data) { 112 | if(data.success && options.success) { 113 | options.success(data.success); 114 | } 115 | else if(data.errors && options.error) { 116 | var errorCode = data.errors.error_code; 117 | delete data.errors.error_code; 118 | options.error( R.flattenErrors(data.errors), errorCode ); 119 | } 120 | }, 121 | error: function() { 122 | if(options.error) { 123 | options.error(['Unknown error processing transaction. Please try again later.']); 124 | } 125 | }, 126 | complete: options.complete 127 | }); 128 | 129 | } 130 | }; 131 | 132 | R.AddOn.createRedemption = function(qty) { 133 | var r = createObject(this); 134 | r.quantity = qty || 1; 135 | return r; 136 | }; 137 | 138 | R.Coupon = { 139 | fromJSON: function(json) { 140 | var c = createObject(R.Coupon); 141 | 142 | if(json.discount_in_cents) 143 | c.discountCost = new R.Cost(-json.discount_in_cents); 144 | else if(json.discount_percent) 145 | c.discountRatio = json.discount_percent/100; 146 | 147 | c.description = json.description; 148 | 149 | return c; 150 | } 151 | 152 | , toJSON: function() { 153 | } 154 | }; 155 | 156 | R.Cost.prototype.discount = function(coupon){ 157 | if(coupon.discountCost) 158 | return this.add(coupon.discountCost); 159 | 160 | var ret = this.sub( this.mult(coupon.discountRatio) ); 161 | if(ret.cents() < 0) { 162 | return R.Cost.FREE; 163 | } 164 | 165 | return ret; 166 | }; 167 | 168 | R.Subscription.getCoupon = function(couponCode, successCallback, errorCallback) { 169 | 170 | if(!R.settings.baseURL) { R.raiseError('Company subdomain not configured'); } 171 | 172 | var couponCurrencyQuery = (R.settings.currency !== undefined ? '?currency='+R.settings.currency : ''); 173 | 174 | return R.ajax({ 175 | url: R.settings.baseURL+'plans/'+this.plan.code+'/coupons/'+couponCode+couponCurrencyQuery, 176 | // data: params, 177 | dataType: "jsonp", 178 | jsonp: "callback", 179 | timeout: 10000, 180 | success: function(data) { 181 | if(data.valid) { 182 | var coupon = R.Coupon.fromJSON(data); 183 | coupon.code = couponCode; 184 | successCallback(coupon); 185 | } 186 | else { 187 | errorCallback(); 188 | } 189 | }, 190 | error: function() { 191 | errorCallback(); 192 | } 193 | }); 194 | }; 195 | 196 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | R.knownCards = { 2 | 'visa': { 3 | prefixes: [4] 4 | , name: 'Visa' 5 | } 6 | , 'mastercard': { 7 | prefixes: [51, 52, 53, 54, 55] 8 | , name: 'MasterCard' 9 | } 10 | , 'american_express': { 11 | prefixes: [34, 37] 12 | , name: 'American Express' 13 | } 14 | , 'discover': { 15 | prefixes: [6011, 62, 64, 65] 16 | , name: 'Discover' 17 | } 18 | , 'diners_club': { 19 | prefixes: [305, 36, 38] 20 | , name: 'Diners Club' 21 | } 22 | , 'carte_blanche': { 23 | prefixes: [300, 301, 302, 303, 304, 305] 24 | } 25 | , 'jcb': { 26 | prefixes: [35] 27 | , name: 'JCB' 28 | } 29 | , 'enroute': { 30 | prefixes: [2014, 2149] 31 | , name: 'EnRoute' 32 | } 33 | , 'maestro': { 34 | prefixes: [5018, 5020, 5038, 6304, 6759, 6761] 35 | , name: 'Maestro' 36 | } 37 | , 'laser': { 38 | prefixes: [6304, 6706, 6771, 6709] 39 | , name: 'Laser' 40 | } 41 | }; 42 | 43 | // Credit card type functions 44 | R.detectCardType = function(cardNumber) { 45 | cardNumber = cardNumber.replace(/\D/g, ''); 46 | var cards = R.knownCards; 47 | 48 | for(var ci in cards) { 49 | if(cards.hasOwnProperty(ci)) { 50 | var c = cards[ci]; 51 | for(var pi=0,pl=c.prefixes.length; pi < pl; ++pi) { 52 | if(c.prefixes.hasOwnProperty(pi)) { 53 | var p = c.prefixes[pi]; 54 | if (new RegExp('^' + p.toString()).test(cardNumber)) 55 | return ci; 56 | } 57 | } 58 | } 59 | } 60 | 61 | return false; 62 | }; 63 | 64 | 65 | // Formats currency amount in the current denomination or one provided 66 | // based on R.locale.currencies rules 67 | R.formatCurrency = function(num,denomination) { 68 | 69 | if(num < 0) { 70 | num = -num; 71 | var negative = true; 72 | } 73 | else { 74 | var negative = false; 75 | } 76 | 77 | denomination = denomination || R.settings.currency || 78 | R.raiseError('currency not configured'); 79 | 80 | var langspec = R.locale.currency; 81 | var currencyspec = R.locale.currencies[denomination]; 82 | 83 | // Format to precision 84 | var str = num.toFixed(currencyspec.precision); 85 | 86 | // Replace default period with format separator 87 | if(langspec.separator != '.') { 88 | str = str.replace(/\./g, langspec.separator); 89 | } 90 | 91 | function insertDelimiters(str) { 92 | var sRegExp = new RegExp('(-?[0-9]+)([0-9]{3})'); 93 | while(sRegExp.test(str)) { 94 | str = str.replace(sRegExp, '$1'+langspec.delimiter+'$2'); 95 | } 96 | return str; 97 | } 98 | 99 | // Apply thousands delimiter 100 | str = insertDelimiters(str); 101 | 102 | // Format unit/number order 103 | var format = langspec.format; 104 | format = format.replace(/%u/g, currencyspec.symbol); 105 | format = format.replace(/%n/g, str); 106 | str = format; 107 | 108 | if(negative) { 109 | str = '-' + str; 110 | } 111 | 112 | return str; 113 | }; 114 | 115 | var euCountries = ["AT","BE","BG","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE","GB"]; 116 | R.isCountryInEU = function(country) { 117 | return $.inArray(country, euCountries) !== -1; 118 | } 119 | 120 | R.isVATNumberApplicable = function(buyerCountry, sellerCountry) { 121 | if(!R.settings.VATPercent) return false; 122 | 123 | if(!R.settings.country) { 124 | R.raiseError('you must configure a country for VAT to work'); 125 | } 126 | 127 | if(!R.isCountryInEU(R.settings.country)) { 128 | R.raiseError('you cannot charge VAT outside of the EU'); 129 | } 130 | 131 | // Outside of EU don't even show the number 132 | if(!R.isCountryInEU(buyerCountry)) { 133 | return false; 134 | } 135 | 136 | return true; 137 | } 138 | 139 | R.isVATChargeApplicable = function(buyerCountry, vatNumber) { 140 | // We made it so the VAT Number is collectable in any case 141 | // where it could be charged, so this is logically sound: 142 | if(!R.isVATNumberApplicable(buyerCountry)) return false; 143 | 144 | var sellerCountry = R.settings.country; 145 | 146 | // 1) Outside EU never pays 147 | // 2) Same country in EU always pays 148 | // 3) Different countries in EU, pay only without vatNumber 149 | return (sellerCountry == buyerCountry || !vatNumber); 150 | }; 151 | 152 | R.flattenErrors = function(obj, attr) { 153 | var arr = []; 154 | 155 | var attr = attr || ''; 156 | 157 | if( typeof obj == 'string' 158 | || typeof obj == 'number' 159 | || typeof obj == 'boolean') { 160 | 161 | if (attr == 'base') { 162 | return [obj]; 163 | } 164 | 165 | return ['' + attr + ' ' + obj]; 166 | } 167 | 168 | for(var k in obj) { 169 | // console.log(k); 170 | if(obj.hasOwnProperty(k)) { 171 | // Inherit parent attribute names when property key 172 | // is a numeric string; how we deal with arrays 173 | attr = (parseInt(k).toString() == k) ? attr : k; 174 | var children = R.flattenErrors(obj[k], attr); 175 | for(var i=0, l=children.length; i < l; ++i) { 176 | arr.push(children[i]); 177 | } 178 | } 179 | } 180 | 181 | return arr; 182 | }; 183 | 184 | // Very small function, but defining for D.R.Y.ness 185 | R.getToken = function(response) { 186 | var token = response.token || 'INVALIDTOKEN'; 187 | return token; 188 | } 189 | 190 | // POST the results from Recurly to the merchant's webserver 191 | R.postResult = function(url, originalResponse, options) { 192 | var token = R.getToken(originalResponse); 193 | 194 | var form = $('
').hide(); 195 | form.attr('action', url) 196 | .attr('method', 'POST') 197 | .attr('enctype', 'application/x-www-form-urlencoded'); 198 | 199 | $('').attr({name: 'recurly_token', value: token}).appendTo(form); 200 | 201 | $('body').append(form); 202 | form.submit(); 203 | }; 204 | 205 | function jsonToSelect(obj) { 206 | var $select = $('' + 849 | '' + 850 | '
' + 851 | '
'); 852 | if(!addOn.displayQuantity) { 853 | $addOn.find('.quantity').remove(); 854 | } 855 | $addOn.data('add_on', addOn); 856 | $addOn.appendTo($addOnsList); 857 | } 858 | 859 | // Quantity Change 860 | $addOnsList.delegate('.add_ons .quantity input', 'change keyup', function(e) { 861 | var $addOn = $(this).closest('.add_on'); 862 | var addOn = $addOn.data('add_on'); 863 | var newQty = parseInt($(this).val(),10) || 1; 864 | subscription.findAddOnByCode(addOn.code).quantity = newQty; 865 | updateTotals(); 866 | }); 867 | 868 | $addOnsList.bind('selectstart', function(e) { 869 | if($(e.target).is('.add_on')) { 870 | e.preventDefault(); 871 | } 872 | }); 873 | 874 | // Add-on click 875 | $addOnsList.delegate('.add_ons .add_on', 'click', function(e) { 876 | if($(e.target).closest('.quantity').length) return; 877 | 878 | var selected = !$(this).hasClass('selected'); 879 | $(this).toggleClass('selected', selected); 880 | 881 | var addOn = $(this).data('add_on'); 882 | 883 | if(selected) { 884 | // add 885 | var sa = subscription.redeemAddOn(addOn); 886 | var $qty = $(this).find('.quantity input'); 887 | sa.quantity = parseInt($qty.val(),10) || 1; 888 | $qty.focus(); 889 | } 890 | else { 891 | // remove 892 | subscription.removeAddOn(addOn.code); 893 | } 894 | 895 | updateTotals(); 896 | }); 897 | } 898 | } 899 | else { 900 | $addOnsList.remove(); 901 | } 902 | 903 | // == COUPON REDEEMER 904 | var $coupon = $form.find('.coupon'); 905 | var lastCode = null; 906 | 907 | function updateCoupon() { 908 | 909 | var code = $coupon.find('input').val(); 910 | if(code == lastCode) { 911 | return; 912 | } 913 | 914 | lastCode = code; 915 | 916 | if(!code) { 917 | $coupon.removeClass('invalid').removeClass('valid'); 918 | $coupon.find('.description').text(''); 919 | subscription.coupon = undefined; 920 | updateTotals(); 921 | return; 922 | } 923 | 924 | $coupon.addClass('checking'); 925 | subscription.getCoupon(code, function(coupon) { 926 | 927 | $coupon.removeClass('checking'); 928 | 929 | subscription.coupon = coupon; 930 | $coupon.removeClass('invalid').addClass('valid'); 931 | $coupon.find('.description').text(coupon.description); 932 | 933 | updateTotals(); 934 | }, function() { 935 | 936 | subscription.coupon = undefined; 937 | 938 | $coupon.removeClass('checking'); 939 | $coupon.removeClass('valid').addClass('invalid'); 940 | $coupon.find('.description').text('Not Found'); 941 | 942 | updateTotals(); 943 | }); 944 | } 945 | 946 | if(options.enableCoupons) { 947 | $coupon.find('input').bind('keyup change', function(e) { 948 | }); 949 | 950 | $coupon.find('input').keypress(function(e) { 951 | if(e.charCode == 13) { 952 | e.preventDefault(); 953 | updateCoupon(); 954 | } 955 | }); 956 | 957 | 958 | $coupon.find('.check').click(function() { 959 | updateCoupon(); 960 | }); 961 | 962 | $coupon.find('input').blur(function() { $coupon.find('.check').click(); }); 963 | } 964 | else { 965 | $coupon.remove(); 966 | } 967 | 968 | 969 | // == VAT 970 | var $vat = $form.find('.vat'); 971 | var $vatNumber = $form.find('.vat_number'); 972 | var $vatNumberInput = $vatNumber.find('input'); 973 | 974 | $vat.find('.title').text('VAT at ' + R.settings.VATPercent + '%'); 975 | function showHideVAT() { 976 | var buyerCountry = $form.find('.country select').val(); 977 | var vatNumberApplicable = R.isVATNumberApplicable(buyerCountry); 978 | 979 | // VAT Number is applicable to collection in any EU country 980 | $vatNumber.toggleClass('applicable', vatNumberApplicable); 981 | $vatNumber.toggleClass('inapplicable', !vatNumberApplicable); 982 | 983 | var vatNumber = $vatNumberInput.val(); 984 | 985 | // Only applicable to charge if isVATApplicable() 986 | var chargeApplicable = R.isVATChargeApplicable(buyerCountry, vatNumber); 987 | $vat.toggleClass('applicable', chargeApplicable); 988 | $vat.toggleClass('inapplicable', !chargeApplicable); 989 | } 990 | // showHideVAT(); 991 | $form.find('.country select').change(function() { 992 | billingInfo.country = $(this).val(); 993 | updateTotals(); 994 | showHideVAT(); 995 | }).change(); 996 | $vatNumberInput.bind('keyup change', function() { 997 | billingInfo.vatNumber = $(this).val(); 998 | updateTotals(); 999 | showHideVAT(); 1000 | }); 1001 | 1002 | // SUBMIT HANDLER 1003 | $form.submit(function(e) { 1004 | e.preventDefault(); 1005 | 1006 | clearServerErrors($form); 1007 | 1008 | 1009 | $form.find('.error').remove(); 1010 | $form.find('.invalid').removeClass('invalid'); 1011 | 1012 | validationGroup(function(puller) { 1013 | pullPlanQuantity($form, subscription.plan, options, puller); 1014 | pullAccountFields($form, account, options, puller); 1015 | pullBillingInfoFields($form, billingInfo, options, puller); 1016 | verifyTOSChecked($form, puller); 1017 | }, function() { 1018 | 1019 | $form.addClass('submitting'); 1020 | $form.find('button.submit').attr('disabled', true).text('Please Wait'); 1021 | 1022 | subscription.save({ 1023 | 1024 | signature: options.signature 1025 | , success: function(response) { 1026 | if(options.successHandler) { 1027 | options.successHandler(R.getToken(response)); 1028 | } 1029 | if(options.successURL) { 1030 | var url = options.successURL; 1031 | R.postResult(url, response, options); 1032 | } 1033 | } 1034 | , error: function(errors) { 1035 | if(!options.onError || !options.onError(errors)) { 1036 | displayServerErrors($form, errors); 1037 | } 1038 | } 1039 | , complete: function() { 1040 | $form.removeClass('submitting'); 1041 | $form.find('button.submit').removeAttr('disabled').text('Subscribe'); 1042 | } 1043 | }); 1044 | }); 1045 | 1046 | }); 1047 | 1048 | // FINALLY - UPDATE INITIAL TOTALS AND INJECT INTO DOM 1049 | updateTotals(); 1050 | 1051 | if(options.beforeInject) { 1052 | options.beforeInject($form.get(0)); 1053 | } 1054 | 1055 | $(function() { 1056 | var $container = $(options.target); 1057 | $container.html($form); 1058 | 1059 | if(options.afterInject) { 1060 | options.afterInject($form.get(0)); 1061 | } 1062 | }); 1063 | 1064 | } 1065 | 1066 | }; 1067 | 1068 | --------------------------------------------------------------------------------