diff --git a/app/controllers/controllers.rb b/app/controllers/controllers.rb new file mode 100644 index 0000000..d11f590 --- /dev/null +++ b/app/controllers/controllers.rb @@ -0,0 +1,2 @@ +load File.expand_path('../discourse_donations/charges_controller.rb', __FILE__) +load File.expand_path('../discourse_donations/checkout_controller.rb', __FILE__) diff --git a/app/controllers/discourse_donations/charges_controller.rb b/app/controllers/discourse_donations/charges_controller.rb index 7563474..017be3f 100644 --- a/app/controllers/discourse_donations/charges_controller.rb +++ b/app/controllers/discourse_donations/charges_controller.rb @@ -1,10 +1,24 @@ -require_dependency 'discourse' - module DiscourseDonations - class ChargesController < ApplicationController - + class ChargesController < ::ApplicationController skip_before_action :verify_authenticity_token, only: [:create] - skip_before_action :check_xhr + + before_action :ensure_logged_in, only: [:cancel_subscription] + before_action :set_user, only: [:index, :create] + before_action :set_email, only: [:index, :create, :cancel_subscription] + + def index + result = {} + + if current_user + stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options) + + list_result = stripe.list(current_user, email: current_user.email) + + result = list_result if list_result.present? + end + + render json: success_json.merge(result) + end def create Rails.logger.info user_params.inspect @@ -12,7 +26,7 @@ module DiscourseDonations output = { 'messages' => [], 'rewards' => [] } if create_account - if !email.present? || !user_params[:username].present? + if !@email.present? || !user_params[:username].present? output['messages'] << I18n.t('login.missing_user_field') end if user_params[:password] && user_params[:password].length > User.max_password_length @@ -28,19 +42,36 @@ module DiscourseDonations end Rails.logger.debug "Creating a Stripe payment" - payment = DiscourseDonations::Stripe.new(secret_key, stripe_options) + stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options) + result = {} begin Rails.logger.debug "Creating a Stripe charge for #{user_params[:amount]}" - charge_params = [user_params[:stripeToken], user_params[:amount]] + opts = { + email: @email, + token: user_params[:stripeToken], + amount: user_params[:amount] + } - if user - charge_params.unshift(user, user.email) + if user_params[:type] === 'once' + result[:charge] = stripe.charge(@user, opts) else - charge_params.unshift(nil, email) + opts[:type] = user_params[:type] + + subscription = stripe.subscribe(@user, opts) + + if subscription && subscription['id'] + invoices = stripe.invoices_for_subscription(@user, + email: opts[:email], + subscription_id: subscription['id'] + ) + end + + result[:subscription] = {} + result[:subscription][:subscription] = subscription if subscription + result[:subscription][:invoices] = invoices if invoices end - charge = payment.charge(*charge_params) rescue ::Stripe::CardError => e err = e.json_body[:error] @@ -52,13 +83,24 @@ module DiscourseDonations render(json: output) && (return) end - if charge['paid'] == true - output['messages'] << I18n.l(Time.now(), format: :long) + ': ' + I18n.t('donations.payment.success') + if (result[:charge] && result[:charge]['paid'] == true) || + (result[:subscription] && result[:subscription][:subscription] && + result[:subscription][:subscription]['status'] === 'active') + + output['messages'] << I18n.t('donations.payment.success') + + if (result[:charge] && result[:charge]['receipt_number']) || + (result[:subscription] && result[:subscription][:invoices].first['receipt_number']) + output['messages'] << " #{I18n.t('donations.payment.receipt_sent', email: @email)}" + end + + output['charge'] = result[:charge] if result[:charge] + output['subscription'] = result[:subscription] if result[:subscription] output['rewards'] << { type: :group, name: group_name } if group_name output['rewards'] << { type: :badge, name: badge_name } if badge_name - if create_account && email.present? + if create_account && @email.present? args = user_params.to_h.slice(:email, :username, :password, :name).merge(rewards: output['rewards']) Jobs.enqueue(:donation_user, args) end @@ -67,6 +109,20 @@ module DiscourseDonations render json: output end + def cancel_subscription + params.require(:subscription_id) + + stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options) + + result = stripe.cancel_subscription(params[:subscription_id]) + + if result[:success] + render json: success_json.merge(subscription: result[:subscription]) + else + render json: failed_json.merge(message: result[:message]) + end + end + private def create_account @@ -97,19 +153,31 @@ module DiscourseDonations end def user_params - params.permit(:user_id, :name, :username, :email, :password, :stripeToken, :amount, :create_account) + params.permit(:user_id, :name, :username, :email, :password, :stripeToken, :type, :amount, :create_account) end - def email - user_params[:email] || user.try(:email) - end + def set_user + user = current_user - def user - if user_params[:user_id] - User.find(user_params[:user_id]) - else - current_user + if user_params[:user_id].present? + if record = User.find_by(user_params[:user_id]) + user = record + end end + + @user = user + end + + def set_email + email = nil + + if user_params[:email].present? + email = user_params[:email] + elsif @user + email = @user.try(:email) + end + + @email = email end end end diff --git a/app/controllers/discourse_donations/payments_controller.rb b/app/controllers/discourse_donations/payments_controller.rb deleted file mode 100644 index e4a6c27..0000000 --- a/app/controllers/discourse_donations/payments_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -module DiscourseDonations - class PaymentsController < ApplicationController - def index - render json: {} - end - - def show - render json: {} - end - end -end diff --git a/app/helpers/discourse_donations/application_helper.rb b/app/helpers/discourse_donations/application_helper.rb deleted file mode 100644 index 9009142..0000000 --- a/app/helpers/discourse_donations/application_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ - -module DiscourseDonations - module ApplicationHelper - end -end diff --git a/app/services/discourse_donations/stripe.rb b/app/services/discourse_donations/stripe.rb index 936fcb7..cf7288e 100644 --- a/app/services/discourse_donations/stripe.rb +++ b/app/services/discourse_donations/stripe.rb @@ -9,54 +9,242 @@ module DiscourseDonations end def checkoutCharge(user = nil, email, token, amount) - customer = customer(user, email, token) + customer = customer(user, + email: email, + source: token, + create: true + ) + + return if !customer + charge = ::Stripe::Charge.create( customer: customer.id, amount: amount, description: @description, currency: @currency ) + charge end - def charge(user = nil, email, token, amount) - customer = customer(user, email, token) + def charge(user = nil, opts) + customer = customer(user, + email: opts[:email], + source: opts[:token], + create: true + ) + + return if !customer + @charge = ::Stripe::Charge.create( customer: customer.id, - amount: amount, - description: description, - currency: currency + amount: opts[:amount], + description: @description, + currency: @currency, + receipt_email: customer.email ) + @charge end - def subscribe(user = nil, email, opts) - customer = customer(user, email, opts[:stripeToken]) - @subscription = ::Stripe::Subscription.create( - customer: customer.id, - plan: opts[:plan] + def subscribe(user = nil, opts) + customer = customer(user, + email: opts[:email], + source: opts[:token], + create: true + ) + + return if !customer + + type = opts[:type] + amount = opts[:amount] + + plans = ::Stripe::Plan.list + plan_id = create_plan_id(type, amount) + + unless plans.data && plans.data.any? { |p| p['id'] === plan_id } + result = create_plan(type, amount) + + plan_id = result['id'] + end + + ::Stripe::Subscription.create( + customer: customer.id, + items: [{ + plan: plan_id + }] ) - @subscription end - def customer(user, email, source) - if user && user.stripe_customer_id - ::Stripe::Customer.retrieve(user.stripe_customer_id) - else - customer = ::Stripe::Customer.create( - email: email, - source: source + def list(user, opts = {}) + customer = customer(user, opts) + + return if !customer + + result = { customer: customer } + + raw_invoices = ::Stripe::Invoice.list(customer: customer.id) + raw_invoices = raw_invoices.is_a?(Object) ? raw_invoices['data'] : [] + + raw_charges = ::Stripe::Charge.list(customer: customer.id) + raw_charges = raw_charges.is_a?(Object) ? raw_charges['data'] : [] + + if raw_invoices.any? + raw_subscriptions = ::Stripe::Subscription.list(customer: customer.id, status: 'all') + raw_subscriptions = raw_subscriptions.is_a?(Object) ? raw_subscriptions['data'] : [] + + if raw_subscriptions.any? + subscriptions = [] + + raw_subscriptions.each do |subscription| + invoices = raw_invoices.select do |invoice| + invoice['subscription'] === subscription['id'] + end + + subscriptions.push( + subscription: subscription, + invoices: invoices + ) + end + + result[:subscriptions] = subscriptions + end + + ## filter out any charges related to subscriptions + raw_invoice_ids = raw_invoices.map { |i| i['id'] } + raw_charges = raw_charges.select { |c| raw_invoice_ids.exclude?(c['invoice']) } + end + + if raw_charges.any? + result[:charges] = raw_charges + end + + result + end + + def invoices_for_subscription(user, opts) + customer = customer(user, + email: opts[:email] + ) + + invoices = [] + + if customer + result = ::Stripe::Invoice.list( + customer: customer.id, + subscription: opts[:subscription_id] ) + + invoices = result['data'] if result['data'] + end + + invoices + end + + def cancel_subscription(subscription_id) + if subscription = ::Stripe::Subscription.retrieve(subscription_id) + result = subscription.delete + + if result['status'] === 'canceled' + { success: true, subscription: subscription } + else + { success: false, message: I18n.t('donations.subscription.error.not_cancelled') } + end + else + { success: false, message: I18n.t('donations.subscription.error.not_found') } + end + end + + def customer(user, opts = {}) + customer = nil + + if user && user.stripe_customer_id + begin + customer = ::Stripe::Customer.retrieve(user.stripe_customer_id) + rescue ::Stripe::StripeError => e + user.custom_fields['stripe_customer_id'] = nil + user.save_custom_fields(true) + customer = nil + end + end + + if !customer && opts[:email] + begin + customers = ::Stripe::Customer.list(email: opts[:email]) + + if customers && customers['data'] + customer = customers['data'].first if customers['data'].any? + end + + if customer && user + user.custom_fields['stripe_customer_id'] = customer.id + user.save_custom_fields(true) + end + rescue ::Stripe::StripeError => e + customer = nil + end + end + + if !customer && opts[:create] + customer = ::Stripe::Customer.create( + email: opts[:email], + source: opts[:source] + ) + if user user.custom_fields['stripe_customer_id'] = customer.id user.save_custom_fields(true) end - customer end + + customer end def successful? @charge[:paid] end + + def create_plan(type, amount) + id = create_plan_id(type, amount) + nickname = id.gsub(/_/, ' ').titleize + + products = ::Stripe::Product.list(type: 'service') + + if products['data'] && products['data'].any? { |p| p['id'] === product_id } + product = product_id + else + result = create_product + product = result['id'] + end + + ::Stripe::Plan.create( + id: id, + nickname: nickname, + interval: type, + currency: @currency, + product: product, + amount: amount.to_i + ) + end + + def create_product + ::Stripe::Product.create( + id: product_id, + name: product_name, + type: 'service' + ) + end + + def product_id + @product_id ||= "#{SiteSetting.title}_recurring_donation".freeze + end + + def product_name + @product_name ||= I18n.t('donations.recurring', site_title: SiteSetting.title) + end + + def create_plan_id(type, amount) + "discourse_donation_recurring_#{type}_#{amount}".freeze + end end end diff --git a/app/services/services.rb b/app/services/services.rb new file mode 100644 index 0000000..8e8aabc --- /dev/null +++ b/app/services/services.rb @@ -0,0 +1,2 @@ +load File.expand_path('../discourse_donations/rewards.rb', __FILE__) +load File.expand_path('../discourse_donations/stripe.rb', __FILE__) diff --git a/assets/javascripts/discourse/components/donation-list.js.es6 b/assets/javascripts/discourse/components/donation-list.js.es6 new file mode 100644 index 0000000..3b9b4da --- /dev/null +++ b/assets/javascripts/discourse/components/donation-list.js.es6 @@ -0,0 +1,8 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Component.extend({ + classNames: 'donation-list', + hasSubscriptions: Ember.computed.notEmpty('subscriptions'), + hasCharges: Ember.computed.notEmpty('charges') +}) diff --git a/assets/javascripts/discourse/components/donation-row.js.es6 b/assets/javascripts/discourse/components/donation-row.js.es6 new file mode 100644 index 0000000..56533f5 --- /dev/null +++ b/assets/javascripts/discourse/components/donation-row.js.es6 @@ -0,0 +1,96 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { formatAnchor, formatAmount } from '../lib/donation-utilities'; +import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; +import showModal from "discourse/lib/show-modal"; + +export default Ember.Component.extend({ + classNameBindings: [':donation-row', 'canceled', 'updating'], + includePrefix: Ember.computed.or('invoice', 'charge'), + canceled: Ember.computed.equal('subscription.status', 'canceled'), + + @computed('subscription', 'invoice', 'charge', 'customer') + data(subscription, invoice, charge, customer) { + if (subscription) { + return $.extend({}, subscription.plan, { + anchor: subscription.billing_cycle_anchor + }); + } else if (invoice) { + let receiptSent = false; + + if (invoice.receipt_number && customer.email) { + receiptSent = true; + } + + return $.extend({}, invoice.lines.data[0], { + anchor: invoice.date, + invoiceLink: invoice.invoice_pdf, + receiptSent + }); + } else if (charge) { + let receiptSent = false; + + if (charge.receipt_number && charge.receipt_email) { + receiptSent = true; + } + + return $.extend({}, charge, { + anchor: charge.created, + receiptSent + }); + } + }, + + @computed('data.currency') + currency(currency) { + return currency ? currency.toUpperCase() : null; + }, + + @computed('data.amount', 'currency') + amount(amount, currency) { + return formatAmount(amount, currency); + }, + + @computed('data.interval') + interval(interval) { + return interval || 'once'; + }, + + @computed('data.anchor', 'interval') + period(anchor, interval) { + return I18n.t(`discourse_donations.period.${interval}`, { + anchor: formatAnchor(interval, moment.unix(anchor)) + }) + }, + + cancelSubscription() { + const subscriptionId = this.get('subscription.id'); + this.set('updating', true); + + ajax('/donate/charges/cancel-subscription', { + data: { + subscription_id: subscriptionId + }, + method: 'put' + }).then(result => { + if (result.success) { + this.set('subscription', result.subscription); + } + }).catch(popupAjaxError).finally(() => { + this.set('updating', false); + }); + }, + + actions: { + cancelSubscription() { + showModal('cancel-subscription', { + model: { + currency: this.get('currency'), + amount: this.get('amount'), + period: this.get('period'), + confirm: () => this.cancelSubscription() + } + }); + } + } +}) diff --git a/assets/javascripts/discourse/components/stripe-card.js.es6 b/assets/javascripts/discourse/components/stripe-card.js.es6 index ee83b52..2708ca1 100644 --- a/assets/javascripts/discourse/components/stripe-card.js.es6 +++ b/assets/javascripts/discourse/components/stripe-card.js.es6 @@ -1,21 +1,47 @@ import { ajax } from 'discourse/lib/ajax'; -import { getRegister } from 'discourse-common/lib/get-owner'; +import { formatAnchor, zeroDecimalCurrencies } from '../lib/donation-utilities'; import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { emailValid } from "discourse/lib/utilities"; export default Ember.Component.extend({ result: [], - amount: 1, stripe: null, transactionInProgress: null, settings: null, showTransactionFeeDescription: false, + includeTransactionFee: true, init() { this._super(); - this.set('anon', (!Discourse.User.current())); - this.set('settings', getRegister(this).lookup('site-settings:main')); - this.set('create_accounts', this.get('anon') && this.get('settings').discourse_donations_enable_create_accounts); - this.set('stripe', Stripe(this.get('settings').discourse_donations_public_key)); + const user = this.get('currentUser'); + const settings = Discourse.SiteSettings; + + this.set('create_accounts', !user && settings.discourse_donations_enable_create_accounts); + this.set('stripe', Stripe(settings.discourse_donations_public_key)); + + const types = settings.discourse_donations_types.split('|') || []; + const amounts = this.get('donateAmounts'); + + this.setProperties({ + types, + type: types[0], + amount: amounts[0].value + }); + }, + + @computed('types') + donationTypes(types) { + return types.map((type) => { + return { + id: type, + name: I18n.t(`discourse_donations.types.${type}`) + }; + }); + }, + + @computed('type') + period(type) { + return I18n.t(`discourse_donations.period.${type}`, { anchor: formatAnchor(type) }); }, @computed @@ -36,9 +62,23 @@ export default Ember.Component.extend({ @computed('stripe') card(stripe) { let elements = stripe.elements(); - return elements.create('card', { - hidePostalCode: !this.get('settings').discourse_donations_zip_code + let card = elements.create('card', { + hidePostalCode: !Discourse.SiteSettings.discourse_donations_zip_code }); + + card.addEventListener('change', (event) => { + if (event.error) { + this.set('stripeError', event.error.message); + } else { + this.set('stripeError', ''); + } + + if (event.elementType === 'card' && event.complete) { + this.set('stripeReady', true); + } + }); + + return card; }, @computed('amount') @@ -55,9 +95,48 @@ export default Ember.Component.extend({ return amount; }, + @computed('email') + emailValid(email) { + return emailValid(email); + }, + + @computed('email', 'emailValid') + showEmailError(email, emailValid) { + return email && email.length > 3 && !emailValid; + }, + + @computed('currentUser', 'emailValid') + userReady(currentUser, emailValid) { + return currentUser || emailValid; + }, + + @computed('userReady', 'stripeReady') + formIncomplete(userReady, stripeReady) { + return !userReady || !stripeReady; + }, + + @computed('transactionInProgress', 'formIncomplete') + disableSubmit(transactionInProgress, formIncomplete) { + return transactionInProgress || formIncomplete; + }, + didInsertElement() { this._super(); this.get('card').mount('#card-element'); + Ember.$(document).on('click', Ember.run.bind(this, this.documentClick)); + }, + + willDestroyElement() { + Ember.$(document).off('click', Ember.run.bind(this, this.documentClick)); + }, + + documentClick(e) { + let $element = this.$('.transaction-fee-description'); + let $target = $(e.target); + if ($target.closest($element).length < 1 && + this._state !== 'destroying') { + this.set('showTransactionFeeDescription', false); + } }, setSuccess() { @@ -79,27 +158,57 @@ export default Ember.Component.extend({ submitStripeCard() { let self = this; - self.set('transactionInProgress', true); + this.set('transactionInProgress', true); + this.get('stripe').createToken(this.get('card')).then(data => { self.set('result', []); if (data.error) { - self.set('result', data.error.message); + this.setProperties({ + stripeError: data.error.message, + stripeReady: false + }); self.endTranscation(); } else { - const transactionFeeEnabled = Discourse.SiteSettings.discourse_donations_enable_transaction_fee; - const amount = transactionFeeEnabled ? this.get('totalAmount') : this.get('amount'); + const settings = Discourse.SiteSettings; + + const transactionFeeEnabled = settings.discourse_donations_enable_transaction_fee; + let amount = transactionFeeEnabled ? this.get('totalAmount') : this.get('amount'); + + if (zeroDecimalCurrencies.indexOf(settings.discourse_donations_currency) === -1) { + amount = amount * 100; + } + let params = { stripeToken: data.token.id, - amount: amount * 100, + type: self.get('type'), + amount, email: self.get('email'), username: self.get('username'), create_account: self.get('create_accounts') }; if(!self.get('paymentSuccess')) { - ajax('/charges', { data: params, method: 'post' }).then(d => { - self.concatMessages(d.messages); + ajax('/donate/charges', { + data: params, + method: 'post' + }).then(result => { + if (result.subscription) { + let subscription = $.extend({}, result.subscription, { + new: true + }); + this.get('subscriptions').unshiftObject(subscription); + } + + if (result.charge) { + let charge = $.extend({}, result.charge, { + new: true + }); + this.get('charges').unshiftObject(charge); + } + + self.concatMessages(result.messages); + self.endTranscation(); }); } diff --git a/assets/javascripts/discourse/controllers/cancel-subscription.js.es6 b/assets/javascripts/discourse/controllers/cancel-subscription.js.es6 new file mode 100644 index 0000000..57a6565 --- /dev/null +++ b/assets/javascripts/discourse/controllers/cancel-subscription.js.es6 @@ -0,0 +1,12 @@ +export default Ember.Controller.extend({ + actions: { + confirm() { + this.get('model.confirm')(); + this.send('closeModal'); + }, + + cancel() { + this.send('closeModal'); + } + } +}) diff --git a/assets/javascripts/discourse/controllers/donate.js.es6 b/assets/javascripts/discourse/controllers/donate.js.es6 new file mode 100644 index 0000000..2073af0 --- /dev/null +++ b/assets/javascripts/discourse/controllers/donate.js.es6 @@ -0,0 +1,53 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { ajax } from 'discourse/lib/ajax'; +import { getOwner } from 'discourse-common/lib/get-owner'; + +export default Ember.Controller.extend({ + loadingDonations: false, + loadDonationsDisabled: Ember.computed.not('emailVaild'), + + @computed('charges.[]', 'subscriptions.[]') + hasDonations(charges, subscriptions) { + return (charges && charges.length > 0) || + (subscriptions && subscriptions.length > 0); + }, + + @computed('email') + emailVaild(email) { + return emailValid(email); + }, + + actions: { + loadDonations() { + let email = this.get('email'); + + this.set('loadingDonations', true); + + ajax('/donate/charges', { + data: { email }, + type: 'GET' + }).then((result) => { + this.setProperties({ + charges: Ember.A(result.charges), + subscriptions: Ember.A(result.subscriptions), + customer: result.customer + }); + }).catch(popupAjaxError).finally(() => { + this.setProperties({ + loadingDonations: false, + hasEmailResult: true + }); + + Ember.run.later(() => { + this.set('hasEmailResult', false); + }, 6000) + }) + }, + + showLogin() { + const controller = getOwner(this).lookup('route:application'); + controller.send('showLogin'); + } + } +}) diff --git a/assets/javascripts/discourse/lib/donation-utilities.js.es6 b/assets/javascripts/discourse/lib/donation-utilities.js.es6 new file mode 100644 index 0000000..6f82bf2 --- /dev/null +++ b/assets/javascripts/discourse/lib/donation-utilities.js.es6 @@ -0,0 +1,31 @@ +const formatAnchor = function(type = null, time = moment()) { + let format; + + switch(type) { + case 'once': + format = 'Do MMMM YYYY'; + break; + case 'week': + format = 'dddd'; + break; + case 'month': + format = 'Do'; + break; + case 'year': + format = 'MMMM D'; + break; + default: + format = 'dddd'; + } + + return moment(time).format(format); +} + +const zeroDecimalCurrencies = ['MGA', 'BIF', 'CLP', 'PYG', 'DFJ', 'RWF', 'GNF', 'UGX', 'JPY', 'VND', 'VUV', 'XAF', 'KMF', 'KRW', 'XOF', 'XPF']; + +const formatAmount = function(amount, currency) { + let zeroDecimal = zeroDecimalCurrencies.indexOf(currency) > -1; + return zeroDecimal ? amount : (amount / 100).toFixed(2); +} + +export { formatAnchor, formatAmount, zeroDecimalCurrencies } diff --git a/assets/javascripts/discourse/routes/donate.js.es6 b/assets/javascripts/discourse/routes/donate.js.es6 new file mode 100644 index 0000000..04d7994 --- /dev/null +++ b/assets/javascripts/discourse/routes/donate.js.es6 @@ -0,0 +1,29 @@ +import DiscourseRoute from "discourse/routes/discourse"; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { ajax } from 'discourse/lib/ajax'; + +export default DiscourseRoute.extend({ + setupController(controller) { + let charges = []; + let subscriptions = []; + let customer = {}; + + controller.set('loadingDonations', true); + + ajax('/donate/charges').then((result) => { + if (result) { + charges = result.charges; + subscriptions = result.subscriptions; + customer = result.customer; + } + + controller.setProperties({ + charges: Ember.A(charges), + subscriptions: Ember.A(subscriptions), + customer + }); + }).catch(popupAjaxError).finally(() => { + controller.set('loadingDonations', false); + }) + } +}); diff --git a/assets/javascripts/discourse/templates/components/donation-list.hbs b/assets/javascripts/discourse/templates/components/donation-list.hbs new file mode 100644 index 0000000..4a23964 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/donation-list.hbs @@ -0,0 +1,28 @@ +{{#if hasSubscriptions}} +