From 5723b91ba2724c44bc5856433203cdee5174d08b Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 21 Jun 2018 19:00:19 +1000 Subject: [PATCH 1/7] Donation form style updates --- assets/stylesheets/discourse-donations.scss | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/assets/stylesheets/discourse-donations.scss b/assets/stylesheets/discourse-donations.scss index dcbe614..50f5927 100644 --- a/assets/stylesheets/discourse-donations.scss +++ b/assets/stylesheets/discourse-donations.scss @@ -14,6 +14,20 @@ div.stripe-errors { .donations-page-payment { padding-top: 60px; + + #payment-form { + .control-label { + margin: 0 6.5px; + } + + .select-kit ul { + margin: 0; + } + + input[type="checkbox"] { + margin: 0; + } + } } .transaction-fee-description { From d55d149d7de8e3f790311d22afcd9f9e7baacbc8 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 21 Jun 2018 19:00:43 +1000 Subject: [PATCH 2/7] Add recurring donations --- .../discourse_donations/charges_controller.rb | 43 ++++++++---- app/services/discourse_donations/stripe.rb | 70 ++++++++++++++++--- .../discourse/components/stripe-card.js.es6 | 34 +++++++++ .../templates/components/stripe-card.hbs | 12 +++- config/locales/client.en.yml | 14 +++- config/settings.yml | 9 +++ plugin.rb | 2 +- 7 files changed, 158 insertions(+), 26 deletions(-) diff --git a/app/controllers/discourse_donations/charges_controller.rb b/app/controllers/discourse_donations/charges_controller.rb index 7563474..c712d19 100644 --- a/app/controllers/discourse_donations/charges_controller.rb +++ b/app/controllers/discourse_donations/charges_controller.rb @@ -1,10 +1,11 @@ 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 :set_user_and_email, only: [:create] def create Rails.logger.info user_params.inspect @@ -12,7 +13,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 @@ -32,15 +33,19 @@ module DiscourseDonations 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' + charge = payment.charge(@user, opts) else - charge_params.unshift(nil, email) + opts[:type] = user_params[:type] + charge = payment.subscribe(@user, opts) end - charge = payment.charge(*charge_params) rescue ::Stripe::CardError => e err = e.json_body[:error] @@ -58,7 +63,7 @@ module DiscourseDonations 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 @@ -97,19 +102,27 @@ 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 user - if user_params[:user_id] - User.find(user_params[:user_id]) - else - current_user + def set_user_and_email + user = current_user + + if user_params[:user_id].present? + user = User.find(user_params[:user_id]) end + + if user_params[:email].present? + email = user_params[:email] + else + email = user.try(:email) + end + + @user = user + @email = email end end end diff --git a/app/services/discourse_donations/stripe.rb b/app/services/discourse_donations/stripe.rb index 936fcb7..893d4a2 100644 --- a/app/services/discourse_donations/stripe.rb +++ b/app/services/discourse_donations/stripe.rb @@ -1,3 +1,5 @@ +RECURRING_DONATION_PRODUCT_ID = 'discourse_donation_recurring' + module DiscourseDonations class Stripe attr_reader :charge, :currency, :description @@ -10,32 +12,47 @@ module DiscourseDonations def checkoutCharge(user = nil, email, token, amount) customer = customer(user, email, token) + 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, opts[:email], opts[:token]) + @charge = ::Stripe::Charge.create( customer: customer.id, - amount: amount, - description: description, - currency: currency + amount: opts[:amount], + description: @description, + currency: @currency ) + @charge end - def subscribe(user = nil, email, opts) - customer = customer(user, email, opts[:stripeToken]) + def subscribe(user = nil, opts) + customer = customer(user, opts[:email], opts[:token]) + + plans = ::Stripe::Plan.list + type = opts[:type] + plan_id = create_plan_id(type) + + unless plans.data && plans.data.any? { |p| p['id'] === plan_id } + result = create_plan(type, opts[:amount]) + plan_id = result['id'] + end + @subscription = ::Stripe::Subscription.create( customer: customer.id, - plan: opts[:plan] + items: [{ plan: plan_id }] ) + @subscription end @@ -47,10 +64,12 @@ module DiscourseDonations email: email, source: source ) + if user user.custom_fields['stripe_customer_id'] = customer.id user.save_custom_fields(true) end + customer end end @@ -58,5 +77,40 @@ module DiscourseDonations def successful? @charge[:paid] end + + def create_plan(type, amount) + id = create_plan_id(type) + nickname = id.gsub(/_/, ' ').titleize + + products = ::Stripe::Product.list(type: 'service') + + if products['data'] && products['data'].any? { |p| p['id'] === RECURRING_DONATION_PRODUCT_ID } + product = RECURRING_DONATION_PRODUCT_ID + else + result = create_product + product = result['id'] + end + + ::Stripe::Plan.create( + id: id, + nickname: nickname, + interval: type.tr('ly', ''), + currency: @currency, + product: product, + amount: amount.to_i + ) + end + + def create_product + ::Stripe::Product.create( + id: RECURRING_DONATION_PRODUCT_ID, + name: "Discourse Donation Recurring", + type: 'service' + ) + end + + def create_plan_id(type) + "discourse_donation_recurring_#{type}" + end end end diff --git a/assets/javascripts/discourse/components/stripe-card.js.es6 b/assets/javascripts/discourse/components/stripe-card.js.es6 index ee83b52..add4db7 100644 --- a/assets/javascripts/discourse/components/stripe-card.js.es6 +++ b/assets/javascripts/discourse/components/stripe-card.js.es6 @@ -16,6 +16,39 @@ export default Ember.Component.extend({ 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 types = Discourse.SiteSettings.discourse_donations_types.split('|') || []; + this.set('types', types); + this.set('type', types[0]); + }, + + @computed('types') + donationTypes(types) { + return types.map((type) => { + return { + id: type, + name: I18n.t(`discourse_donations.types.${type}`) + } + }) + }, + + @computed('type') + period(type) { + let anchor; + + if (type === 'weekly') { + anchor = moment().format('dddd'); + } + + if (type === 'monthly') { + anchor = moment().format('Do'); + } + + if (type === 'yearly') { + anchor = moment().format('MMMM D'); + } + + return I18n.t(`discourse_donations.period.${type}`, { anchor }); }, @computed @@ -91,6 +124,7 @@ export default Ember.Component.extend({ const amount = transactionFeeEnabled ? this.get('totalAmount') : this.get('amount'); let params = { stripeToken: data.token.id, + type: self.get('type'), amount: amount * 100, email: self.get('email'), username: self.get('username'), diff --git a/assets/javascripts/discourse/templates/components/stripe-card.hbs b/assets/javascripts/discourse/templates/components/stripe-card.hbs index d3fd676..7b3fc3b 100644 --- a/assets/javascripts/discourse/templates/components/stripe-card.hbs +++ b/assets/javascripts/discourse/templates/components/stripe-card.hbs @@ -1,5 +1,14 @@
+
+ +
+ {{combo-box content=donationTypes value=type}} +
+
+
diff --git a/assets/javascripts/discourse/templates/components/donation-row.hbs b/assets/javascripts/discourse/templates/components/donation-row.hbs new file mode 100644 index 0000000..d37abb4 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/donation-row.hbs @@ -0,0 +1,39 @@ +{{#if includePrefix}} + {{i18n 'discourse_donations.invoice_prefix'}} +{{/if}} + +{{currency}} + +{{amount}} + +{{period}} + +{{#if invoice}} + ({{i18n 'discourse_donations.invoice'}}) +{{/if}} + +{{#if currentUser}} + {{#if subscription}} + {{#if updating}} + {{loading-spinner size='small'}} + {{else}} + {{#unless canceled}} + + {{i18n 'cancel'}} + + {{/unless}} + {{/if}} + {{/if}} +{{/if}} + +{{#if receiptSent}} + + {{i18n 'discourse_donations.receipt' email=customer.email}} +{{/if}} + +{{#if new}} + + {{d-icon 'circle'}} + {{i18n 'new_item'}} + +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/stripe-card.hbs b/assets/javascripts/discourse/templates/components/stripe-card.hbs index a23c7fc..154a746 100644 --- a/assets/javascripts/discourse/templates/components/stripe-card.hbs +++ b/assets/javascripts/discourse/templates/components/stripe-card.hbs @@ -33,6 +33,7 @@ +
-{{#if currentUser}} -
-

{{i18n 'discourse_donations.donations.title'}}

- {{#if loadingDonations}} - {{loading-spinner size='small'}} - {{else}} +
+

{{i18n 'discourse_donations.donations.title'}}

+ {{#if loadingDonations}} + {{i18n 'discourse_donations.donations.loading'}} + {{loading-spinner size='small'}} + {{else}} + {{#if currentUser}} {{#if hasDonations}} - {{donation-list charges=charges subscriptions=subscriptions}} + {{donation-list charges=charges subscriptions=subscriptions customer=customer}} {{else}} {{i18n 'discourse_donations.donations.none'}} {{/if}} + {{else}} + {{#if hasDonations}} + {{donation-list charges=charges subscriptions=subscriptions customer=customer}} + {{else}} + {{#if hasEmailResult}} + {{i18n 'discourse_donations.donations.none_email' email=email}} + {{else}} + {{input value=email placeholder=(i18n 'email')}} + {{d-button action='loadDonations' label='discourse_donations.donations.load' disabled=loadDonationsDisabled}} + {{/if}} + {{/if}} {{/if}} -
-{{/if}} + {{/if}} +
diff --git a/assets/javascripts/discourse/templates/modal/cancel-subscription.hbs b/assets/javascripts/discourse/templates/modal/cancel-subscription.hbs new file mode 100644 index 0000000..b67cb78 --- /dev/null +++ b/assets/javascripts/discourse/templates/modal/cancel-subscription.hbs @@ -0,0 +1,11 @@ +{{#d-modal-body title='discourse_donations.subscription.cancel.title'}} + {{i18n 'discourse_donations.subscription.cancel.description' site=siteSettings.title + currency=model.currency + amount=model.amount + period=model.period}} +{{/d-modal-body}} + + diff --git a/assets/stylesheets/discourse-donations.scss b/assets/stylesheets/discourse-donations.scss index 878b905..a120f28 100644 --- a/assets/stylesheets/discourse-donations.scss +++ b/assets/stylesheets/discourse-donations.scss @@ -61,11 +61,15 @@ div.stripe-errors { .donation-list { .subscription-list, .charge-list { margin-bottom: 10px; - display: inline-block; > ul { margin: 10px 0; list-style: none; + + .spinner { + height: 5px; + width: 5px; + } } } @@ -75,3 +79,32 @@ div.stripe-errors { } } } + +.donation-row { + span { + line-height: 25px; + } + + &.canceled { + text-decoration: line-through; + } + + &.updating { + color: $primary-low; + } + + .new-flag { + color: $tertiary; + margin-left: 5px; + + .fa { + line-height: 16px; + font-size: 8px; + } + + > * { + display: inline-block; + vertical-align: middle; + } + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 657b3de..69076a5 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -27,11 +27,17 @@ en: title: "Make a Donation" amount: Amount card: Card - submit: Make Payment + submit: Donate submit_with_create_account: Make Payment and Create Account - invoice: "Invoice" + invoice: "invoice" invoice_prefix: "You gave" receipt: "Receipt sent to {{email}}." + subscription: + cancel: + title: "Cancel Recurring Donation" + description: > + Are you sure you want to cancel your recurring donation to {{site}} + of {{currency}} {{amount}} {{period}}? email_instructions: "Required to send you a receipt. Not used for marketing." transaction_fee: label: "Include transaction fee of {{currency}} {{transactionFee}}" @@ -52,6 +58,9 @@ en: year: "every year on {{anchor}}" donations: title: "Your Donations" + load: "Load Donations" + loading: "Loading donations" charges: "Once Off" subscriptions: "Recurring" none: "You haven't made a donation yet." + none_email: "There are no donations for {{email}}." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 9b59074..c72bc8f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1,7 +1,11 @@ en: donations: - recurring: "%{site_title} recurring donation" + recurring: "%{site_title} Recurring Donation" payment: success: 'Thank you, your donation has been successful.' receipt_sent: 'A receipt has been sent to %{email}.' invoice_sent: 'An invoice has been sent to %{email}.' + subscription: + error: + not_found: "Subscription not found." + not_cancelled: "Subscription not cancelled." diff --git a/config/routes.rb b/config/routes.rb index c17ffdd..b08fab8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,8 @@ DiscourseDonations::Engine.routes.draw do get '/' => 'charges#index' + resources :charges, only: [:index, :create] + put '/charges/cancel-subscription' => 'charges#cancel_subscription' + resources :checkout, only: [:create] end From 0260197e9ee3cd53195d4fdcdcceaa8087a5d348 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 28 Jun 2018 13:46:02 +1000 Subject: [PATCH 7/7] various --- .../javascripts/discourse/components/stripe-card.js.es6 | 8 ++++---- .../discourse/templates/components/stripe-card.hbs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/javascripts/discourse/components/stripe-card.js.es6 b/assets/javascripts/discourse/components/stripe-card.js.es6 index 06d4ad3..2708ca1 100644 --- a/assets/javascripts/discourse/components/stripe-card.js.es6 +++ b/assets/javascripts/discourse/components/stripe-card.js.es6 @@ -1,5 +1,4 @@ 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"; @@ -10,6 +9,7 @@ export default Ember.Component.extend({ transactionInProgress: null, settings: null, showTransactionFeeDescription: false, + includeTransactionFee: true, init() { this._super(); @@ -35,8 +35,8 @@ export default Ember.Component.extend({ return { id: type, name: I18n.t(`discourse_donations.types.${type}`) - } - }) + }; + }); }, @computed('type') @@ -192,7 +192,7 @@ export default Ember.Component.extend({ ajax('/donate/charges', { data: params, method: 'post' - }).then(result => { + }).then(result => { if (result.subscription) { let subscription = $.extend({}, result.subscription, { new: true diff --git a/assets/javascripts/discourse/templates/components/stripe-card.hbs b/assets/javascripts/discourse/templates/components/stripe-card.hbs index 154a746..e8ff719 100644 --- a/assets/javascripts/discourse/templates/components/stripe-card.hbs +++ b/assets/javascripts/discourse/templates/components/stripe-card.hbs @@ -11,7 +11,7 @@
{{combo-box valueAttribute="value" content=donateAmounts value=amount}} @@ -22,7 +22,7 @@
{{input type="checkbox" checked=includeTransactionFee}} - {{i18n 'discourse_donations.transaction_fee.label' transactionFee=transactionFee currency=settings.discourse_donations_currency}} + {{i18n 'discourse_donations.transaction_fee.label' transactionFee=transactionFee currency=siteSettings.discourse_donations_currency}}
{{d-icon 'info-circle'}} {{#if showTransactionFeeDescription}} @@ -39,7 +39,7 @@ {{i18n 'discourse_donations.transaction_fee.total'}}
- {{settings.discourse_donations_currency}} + {{siteSettings.discourse_donations_currency}} {{totalAmount}} {{period}}