├── .gitignore ├── LICENSE ├── README.md ├── client ├── billing.coffee ├── i18n │ ├── arabic.coffee │ ├── english.coffee │ └── french.coffee ├── index.html ├── lib │ └── parsley.css ├── startup.coffee ├── styles.less └── views │ ├── charges │ ├── charges.coffee │ ├── charges.html │ └── charges.less │ ├── creditCard │ ├── creditCard.coffee │ ├── creditCard.html │ └── creditCard.less │ ├── currentCreditCard │ ├── currentCreditCard.coffee │ └── currentCreditCard.html │ └── invoices │ ├── invoices.coffee │ ├── invoices.html │ └── invoices.less ├── collections └── users.coffee ├── package.js ├── packages └── .gitignore ├── public └── img │ ├── credit-cards.png │ └── cvc.png ├── server ├── billing.coffee ├── methods.coffee ├── startup.coffee └── webhooks.coffee └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-project 2 | *.sublime-workspace 3 | .build* 4 | .npm 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Differential 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | meteor-billing 2 | ============== 3 | 4 | Package for common billing functionality. 5 | See full [documentation](http://github.differential.com/meteor-billing/). 6 | -------------------------------------------------------------------------------- /client/billing.coffee: -------------------------------------------------------------------------------- 1 | Result = -> 2 | _dep: new Deps.Dependency 3 | _val: null 4 | _hasRun: false 5 | 6 | _get: -> 7 | @_dep.depend() 8 | @_val 9 | 10 | _set: (val) -> 11 | unless EJSON.equals @_val, val 12 | @_val = val 13 | @_dep.changed() 14 | 15 | 16 | @Billing = 17 | settings: 18 | publishableKey: '' 19 | requireAddress: false 20 | requireName: false 21 | showInvoicePeriod: true 22 | showPricingPlan: true 23 | invoiceExplaination: '' 24 | currency: '$' 25 | language: 'en' 26 | ddBeforeMm: false #for countries with date format dd/mm/yyyy 27 | 28 | config: (opts) -> 29 | @settings = _.extend @settings, opts 30 | T9n.language = @settings.language 31 | 32 | isValid: -> 33 | $('form#billing-creditcard').parsley().validate() 34 | 35 | createToken: (form, callback) -> 36 | Stripe.setPublishableKey(@settings.publishableKey); 37 | $form = $(form) 38 | Stripe.card.createToken( 39 | name: $(form).find('[name=cc-name]').val() 40 | number: $form.find('[name=cc-num]').val() 41 | exp_month: $form.find('[name=cc-exp-month]').val() 42 | exp_year: $form.find('[name=cc-exp-year]').val() 43 | cvc: $form.find('[name=cc-cvc]').val() 44 | address_line1: $form.find('[name=cc-address-line-1]').val() 45 | address_line2: $form.find('[name=cc-address-line-2]').val() 46 | address_city: $form.find('[name=cc-address-city]').val() 47 | address_state: $form.find('[name=cc-address-state]').val() 48 | address_zip: $form.find('[name=cc-address-zip]').val() 49 | , callback) # callback(status, response) 50 | 51 | _results: {} 52 | 53 | getResults: (methodName) -> 54 | unless @_results[methodName] 55 | @_results[methodName] = new Result 56 | 57 | # Get the result 58 | res = @_results[methodName]._get() 59 | 60 | # If its an error object - return falsy value 61 | if res.error then res = null else res 62 | 63 | # Null out to ensure fresh data subsequent calls 64 | if res then @_results[methodName] = null 65 | 66 | # Return it! 67 | res 68 | 69 | call: (methodName, params) -> 70 | results = @_results 71 | 72 | unless results[methodName] 73 | results[methodName] = new Result 74 | 75 | unless results[methodName]._hasRun 76 | results[methodName]._hasRun = true 77 | args = Array.prototype.splice.call arguments, 1 78 | Meteor.apply methodName, args, (err, res) -> 79 | results[methodName]._set if err then err else res 80 | 81 | ready: -> 82 | res = results[methodName]._get() 83 | if res then true else false -------------------------------------------------------------------------------- /client/i18n/arabic.coffee: -------------------------------------------------------------------------------- 1 | ar = 2 | # Billing 3 | "Past Invoices": "فواتير سابقة" 4 | "Upcoming Invoice": "فواتير قادمة" 5 | "Cancel Subscription": "الغاء الاشتراك" 6 | "Confirm Cancelling Subscription": "تأكيد الغاء الاشتراك" 7 | "Are you sure you want to cancel your subscription?": "هل انت متأكد انك تريد الغاء اشتراكك؟ سوف نحفظ بياناتك حتى تتمكن من اعادة حسابك فى اى وقت" 8 | "Cancel": "الغاء" 9 | "on": "على" 10 | "Yes, cancel my subscription": "نعم, الغى اشتراكى" 11 | "Line Items": "خط الاصناف" 12 | "Paid": "مدفوع" 13 | "month": "شهر" 14 | "Nothing to see here": "لا شيئ هنا" 15 | 16 | # Credit Card 17 | "Name": "الإسم" 18 | "Billing Info": "بيانات الدفع" 19 | "Card Number": "رقم كارت الائتمان" 20 | "Expiration": "تاريخ الانتهاء" 21 | "Card Code": "رمز الكارت السرى" 22 | "Subscription to": "الاشتراك ب" 23 | "Address": "العنوان" 24 | 25 | # Notifications 26 | "Error getting past invoices": "خطأ فى ايجاد الفواتير السابقة" 27 | "Error getting upcoming invoice": "خطأ فى ايجاد الفواتير القادمة" 28 | "Error canceling subscription": "خطأ فى الغاء الاشتراك" 29 | "Your subscription has been canceled": "تم الغاء اشتراكك" 30 | 31 | i18n.map "ar", ar 32 | -------------------------------------------------------------------------------- /client/i18n/english.coffee: -------------------------------------------------------------------------------- 1 | en = 2 | # Billing 3 | "Past Charges": "Past Charges" 4 | "Past Invoices": "Past Invoices" 5 | "Upcoming Invoice": "Upcoming Invoice" 6 | "Cancel Subscription": "Cancel Subscription" 7 | "Confirm Cancelling Subscription": "Confirm Cancelling Subscription" 8 | "Are you sure you want to cancel your subscription?": "Are you sure you want to cancel your subscription? Your data will be saved, so you can re-enable your account at any time." 9 | "Cancel": "Cancel" 10 | "on": "on" 11 | "Yes, cancel my subscription": "Yes, cancel my subscription" 12 | "Line Items": "Line Items" 13 | "Paid": "Paid" 14 | "month": "month" 15 | "Nothing to see here": "Nothing to see here." 16 | 17 | # Credit Card 18 | "Name": "Name" 19 | "Billing Info": "Billing Info" 20 | "Card Number": "Card Number" 21 | "Expiration": "Expiration" 22 | "Card Code": "Card Code" 23 | "Subscription to": "Subscription to" 24 | "Address": "Address" 25 | 26 | # Notifications 27 | "Error getting past charges": "Error getting past charges." 28 | "Error getting past invoices": "Error getting past invoices." 29 | "Error getting upcoming invoice": "Error getting upcoming invoice." 30 | "Error canceling subscription": "Error canceling subscription." 31 | "Your subscription has been canceled": "Your subscription has been canceled." 32 | 33 | i18n.map "en", en 34 | -------------------------------------------------------------------------------- /client/i18n/french.coffee: -------------------------------------------------------------------------------- 1 | fr= 2 | "Past Charges": "Transactions passées", 3 | "Past Invoices": "Reçus", 4 | "Upcoming Invoice": "Prochain Reçu", 5 | "Cancel Subscription": "Annuler la souscription", 6 | "Confirm Cancelling Subscription": "Confirmez l'annulation de la souscription", 7 | "Are you sure you want to cancel your subscription?": "Etes-vous sur de vouloir annuler la souscription? Vos données sont sauvegardées, vous pouvez souscrire de nouveau à tout moment", 8 | "Cancel": "Annuler", 9 | "on": "sur", 10 | "Yes, cancel my subscription": "Oui, annulez ma souscription", 11 | "Line Items": "Line Items", 12 | "Paid": "Payé", 13 | "month": "mois", 14 | "Nothing to see here": "Rien à signaler.", 15 | "Name": "Nom", 16 | "Billing Info": "Informations", 17 | "Card Number": "Numéro", 18 | "Expiration": "Date d'expiration", 19 | "Card Code": "Code CCV", 20 | "Subscription to": "Souscription à", 21 | "Address": "Adresse", 22 | "Error getting past charges": "Erreur d'accès aux transactions passées.", 23 | "Error getting past invoices": "Erreur d'accès aux reçus passées.", 24 | "Error getting upcoming invoice": "Erreur d'accès au prochain reçu.", 25 | "Error canceling subscription": "Erreur survenue pour annuler votre souscription.", 26 | "Your subscription has been canceled": "Votre souscription a bien été annulée." 27 | 28 | i18n.map "fr", fr 29 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/lib/parsley.css: -------------------------------------------------------------------------------- 1 | input.parsley-success, 2 | select.parsley-success, 3 | textarea.parsley-success { 4 | color: #468847; 5 | background-color: #DFF0D8; 6 | border: 1px solid #D6E9C6; 7 | } 8 | 9 | input.parsley-error, 10 | select.parsley-error, 11 | textarea.parsley-error { 12 | color: #B94A48; 13 | background-color: #F2DEDE; 14 | border: 1px solid #EED3D7; 15 | } 16 | 17 | .parsley-errors-list { 18 | margin: 2px 0 3px 0; 19 | padding: 0; 20 | list-style-type: none; 21 | font-size: 0.9em; 22 | line-height: 0.9em; 23 | opacity: 0; 24 | -moz-opacity: 0; 25 | -webkit-opacity: 0; 26 | 27 | transition: all .3s ease-in; 28 | -o-transition: all .3s ease-in; 29 | -ms-transition: all .3s ease-in-; 30 | -moz-transition: all .3s ease-in; 31 | -webkit-transition: all .3s ease-in; 32 | } 33 | 34 | .parsley-errors-list.filled { 35 | opacity: 1; 36 | } 37 | -------------------------------------------------------------------------------- /client/startup.coffee: -------------------------------------------------------------------------------- 1 | Meteor.startup -> 2 | Meteor.subscribe 'currentUser' -------------------------------------------------------------------------------- /client/styles.less: -------------------------------------------------------------------------------- 1 | // .error-msg { 2 | // color: #b94a48; 3 | // font-size: .9em; 4 | // } 5 | 6 | ul.parsley-error-list{ 7 | font-size: .8em; 8 | color: #787878; 9 | font-style: italic; 10 | list-style-type: none; 11 | padding: 0; 12 | margin: 0; 13 | } -------------------------------------------------------------------------------- /client/views/charges/charges.coffee: -------------------------------------------------------------------------------- 1 | Template.charges.created = -> 2 | Session.set 'charges.error', null 3 | Session.set 'charges.success', null 4 | Session.set 'charges.charges', null 5 | 6 | Template.charges.rendered = -> 7 | usr = BillingUser.current() 8 | 9 | unless usr then return 10 | 11 | if not Session.get 'charges.charges' 12 | Session.set 'charges.working', true 13 | Meteor.call 'listCharges', (error, response) -> 14 | Session.set 'charges.working', false 15 | if error 16 | if error.error is 404 17 | Session.set 'charges.charges', null 18 | else 19 | Session.set 'charges.error', t9n('Error getting charges') 20 | else 21 | Session.set 'charges.charges', response.data 22 | 23 | inDollars = (amt) -> 24 | currency = Billing.settings.currency 25 | if amt >= 0 26 | "#{currency}#{(amt / 100).toFixed(2)}" 27 | else 28 | "-#{currency}#{Math.abs(amt / 100).toFixed(2)}" 29 | 30 | formatDate = (timestamp) -> 31 | d = new Date(timestamp * 1000) 32 | monthOrDay = [d.getMonth()+1, d.getDate()] 33 | if Billing.settings.ddBeforeMm 34 | monthOrDay.reverse() 35 | monthOrDay.join('/') + '/' + d.getFullYear() 36 | 37 | 38 | Template.charges.helpers 39 | billingError: -> 40 | Session.get 'charges.error' 41 | 42 | billingSuccess: -> 43 | Session.get 'charges.success' 44 | 45 | charges: -> 46 | Session.get 'charges.charges' 47 | 48 | chargesWorking: -> 49 | Session.get 'charges.working' 50 | 51 | 52 | Template._charge.helpers 53 | chargeDate: (timestamp) -> 54 | formatDate(timestamp) 55 | 56 | chargeAmt: (amt) -> 57 | inDollars(amt) 58 | -------------------------------------------------------------------------------- /client/views/charges/charges.html: -------------------------------------------------------------------------------- 1 | 29 | 30 | 57 | -------------------------------------------------------------------------------- /client/views/charges/charges.less: -------------------------------------------------------------------------------- 1 | .charges{ 2 | 3 | .panel { 4 | .panel-heading{ 5 | padding: 10px 8px; 6 | a{ 7 | text-decoration: none; 8 | } 9 | } 10 | .panel-body{ 11 | padding: 8px; 12 | } 13 | } 14 | 15 | .billing-loader{ 16 | color: rgba(5, 5, 5, 0.2); 17 | text-align: center; 18 | } 19 | 20 | .charge{ 21 | font-size: 1em; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/views/creditCard/creditCard.coffee: -------------------------------------------------------------------------------- 1 | Template.creditCard.helpers 2 | requireAddress: -> 3 | Billing.settings.requireAddress 4 | 5 | requireName: -> 6 | Billing.settings.requireName -------------------------------------------------------------------------------- /client/views/creditCard/creditCard.html: -------------------------------------------------------------------------------- 1 | 79 | -------------------------------------------------------------------------------- /client/views/creditCard/creditCard.less: -------------------------------------------------------------------------------- 1 | .credit-card{ 2 | img { 3 | height: 20px; 4 | margin-left: 10px; 5 | } 6 | 7 | label { 8 | img { 9 | height: 15px; 10 | margin-left: 10px; 11 | position: relative; 12 | top: -2px; 13 | } 14 | } 15 | 16 | .expiration { 17 | 18 | input { 19 | text-align: center; 20 | } 21 | 22 | span { 23 | position: absolute; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /client/views/currentCreditCard/currentCreditCard.coffee: -------------------------------------------------------------------------------- 1 | currentCardComputation = null 2 | 3 | Template.currentCreditCard.rendered = -> 4 | currentCardComputation = Deps.autorun -> 5 | billing = Meteor.user().billing 6 | if billing and billing.customerId and billing.cardId 7 | Meteor.call 'retrieveCard', Meteor.userId(), (err, response) -> 8 | unless err then Session.set 'currentCreditCard.card', response 9 | 10 | Template.currentCreditCard.helpers 11 | card: -> 12 | Session.get 'currentCreditCard.card' 13 | 14 | Template.currentCreditCard.destroyed = -> 15 | if currentCardComputation then currentCardComputation.stop() 16 | -------------------------------------------------------------------------------- /client/views/currentCreditCard/currentCreditCard.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/invoices/invoices.coffee: -------------------------------------------------------------------------------- 1 | Template.invoices.created = -> 2 | Session.set 'invoices.error', null 3 | Session.set 'invoices.success', null 4 | Session.set 'invoices.invoices.past', null 5 | Session.set 'invoices.invoices.upcoming', null 6 | 7 | Template.invoices.rendered = -> 8 | usr = BillingUser.current() 9 | 10 | unless usr then return 11 | 12 | if usr.billing and not Session.get 'invoices.invoices.past' 13 | Session.set 'invoices.past.working', true 14 | Meteor.call 'getInvoices', (error, response) -> 15 | Session.set 'invoices.past.working', false 16 | if error 17 | if error.error is 404 18 | Session.set 'invoices.invoices.past', null 19 | else 20 | Session.set 'invoices.error', t9n('Error getting past invoices') 21 | else 22 | Session.set 'invoices.invoices.past', response.data 23 | 24 | if usr.billing and usr.billing.subscriptionId and not Session.get 'invoices.invoices.upcoming' 25 | Session.set 'invoices.upcoming.working', true 26 | Meteor.call 'getUpcomingInvoice', (error, response) -> 27 | Session.set 'invoices.upcoming.working', false 28 | if error 29 | if error.error is 404 30 | Session.set 'invoices.invoices.upcoming', null 31 | else 32 | Session.set 'invoices.error', t9n('Error getting upcoming invoice') 33 | else 34 | response.id = new Meteor.Collection.ObjectID().toHexString() 35 | Session.set 'invoices.invoices.upcoming', response 36 | 37 | inDollars = (amt) -> 38 | currency = Billing.settings.currency 39 | if amt >= 0 40 | "#{currency}#{(amt / 100).toFixed(2)}" 41 | else 42 | "-#{currency}#{Math.abs(amt / 100).toFixed(2)}" 43 | 44 | formatDate = (timestamp) -> 45 | d = new Date(timestamp * 1000) 46 | monthOrDay = [d.getMonth()+1, d.getDate()] 47 | if Billing.settings.ddBeforeMm 48 | monthOrDay.reverse() 49 | monthOrDay.join('/') + '/' + d.getFullYear() 50 | 51 | 52 | Template.invoices.helpers 53 | billingError: -> 54 | Session.get 'invoices.error' 55 | 56 | billingSuccess: -> 57 | Session.get 'invoices.success' 58 | 59 | cancelingSubscription: -> 60 | Session.get 'invoices.cancelingSubscription' 61 | 62 | invoices: -> 63 | Session.get 'invoices.invoices.past' 64 | 65 | upcomingInvoice: -> 66 | Session.get 'invoices.invoices.upcoming' 67 | 68 | showPricingPlan: -> 69 | Billing.settings.showPricingPlan 70 | 71 | pricingPlan: -> 72 | upcomingInvoice = Session.get 'invoies.invoices.upcoming' 73 | if upcomingInvoice 74 | sub = _.findWhere upcomingInvoice.lines.data, type: 'subscription' 75 | plan = sub.plan 76 | "#{inDollars(plan.amount)}/#{t9n(plan.interval)}" 77 | 78 | hasSubscription: -> 79 | usr = BillingUser.current() 80 | usr and usr.billing and BillingUser.current().billing.subscriptionId 81 | 82 | pastInvoicesWorking: -> 83 | Session.get 'invoices.past.working' 84 | 85 | upcomingInvoiceWorking: -> 86 | Session.get 'invoices.upcoming.working' 87 | 88 | 89 | Template.invoices.events 90 | 'click #cancel-subscription': (e) -> 91 | e.preventDefault() 92 | $('#confirm-cancel-subscription').modal('show') 93 | 94 | 95 | # 96 | # Cancel Subscription Modal 97 | # 98 | Template.cancelSubscriptionModal.created = -> 99 | Session.set 'invoices.cancelingSubscription', false 100 | 101 | Template.cancelSubscriptionModal.events 102 | 'click .btn-confirm-cancel-subscription': (e) -> 103 | e.preventDefault() 104 | $('#confirm-cancel-subscription').modal('hide') 105 | Session.set 'invoices.cancelingSubscription', true 106 | Meteor.call 'cancelSubscription', Meteor.user().billing.customerId, (error, response) -> 107 | Session.set 'invoices.cancelingSubscription', false 108 | if error 109 | Session.set 'invoices.error', t9n('Error canceling subscription') 110 | else 111 | Session.set 'invoices.success', t9n('Your subscription has been canceled') 112 | 113 | 114 | Template._invoice.helpers 115 | lineItemDescription: -> 116 | if @type is 'subscription' 117 | "#{t9n("Subscription to")} #{@plan.name} (#{inDollars(@plan.amount)}/#{t9n(@plan.interval)})" 118 | else if @type is 'invoiceitem' 119 | @description 120 | 121 | lineItemPeriod: -> 122 | if @period.start is @period.end then formatDate(@period.start) 123 | else "#{formatDate(@period.start)} - #{formatDate(@period.end)}" 124 | 125 | invoiceDate: (timestamp) -> 126 | formatDate(timestamp) 127 | 128 | invoiceAmt: (amt) -> 129 | inDollars(amt) 130 | 131 | showInvoicePeriod: -> 132 | Billing.settings.showInvoicePeriod 133 | 134 | invoiceExplaination: -> 135 | Billing.settings.invoiceExplaination -------------------------------------------------------------------------------- /client/views/invoices/invoices.html: -------------------------------------------------------------------------------- 1 | 77 | 78 | 79 | 101 | 102 | 103 | 155 | -------------------------------------------------------------------------------- /client/views/invoices/invoices.less: -------------------------------------------------------------------------------- 1 | .invoices{ 2 | 3 | .panel { 4 | .panel-heading{ 5 | padding: 10px 8px; 6 | a{ 7 | text-decoration: none; 8 | } 9 | } 10 | .panel-body{ 11 | padding: 8px; 12 | 13 | .billing-disclaimer{ 14 | padding-top: 30px; 15 | } 16 | } 17 | } 18 | 19 | .billing-loader{ 20 | color: rgba(5, 5, 5, 0.2); 21 | text-align: center; 22 | } 23 | 24 | .billing-footer{ 25 | margin-top: 80px; 26 | .price{ 27 | text-align: right; 28 | margin: 5px 0px; 29 | font-weight: 500; 30 | font-size: 30px; 31 | } 32 | } 33 | 34 | .invoice{ 35 | font-size: .7em; 36 | } 37 | } -------------------------------------------------------------------------------- /collections/users.coffee: -------------------------------------------------------------------------------- 1 | class BillingUser extends Minimongoid 2 | @_collection: Meteor.users 3 | 4 | @current: -> 5 | @first _id: Meteor.userId() -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "ryw:billing", 3 | summary: "Various billing functionality packaged up.", 4 | version: "2.0.2", 5 | git: "https://github.com/Differential/meteor-billing" 6 | }); 7 | 8 | Npm.depends({ 9 | stripe: "2.8.0" 10 | }); 11 | 12 | Package.on_use(function (api, where) { 13 | api.versionsFrom("METEOR@0.9.0"); 14 | 15 | api.use([ 16 | 'templating', 17 | 'less', 18 | 'jquery', 19 | 'deps', 20 | 'natestrauser:parsleyjs@1.1.7', 21 | 'anti:i18n@0.4.3' 22 | ], 'client'); 23 | 24 | api.use([ 25 | 'accounts-password', 26 | 'arunoda:npm@0.2.6', 27 | 'hellogerard:reststop2@0.5.9' 28 | ], 'server'); 29 | 30 | api.use([ 31 | 'coffeescript', 32 | 'mrt:minimongoid@0.8.3' 33 | ], ['client', 'server']); 34 | 35 | 36 | api.addFiles([ 37 | 'collections/users.coffee' 38 | ], ['client', 'server']); 39 | 40 | api.addFiles([ 41 | 'client/views/creditCard/creditCard.html', 42 | 'client/views/creditCard/creditCard.less', 43 | 'client/views/creditCard/creditCard.coffee', 44 | 'client/views/invoices/invoices.html', 45 | 'client/views/invoices/invoices.coffee', 46 | 'client/views/invoices/invoices.less', 47 | 'client/views/charges/charges.html', 48 | 'client/views/charges/charges.coffee', 49 | 'client/views/charges/charges.less', 50 | 'client/views/currentCreditCard/currentCreditCard.html', 51 | 'client/views/currentCreditCard/currentCreditCard.coffee', 52 | 'client/lib/parsley.css', 53 | 'client/startup.coffee', 54 | 'client/billing.coffee', 55 | 'client/index.html', 56 | 'client/styles.less', 57 | 'public/img/credit-cards.png', 58 | 'public/img/cvc.png', 59 | 'client/i18n/english.coffee', 60 | 'client/i18n/arabic.coffee', 61 | 'client/i18n/french.coffee' 62 | ], 'client'); 63 | 64 | api.addFiles([ 65 | 'server/startup.coffee', 66 | 'server/billing.coffee', 67 | 'server/methods.coffee', 68 | 'server/webhooks.coffee' 69 | ], 'server'); 70 | 71 | api.export('BillingUser', ['server', 'client']); 72 | api.export('i18n', 'client'); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | /meteor-billing 2 | /stripe 3 | -------------------------------------------------------------------------------- /public/img/credit-cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Differential/meteor-billing/589e53c0df11c5ee030468e78287c94588829e29/public/img/credit-cards.png -------------------------------------------------------------------------------- /public/img/cvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Differential/meteor-billing/589e53c0df11c5ee030468e78287c94588829e29/public/img/cvc.png -------------------------------------------------------------------------------- /server/billing.coffee: -------------------------------------------------------------------------------- 1 | @Billing = 2 | settings: {} 3 | config: (opts) -> 4 | defaults = 5 | secretKey: '' 6 | @settings = _.extend defaults, opts -------------------------------------------------------------------------------- /server/methods.coffee: -------------------------------------------------------------------------------- 1 | wrap = (resource, method, params) -> 2 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 3 | call = Async.wrap Stripe[resource], method 4 | try 5 | call params 6 | catch e 7 | console.error e 8 | throw new Meteor.Error 500, e.message 9 | 10 | 11 | Meteor.methods 12 | 13 | # 14 | # Creates stripe customer then updates the user document with the stripe customerId and cardId 15 | # 16 | createCustomer: (userId, card) -> 17 | console.log 'Creating customer for', userId 18 | user = BillingUser.first(_id: userId) 19 | unless user then throw new Meteor.Error 404, "User not found. Customer cannot be created." 20 | 21 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 22 | create = Async.wrap Stripe.customers, 'create' 23 | try 24 | email = if user.emails then user.emails[0].address else '' 25 | customer = create email: email, card: card.id 26 | Meteor.users.update _id: user._id, 27 | $set: 'billing.customerId': customer.id, 'billing.cardId': customer.default_card 28 | catch e 29 | console.error e 30 | throw new Meteor.Error 500, e.message 31 | 32 | # 33 | # Create a card on a customer and set cardId 34 | # 35 | createCard: (userId, card) -> 36 | console.log 'Creating card for', userId 37 | user = BillingUser.first _id: userId 38 | unless user then throw new Meteor.Error 404, "User not found. Card cannot be created." 39 | 40 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 41 | createCard = Async.wrap Stripe.customers, 'createCard' 42 | try 43 | card = createCard user.billing.customerId, card: card.id 44 | user.update('billing.cardId': card.id) 45 | catch e 46 | console.error e 47 | throw new Meteor.Error 500, e.message 48 | 49 | # 50 | # Get details about a customers credit card 51 | # 52 | retrieveCard: (userId) -> 53 | console.log "Retrieving card for #{userId}" 54 | user = BillingUser.first _id: userId 55 | unless user then throw new Meteor.Error 404, "User not found. Cannot retrieve card info." 56 | 57 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 58 | retrieveCard = Async.wrap Stripe.customers, 'retrieveCard' 59 | try 60 | retrieveCard user.billing.customerId, user.billing.cardId 61 | catch e 62 | console.log e 63 | throw new Meteor.Error 500, e.message 64 | 65 | # 66 | # Delete a card on customer and unset cardId 67 | # 68 | deleteCard: (userId) -> 69 | console.log 'Deleting card for', userId 70 | user = BillingUser.first _id: userId 71 | unless user then throw new Meteor.Error 404, "User not found. Card cannot be deleted." 72 | 73 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 74 | deleteCard = Async.wrap Stripe.customers, 'deleteCard' 75 | try 76 | card = deleteCard user.billing.customerId, user.billing.cardId 77 | user.update('billing.cardId': null) 78 | catch e 79 | console.error e 80 | throw new Meteor.Error 500, e.message 81 | 82 | # 83 | # Create a single one-time charge 84 | # 85 | createCharge: (params) -> 86 | console.log "Creating charge" 87 | wrap 'charges', 'create', params 88 | 89 | # 90 | # List charges for user with any filters applied 91 | # 92 | listCharges: (filters) -> 93 | console.log "Getting past charges for", Meteor.userId() 94 | if Meteor.user().billing 95 | params = customer: Meteor.user().billing.customerId 96 | if filters 97 | params = _.extend params, filters 98 | wrap 'charges', 'list', params 99 | else 100 | throw new Meteor.Error 404, "Customer not found. Cannot list charges." 101 | 102 | 103 | # 104 | # Update stripe subscription for user with provided plan and quantitiy 105 | # 106 | updateSubscription: (userId, params) -> 107 | console.log 'Updating subscription for', userId 108 | user = BillingUser.first(_id: userId) 109 | if user then customerId = user.billing.customerId 110 | unless user and customerId then new Meteor.Error 404, "User not found. Subscription cannot be updated." 111 | if user.billing.waiveFees or user.billing.admin then return 112 | 113 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 114 | updateSubscription = Async.wrap Stripe.customers, 'updateSubscription' 115 | try 116 | subscription = updateSubscription customerId, params 117 | Meteor.users.update _id: userId, 118 | $set: 'billing.subscriptionId': subscription.id, 'billing.planId' : params.plan 119 | catch e 120 | console.error e 121 | throw new Meteor.Error 500, e.message 122 | 123 | # 124 | # Manually cancels the stripe subscription for the provided customerId 125 | # 126 | cancelSubscription: (customerId) -> 127 | console.log 'Canceling subscription for', customerId 128 | user = BillingUser.first('billing.customerId': customerId) 129 | unless user then new Meteor.Error 404, "User not found. Subscription cannot be canceled." 130 | 131 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 132 | cancelSubscription = Async.wrap Stripe.customers, 'cancelSubscription' 133 | try 134 | cancelSubscription customerId 135 | catch e 136 | console.error e 137 | throw new Meteor.Error 500, e.message 138 | 139 | 140 | # 141 | # A subscription was deleted from Stripe, remove subscriptionId and card from user. 142 | # 143 | subscriptionDeleted: (customerId) -> 144 | console.log 'Subscription deleted for', customerId 145 | user = BillingUser.first('billing.customerId': customerId) 146 | unless user then new Meteor.Error 404, "User not found. Subscription cannot be deleted." 147 | user.update 'billing.subscriptionId': null, 'billing.planId': null 148 | 149 | 150 | # 151 | # Get past invoices 152 | # 153 | getInvoices: -> 154 | console.log 'Getting past invoices for', Meteor.userId() 155 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 156 | if Meteor.user().billing 157 | customerId = Meteor.user().billing.customerId 158 | try 159 | invoices = Async.wrap(Stripe.invoices, 'list')(customer: customerId) 160 | catch e 161 | console.error e 162 | throw new Meteor.Error 500, e.message 163 | invoices 164 | else 165 | throw new Meteor.Error 404, "No subscription" 166 | 167 | 168 | # 169 | # Get next invoice 170 | # 171 | getUpcomingInvoice: -> 172 | console.log 'Getting upcoming invoice for', Meteor.userId() 173 | Stripe = Npm.require('stripe')(Billing.settings.secretKey) 174 | if Meteor.user().billing 175 | customerId = Meteor.user().billing.customerId 176 | try 177 | invoice = Async.wrap(Stripe.invoices, 'retrieveUpcoming')(customerId) 178 | catch e 179 | console.error e 180 | throw new Meteor.Error 500, e.message 181 | invoice 182 | else 183 | throw new Meteor.Error 404, "No subscription" 184 | -------------------------------------------------------------------------------- /server/startup.coffee: -------------------------------------------------------------------------------- 1 | Meteor.startup -> 2 | 3 | # Publish user with billing object 4 | Meteor.publish 'currentUser', -> 5 | Meteor.users.find _id: @userId, 6 | fields: billing: 1 7 | 8 | # Patch any existing/new users up with an empty billing object 9 | cursor = Meteor.users.find() 10 | cursor.observe 11 | added: (usr) -> 12 | unless usr.billing 13 | Meteor.users.update _id: usr._id, 14 | $set: billing: {} -------------------------------------------------------------------------------- /server/webhooks.coffee: -------------------------------------------------------------------------------- 1 | RESTstop.add 'webhooks', -> 2 | console.log 'Recieving Webhook --- ', @params.type 3 | 4 | switch @params.type 5 | when 'charge.failed' 6 | RESTstop.call @, 'cancelSubscription', @params.data.object.customer 7 | when 'customer.subscription.deleted' 8 | RESTstop.call @, 'subscriptionDeleted', @params.data.object.customer 9 | when 'customer.deleted' 10 | BillingUser.destroyAll 'profile.customerId': @params.data.object.id 11 | else console.log(@params.type, 'was ignored') -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "accounts-base", 5 | "1.1.1" 6 | ], 7 | [ 8 | "accounts-password", 9 | "1.0.2" 10 | ], 11 | [ 12 | "anti:i18n", 13 | "0.4.3" 14 | ], 15 | [ 16 | "application-configuration", 17 | "1.0.2" 18 | ], 19 | [ 20 | "arunoda:npm", 21 | "0.2.6" 22 | ], 23 | [ 24 | "base64", 25 | "1.0.0" 26 | ], 27 | [ 28 | "binary-heap", 29 | "1.0.0" 30 | ], 31 | [ 32 | "blaze", 33 | "2.0.1" 34 | ], 35 | [ 36 | "blaze-tools", 37 | "1.0.0" 38 | ], 39 | [ 40 | "boilerplate-generator", 41 | "1.0.0" 42 | ], 43 | [ 44 | "callback-hook", 45 | "1.0.0" 46 | ], 47 | [ 48 | "check", 49 | "1.0.1" 50 | ], 51 | [ 52 | "coffeescript", 53 | "1.0.3" 54 | ], 55 | [ 56 | "ddp", 57 | "1.0.9" 58 | ], 59 | [ 60 | "deps", 61 | "1.0.4" 62 | ], 63 | [ 64 | "ejson", 65 | "1.0.3" 66 | ], 67 | [ 68 | "email", 69 | "1.0.3" 70 | ], 71 | [ 72 | "follower-livedata", 73 | "1.0.1" 74 | ], 75 | [ 76 | "geojson-utils", 77 | "1.0.0" 78 | ], 79 | [ 80 | "hellogerard:reststop2", 81 | "0.5.9" 82 | ], 83 | [ 84 | "html-tools", 85 | "1.0.1" 86 | ], 87 | [ 88 | "htmljs", 89 | "1.0.1" 90 | ], 91 | [ 92 | "id-map", 93 | "1.0.0" 94 | ], 95 | [ 96 | "jquery", 97 | "1.0.0" 98 | ], 99 | [ 100 | "json", 101 | "1.0.0" 102 | ], 103 | [ 104 | "less", 105 | "1.0.9" 106 | ], 107 | [ 108 | "localstorage", 109 | "1.0.0" 110 | ], 111 | [ 112 | "logging", 113 | "1.0.3" 114 | ], 115 | [ 116 | "meteor", 117 | "1.1.1" 118 | ], 119 | [ 120 | "minifiers", 121 | "1.1.0" 122 | ], 123 | [ 124 | "minimongo", 125 | "1.0.3" 126 | ], 127 | [ 128 | "mongo", 129 | "1.0.6" 130 | ], 131 | [ 132 | "mrt:minimongoid", 133 | "0.8.8" 134 | ], 135 | [ 136 | "mrt:underscore-string-latest", 137 | "2.3.3" 138 | ], 139 | [ 140 | "natestrauser:parsleyjs", 141 | "1.1.7" 142 | ], 143 | [ 144 | "npm-bcrypt", 145 | "0.7.7" 146 | ], 147 | [ 148 | "observe-sequence", 149 | "1.0.2" 150 | ], 151 | [ 152 | "ordered-dict", 153 | "1.0.0" 154 | ], 155 | [ 156 | "random", 157 | "1.0.0" 158 | ], 159 | [ 160 | "reactive-var", 161 | "1.0.2" 162 | ], 163 | [ 164 | "retry", 165 | "1.0.0" 166 | ], 167 | [ 168 | "routepolicy", 169 | "1.0.1" 170 | ], 171 | [ 172 | "service-configuration", 173 | "1.0.1" 174 | ], 175 | [ 176 | "sha", 177 | "1.0.0" 178 | ], 179 | [ 180 | "spacebars", 181 | "1.0.2" 182 | ], 183 | [ 184 | "spacebars-compiler", 185 | "1.0.2" 186 | ], 187 | [ 188 | "srp", 189 | "1.0.0" 190 | ], 191 | [ 192 | "templating", 193 | "1.0.7" 194 | ], 195 | [ 196 | "tracker", 197 | "1.0.2" 198 | ], 199 | [ 200 | "ui", 201 | "1.0.3" 202 | ], 203 | [ 204 | "underscore", 205 | "1.0.0" 206 | ], 207 | [ 208 | "webapp", 209 | "1.1.2" 210 | ], 211 | [ 212 | "webapp-hashing", 213 | "1.0.0" 214 | ] 215 | ], 216 | "pluginDependencies": [], 217 | "toolVersion": "meteor-tool@1.0.33", 218 | "format": "1.0" 219 | } --------------------------------------------------------------------------------