Add new plugin files.

This gets the plugin running again without name collisions. A solid starting point
This commit is contained in:
Rimian Perkins
2019-09-11 13:19:07 +10:00
parent 89af44ff78
commit 78914e0511
66 changed files with 51 additions and 3654 deletions
-59
View File
@@ -1,59 +0,0 @@
# Discourse Donations
[![Build Status](https://travis-ci.org/rimian/discourse-donations.svg?branch=master)](https://travis-ci.org/rimian/discourse-donations)
Accept donations from visitors to your [Discourse](https://www.discourse.org/) application. Integrates with [Stripe](https://stripe.com).
## Installation
* Be sure your site is enforcing https.
* Follow the install instructions here: https://meta.discourse.org/t/install-a-plugin/19157
* Add your Stripe public and private keys in settings and set the currency to your local value.
* Enable the plugin and wait for people to donate money.
## Usage
1. Click **Donate**:
![Menu Link](doc/menulink.png)
1. Enter card details:
![Enter card details](doc/pre-payment.png)
1. Click **Make Payment**:
![Enter card details](doc/post-payment.png)
## Creating new user accounts
**This is an experimental feature.** A user can create a new account if they makes a successful donation. Enable this in settings. When a user is not logged in, they will be asked to enter details for a new user account. This feature doesn't support mandatory custom user fields yet.
## Testing
These commands should run:
* ```yarn prettier --list-different 'assets/**/*.scss' '**/*.es6'```
## Tested Credit Card Numbers
These numbers can be used in test mode to simulate a transaction. For more information see the [Stripe docs](https://stripe.com/docs/testing).
Card numbers in **bold** have been tested.
* **4000 0000 0000 0077** Charge succeeds and funds will be added directly to your available balance (bypassing your pending balance).
* **4000 0000 0000 0093** Charge succeeds and domestic pricing is used (other test cards use international pricing). This card is only significant in countries with split pricing.
* **4000 0000 0000 0010** The address_line1_check and address_zip_check verifications fail. If your account is blocking payments that fail postal code validation, the charge is declined.
* **4000 0000 0000 0028** Charge succeeds but the address_line1_check verification fails.
* **4000 0000 0000 0036** The address_zip_check verification fails. If your account is blocking payments that fail postal code validation, the charge is declined.
* **4000 0000 0000 0044** Charge succeeds but the address_zip_check and address_line1_check verifications are both unavailable.
* **4000 0000 0000 0101** If a CVC number is provided, the cvc_check fails. If your account is blocking payments that fail CVC code validation, the charge is declined.
* **4000 0000 0000 0341** Attaching this card to a Customer object succeeds, but attempts to charge the customer fail.
* **4000 0000 0000 9235** Charge succeeds with a risk_level of elevated and placed into review.
* **4000 0000 0000 0002** Charge is declined with a card_declined code.
* **4100 0000 0000 0019** Charge is declined with a card_declined code and a fraudulent reason.
* **4000 0000 0000 0127** Charge is declined with an incorrect_cvc code.
* **4000 0000 0000 0069** Charge is declined with an expired_card code.
* **4000 0000 0000 0119** Charge is declined with a processing_error code.
* **4242 4242 4242 4241** Charge is declined with an incorrect_number code as the card number fails the Luhn check.
## Warranty
This software comes with no warranty of any kind.
-4
View File
@@ -1,4 +0,0 @@
# frozen_string_literal: true
load File.expand_path('../discourse_donations/charges_controller.rb', __FILE__)
load File.expand_path('../discourse_donations/checkout_controller.rb', __FILE__)
@@ -1,190 +0,0 @@
# frozen_string_literal: true
module DiscourseDonations
class ChargesController < ::ApplicationController
skip_before_action :verify_authenticity_token, only: [:create]
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
output = { 'messages' => [], 'rewards' => [] }
if create_account
if [email protected]? || !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
output['messages'] << I18n.t('login.password_too_long')
end
if user_params[:username] && ::User.reserved_username?(user_params[:username])
output['messages'] << I18n.t('login.reserved_username')
end
end
if output['messages'].present?
render(json: output.merge(success: false)) && (return)
end
Rails.logger.debug "Creating a Stripe payment"
stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options)
result = {}
begin
Rails.logger.debug "Creating a Stripe charge for #{user_params[:amount]}"
opts = {
cause: user_params[:cause],
email: @email,
token: user_params[:stripeToken],
amount: user_params[:amount]
}
if user_params[:type] === 'once'
result[:charge] = stripe.charge(@user, opts)
else
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
rescue ::Stripe::CardError => e
err = e.json_body[:error]
output['messages'] << "There was an error (#{err[:type]})."
output['messages'] << "Error code: #{err[:code]}" if err[:code]
output['messages'] << "Decline code: #{err[:decline_code]}" if err[:decline_code]
output['messages'] << "Message: #{err[:message]}" if err[:message]
render(json: output) && (return)
end
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?
args = user_params.to_h.slice(:email, :username, :password, :name).merge(rewards: output['rewards'])
Jobs.enqueue(:donation_user, args)
end
if SiteSetting.discourse_donations_cause_category
Jobs.enqueue(:update_category_donation_statistics)
end
end
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
user_params[:create_account] == 'true' && SiteSetting.discourse_donations_enable_create_accounts
end
def reward?(payment)
payment.present? && payment.successful?
end
def group_name
SiteSetting.discourse_donations_reward_group_name
end
def badge_name
SiteSetting.discourse_donations_reward_badge_name
end
def secret_key
SiteSetting.discourse_donations_secret_key
end
def stripe_options
{
description: SiteSetting.discourse_donations_description,
currency: SiteSetting.discourse_donations_currency
}
end
def user_params
params.permit(:user_id, :name, :username, :email, :password, :stripeToken, :cause, :type, :amount, :create_account)
end
def set_user
user = 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
@@ -1,89 +0,0 @@
# frozen_string_literal: true
require_dependency 'discourse'
module DiscourseDonations
class CheckoutController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:create]
def create
Rails.logger.debug params.inspect
Rails.logger.debug user_params.inspect
output = { 'messages' => [], 'rewards' => [] }
payment = DiscourseDonations::Stripe.new(secret_key, stripe_options)
user = current_user || nil
begin
charge = payment.checkoutCharge(user, user_params[:stripeEmail], user_params[:stripeToken], user_params[:amount])
rescue ::Stripe::CardError => e
err = e.json_body[:error]
output['messages'] << "There was an error (#{err[:type]})."
output['messages'] << "Error code: #{err[:code]}" if err[:code]
output['messages'] << "Decline code: #{err[:decline_code]}" if err[:decline_code]
output['messages'] << "Message: #{err[:message]}" if err[:message]
render(json: output) && (return)
end
if charge['paid']
output['messages'] << I18n.l(Time.now(), format: :long) + ': ' + I18n.t('donations.payment.success')
output['rewards'] << { type: :group, name: group_name } if group_name
output['rewards'] << { type: :badge, name: badge_name } if badge_name
end
render json: output
end
private
def reward?(payment)
payment.present? && payment.successful?
end
def group_name
SiteSetting.discourse_donations_reward_group_name
end
def badge_name
SiteSetting.discourse_donations_reward_badge_name
end
def secret_key
SiteSetting.discourse_donations_secret_key
end
def user_params
params.permit(:amount,
:email,
:stripeToken,
:stripeTokenType,
:stripeEmail,
:stripeCustomerId,
:stripeBillingName,
:stripeBillingAddressLine1,
:stripeBillingAddressZip,
:stripeBillingAddressState,
:stripeBillingAddressCity,
:stripeBillingAddressCountry,
:stripeBillingAddressCountryCode,
:stripeShippingName,
:stripeShippingAddressLine1,
:stripeShippingAddressZip,
:stripeShippingAddressState,
:stripeShippingAddressCity,
:stripeShippingAddressCountry,
:stripeShippingAddressCountryCode
)
end
def stripe_options
{
description: SiteSetting.discourse_donations_description,
currency: SiteSetting.discourse_donations_currency
}
end
end
end
-4
View File
@@ -1,4 +0,0 @@
# frozen_string_literal: true
load File.expand_path('../regular/donation_user.rb', __FILE__)
load File.expand_path('../scheduled/update_category_donation_statistics.rb', __FILE__)
-19
View File
@@ -1,19 +0,0 @@
# frozen_string_literal: true
module Jobs
class DonationUser < ::Jobs::Base
def execute(args)
user = User.create!(args.slice(:username, :password, :name, :email))
return unless user.persisted?
Jobs.enqueue(
:critical_user_email,
type: :signup, user_id: user.id, email_token: user.email_tokens.first.token
)
rewards = DiscourseDonations::Rewards.new(user)
args[:rewards].to_a.each do |reward|
rewards.grant_badge(reward[:name]) if reward[:type] == 'badge'
rewards.add_to_group(reward[:name]) if reward[:type] == 'group'
end
end
end
end
@@ -1,71 +0,0 @@
# frozen_string_literal: true
module Jobs
class UpdateCategoryDonationStatistics < ::Jobs::Scheduled
every 1.day
def execute(args)
return unless SiteSetting.discourse_donations_cause_category
::Stripe.api_key = SiteSetting.discourse_donations_secret_key
totals = {}
backers = {}
categories = []
raw_charges = ::Stripe::Charge.list(
expand: ['data.invoice.subscription', 'data.customer']
)
raw_charges = raw_charges.is_a?(Object) ? raw_charges['data'] : []
raw_charges.each do |c|
cause_base = c['invoice'] && c['invoice']['subscription'] ? c['invoice']['subscription'] : c
category_id = cause_base['metadata']['discourse_cause'].to_i
backer_base = c['customer']
backer_user_id = backer_base['metadata']['discourse_user_id'].to_i
backer_email = backer_base['email']
if category_id > 0 && Category.exists?(id: category_id)
categories.push(category_id)
current = totals[category_id] || {}
amount = c['amount'].to_i
date = Time.at(c['created']).to_datetime
totals[category_id] ||= {}
totals[category_id][:total] ||= 0
totals[category_id][:month] ||= 0
totals[category_id][:total] += amount
if date.month == Date.today.month
totals[category_id][:month] += amount
end
backers[category_id] ||= []
if backer_user_id > 0 && User.exists?(id: backer_user_id)
backers[category_id].push(backer_user_id) unless backers[category_id].include? backer_user_id
elsif user = User.find_by_email(backer_email)
backers[category_id].push(user.id) unless backers[category_id].include? user.id
end
end
end
categories.each do |category_id|
category = Category.find(category_id)
if totals[category_id]
category.custom_fields['donations_total'] = totals[category_id][:total]
category.custom_fields['donations_month'] = totals[category_id][:month]
end
if backers[category_id]
category.custom_fields['donations_backers'] = backers[category_id]
end
category.save_custom_fields(true)
end
end
end
end
@@ -1,32 +0,0 @@
# frozen_string_literal: true
module DiscourseDonations
class Rewards
attr_reader :user
def initialize(user)
@user = user
end
def add_to_group(name)
grp = ::Group.find_by_name(name)
return if grp.nil?
log_group_add(grp)
grp.add(user)
end
def grant_badge(name)
return unless SiteSetting.enable_badges
badge = ::Badge.find_by_name(name)
return if badge.nil?
BadgeGranter.grant(badge, user)
end
private
def log_group_add(grp)
system_user = User.find(-1)
GroupActionLogger.new(system_user, grp).log_add_user_to_group(user)
end
end
end
-273
View File
@@ -1,273 +0,0 @@
# frozen_string_literal: true
module DiscourseDonations
class Stripe
attr_reader :charge, :currency, :description
def initialize(secret_key, opts)
::Stripe.api_key = secret_key
@description = opts[:description]
@currency = opts[:currency]
end
def checkoutCharge(user = nil, email, token, amount)
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, opts)
customer = customer(user,
email: opts[:email],
source: opts[:token],
create: true
)
return if !customer
metadata = {
discourse_cause: opts[:cause]
}
if (user)
metadata[:discourse_user_id] = user.id
end
@charge = ::Stripe::Charge.create(
customer: customer.id,
amount: opts[:amount],
description: @description,
currency: @currency,
receipt_email: customer.email,
metadata: metadata
)
@charge
end
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
}],
metadata: {
discourse_cause: opts[:cause],
discourse_user_id: user.id
}
)
end
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_opts = {
email: opts[:email],
source: opts[:source]
}
if user
customer_opts[:metadata] = {
discourse_user_id: user.id
}
end
customer = ::Stripe::Customer.create(customer_opts)
if user
user.custom_fields['stripe_customer_id'] = customer.id
user.save_custom_fields(true)
end
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
-4
View File
@@ -1,4 +0,0 @@
# frozen_string_literal: true
load File.expand_path('../discourse_donations/rewards.rb', __FILE__)
load File.expand_path('../discourse_donations/stripe.rb', __FILE__)
@@ -1 +0,0 @@
export default Ember.Component.extend({});
@@ -1,5 +0,0 @@
export default Ember.Component.extend({
classNames: "donation-list",
hasSubscriptions: Ember.computed.notEmpty("subscriptions"),
hasCharges: Ember.computed.notEmpty("charges")
});
@@ -1,99 +0,0 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { formatAnchor, formatAmount } from "../lib/donation-utilities";
import { default as computed } 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()
}
});
}
}
});
@@ -1,286 +0,0 @@
import { ajax } from "discourse/lib/ajax";
import { formatAnchor, zeroDecimalCurrencies } from "../lib/donation-utilities";
import { default as computed } from "ember-addons/ember-computed-decorators";
import { emailValid as emailValidHelper } from "discourse/lib/utilities";
export default Ember.Component.extend({
result: [],
stripe: null,
transactionInProgress: null,
settings: null,
showTransactionFeeDescription: false,
includeTransactionFee: true,
init() {
this._super(...arguments);
const user = this.get("currentUser");
const settings = Discourse.SiteSettings;
this.setProperties({
create_accounts:
!user && settings.discourse_donations_enable_create_accounts,
stripe: Stripe(settings.discourse_donations_public_key),
color: jQuery("body").css("color")
});
const types = settings.discourse_donations_types.split("|") || [];
const amounts = this.get("donateAmounts");
this.setProperties({
types,
type: types[0],
amount: amounts[0].value
});
},
@computed
causes() {
const categoryEnabled =
Discourse.SiteSettings.discourse_donations_cause_category;
if (categoryEnabled) {
let categoryIds = Discourse.SiteSettings.discourse_donations_causes_categories.split(
"|"
);
if (categoryIds.length) {
categoryIds = categoryIds.map(Number);
return this.site
.get("categoriesList")
.filter(c => {
return categoryIds.indexOf(c.id) > -1;
})
.map(c => {
return {
id: c.id,
name: c.name
};
});
} else {
return [];
}
} else {
const causes = Discourse.SiteSettings.discourse_donations_causes;
return causes ? causes.split("|") : [];
}
},
@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
donateAmounts() {
const setting = Discourse.SiteSettings.discourse_donations_amounts.split(
"|"
);
if (setting.length) {
return setting.map(amount => {
return {
value: parseInt(amount, 10),
name: `${amount}.00`
};
});
} else {
return [];
}
},
@computed("stripe")
card(stripe) {
const color = this.get("color");
const hidePostalCode = !Discourse.SiteSettings.discourse_donations_zip_code;
const elements = stripe.elements();
const style = {
base: {
color,
iconColor: color,
"::placeholder": { color }
}
};
const card = elements.create("card", { style, hidePostalCode });
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")
transactionFee(amount) {
const fixed =
Discourse.SiteSettings.discourse_donations_transaction_fee_fixed;
const percent =
Discourse.SiteSettings.discourse_donations_transaction_fee_percent;
const fee = (amount + fixed) / (1 - percent) - amount;
return Math.round(fee * 100) / 100;
},
@computed("amount", "transactionFee", "includeTransactionFee")
totalAmount(amount, fee, include) {
if (include) return amount + fee;
return amount;
},
@computed("email")
emailValid(email) {
return emailValidHelper(email);
},
@computed("email", "emailValid")
showEmailError(email, emailValid) {
return email && email.length > 3 && !emailValid;
},
@computed("currentUser", "emailValid")
userReady(currentUser, emailValid) {
return currentUser || emailValid;
},
@computed("cause")
causeValid(cause) {
return cause || !Discourse.SiteSettings.discourse_donations_cause_required;
},
@computed("userReady", "stripeReady", "causeValid")
formIncomplete(userReady, stripeReady, causeValid) {
return !userReady || !stripeReady || !causeValid;
},
@computed("transactionInProgress", "formIncomplete")
disableSubmit(transactionInProgress, formIncomplete) {
return transactionInProgress || formIncomplete;
},
didInsertElement() {
this._super();
this.get("card").mount("#card-element");
jQuery(document).on("click", Ember.run.bind(this, this.documentClick));
},
willDestroyElement() {
jQuery(document).off("click", Ember.run.bind(this, this.documentClick));
},
documentClick(e) {
let $element = jQuery(".transaction-fee-description");
let $target = jQuery(e.target);
if ($target.closest($element).length < 1 && this._state !== "destroying") {
this.set("showTransactionFeeDescription", false);
}
},
setSuccess() {
this.set("paymentSuccess", true);
},
endTranscation() {
this.set("transactionInProgress", false);
},
concatMessages(messages) {
this.set("result", this.get("result").concat(messages));
},
actions: {
toggleTransactionFeeDescription() {
this.toggleProperty("showTransactionFeeDescription");
},
submitStripeCard() {
let self = this;
this.set("transactionInProgress", true);
this.get("stripe")
.createToken(this.get("card"))
.then(data => {
self.set("result", []);
if (data.error) {
this.setProperties({
stripeError: data.error.message,
stripeReady: false
});
self.endTranscation();
} else {
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,
cause: self.get("cause"),
type: self.get("type"),
amount,
email: self.get("email"),
username: self.get("username"),
create_account: self.get("create_accounts")
};
if (!self.get("paymentSuccess")) {
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();
self.onCompleteTransation();
});
}
}
});
}
}
});
@@ -1,3 +0,0 @@
{{#if siteSettings.discourse_donations_cause_category}}
{{mount-widget widget="category-header-widget" args=(hash currentPath=currentPath)}}
{{/if}}
@@ -1,36 +0,0 @@
{{#if siteSettings.discourse_donations_cause_category}}
<section class='field'>
{{input type="checkbox" checked=category.custom_fields.donations_show_amounts}}
<span>{{i18n 'discourse_donations.cause.amounts.setting_label'}}</span>
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.github.setting_label'}}</label>
{{text-field value=category.custom_fields.donations_github placeholderKey="discourse_donations.cause.github.setting_placeholder"}}
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.meta.setting_label'}}</label>
{{text-field value=category.custom_fields.donations_meta placeholderKey="discourse_donations.cause.meta.setting_placeholder"}}
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.maintainers.label'}}</label>
{{user-selector usernames=category.custom_fields.donations_maintainers}}
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.maintainers.setting_label'}}</label>
{{input value=category.custom_fields.donations_maintainers_label}}
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.release_latest.label'}}</label>
{{input value=category.custom_fields.donations_release_latest}}
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.release_oldest.label'}}</label>
{{input value=category.custom_fields.donations_release_oldest}}
</section>
{{/if}}
@@ -1,5 +0,0 @@
{{#if siteSettings.discourse_donations_enabled}}
<a href="/donate">
{{i18n 'discourse_donations.nav_item'}}
</a>
{{/if}}
@@ -1,12 +0,0 @@
export default Ember.Controller.extend({
actions: {
confirm() {
this.get("model.confirm")();
this.send("closeModal");
},
cancel() {
this.send("closeModal");
}
}
});
@@ -1,61 +0,0 @@
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";
import { emailValid } from "discourse/lib/utilities";
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: {
stripeTransationCompleteCtr() {},
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");
}
}
});
@@ -0,0 +1,12 @@
export default function() {
const { disabled_plugins = [] } = this.site;
if (disabled_plugins.indexOf("discourse-patrons") !== -1) {
return;
}
this.route("patrons", function() {
this.route("show", { path: ":payment_id" });
});
}
@@ -1,3 +0,0 @@
export default function() {
this.route("donate");
}
@@ -1,56 +0,0 @@
import { withPluginApi } from "discourse/lib/plugin-api";
export default {
name: "donations-edits",
initialize(container) {
const siteSettings = container.lookup("site-settings:main");
withPluginApi("0.8.12", api => {
api.decorateCooked(
$post => {
const $form = $post.find(".stripe-checkout");
if ($form.length) {
const $input = $form.find("input");
var s = document.createElement("script");
s.src = "https://checkout.stripe.com/checkout.js";
s.setAttribute("class", "stripe-button");
s.setAttribute(
"data-key",
siteSettings.discourse_donations_public_key
);
s.setAttribute("data-amount", $input.attr("amount"));
s.setAttribute(
"data-name",
siteSettings.discourse_donations_shop_name
);
s.setAttribute("data-description", $form.attr("content"));
s.setAttribute("data-image", $form.attr("image") || "");
s.setAttribute("data-locale", "auto");
s.setAttribute(
"data-zip-code",
siteSettings.discourse_donations_zip_code
);
s.setAttribute(
"data-billing-address",
siteSettings.discourse_donations_billing_address
);
s.setAttribute(
"data-currency",
siteSettings.discourse_donations_currency
);
$form.append(s);
}
},
{ id: "discourse-donations" }
);
if (siteSettings.discourse_donations_cause_category) {
api.decorateWidget("category-header-widget:after", helper => {
helper.widget.appEvents.on("page:changed", () => {
helper.widget.scheduleRerender();
});
});
}
});
}
};
@@ -1,48 +0,0 @@
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 };
@@ -1,40 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
import DiscourseURL from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax";
export default DiscourseRoute.extend({
redirect() {
if (!Discourse.SiteSettings.discourse_donations_enabled) {
DiscourseURL.routeTo("/");
return;
}
},
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);
});
}
});
@@ -1,9 +0,0 @@
<form id="payment-form" class="form-horizontal">
<div class="donations-page-payment">
{{stripe-card
charges=charges
subscriptions=subscriptions
onCompleteTransation=onCompleteTransation
}}
</div>
</form>
@@ -1,28 +0,0 @@
{{#if hasSubscriptions}}
<div class="subscription-list">
<div class="underline">{{i18n 'discourse_donations.donations.subscriptions'}}</div>
<ul>
{{#each subscriptions as |s|}}
<li>{{donation-row subscription=s.subscription customer=customer new=s.new}}</li>
{{#if s.invoices}}
<ul>
{{#each s.invoices as |invoice|}}
<li>{{donation-row invoice=invoice customer=customer new=s.new}}</li>
{{/each}}
</ul>
{{/if}}
{{/each}}
</ul>
</div>
{{/if}}
{{#if hasCharges}}
<div class="charge-list">
<div class='underline'>{{i18n 'discourse_donations.donations.charges'}}</div>
<ul>
{{#each charges as |charge|}}
<li>{{donation-row charge=charge customer=customer new=charge.new}}</li>
{{/each}}
</ul>
</div>
{{/if}}
@@ -1,41 +0,0 @@
{{#if includePrefix}}
<span>{{i18n 'discourse_donations.invoice_prefix'}}</span>
{{/if}}
<span class="donation-row-currency">{{currency}}</span>
<span class="donation-row-amount">{{amount}}</span>
<span class="donation-row-period">{{period}}</span>
{{#if invoice}}
<a href='{{data.invoiceLink}}' target='_blank'>({{i18n 'discourse_donations.invoice'}})</a>
{{/if}}
{{#if currentUser}}
{{#if subscription}}
<span class="donation-row-subscription">
{{#if updating}}
{{loading-spinner size='small'}}
{{else}}
{{#unless canceled}}
<a {{action 'cancelSubscription'}}>
{{i18n 'cancel'}}
</a>
{{/unless}}
{{/if}}
</span>
{{/if}}
{{/if}}
{{#if receiptSent}}
<span>–</span>
<span>{{i18n 'discourse_donations.receipt' email=customer.email}}</span>
{{/if}}
{{#if new}}
<span class="new-flag">
{{d-icon 'circle'}}
<span>{{i18n 'new_item'}}</span>
</span>
{{/if}}
@@ -1,118 +0,0 @@
<div class="control-group">
<label class="control-label">
{{i18n 'discourse_donations.type'}}
</label>
<div class="controls controls-dropdown">
{{combo-box content=donationTypes value=type}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="card-element">
{{i18n 'discourse_donations.amount'}}
{{siteSettings.discourse_donations_currency}}
</label>
<div class="controls controls-dropdown">
{{combo-box valueAttribute="value" content=donateAmounts value=amount}}
</div>
</div>
{{#if siteSettings.discourse_donations_enable_transaction_fee}}
<div class="control-group">
<div class="controls">
{{input type="checkbox" checked=includeTransactionFee}}
<span>{{i18n 'discourse_donations.transaction_fee.label' transactionFee=transactionFee currency=siteSettings.discourse_donations_currency}}</span>
<div class='transaction-fee-description' {{action 'toggleTransactionFeeDescription'}}>
{{d-icon 'info-circle'}}
{{#if showTransactionFeeDescription}}
<div class="transaction-fee-description-modal">
{{i18n 'discourse_donations.transaction_fee.description'}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="control-group">
<label class='control-label'>
{{i18n 'discourse_donations.transaction_fee.total'}}
</label>
<div class="controls">
{{siteSettings.discourse_donations_currency}}
{{totalAmount}}
{{period}}
</div>
</div>
{{/if}}
<div class="control-group" style="width: 550px;">
<label class="control-label" for="card-element">{{i18n 'discourse_donations.card'}}</label>
<div class="controls">
<div id="card-element"></div>
{{#if stripeError}}
<div class="stripe-error">{{stripeError}}</div>
{{/if}}
</div>
</div>
{{#unless currentUser}}
<div class="control-group">
<label class="control-label" for="card-element">{{i18n 'user.email.title'}}</label>
<div class="controls">
{{text-field value=email}}
{{#if showEmailError}}
<div class="error">{{i18n 'user.email.invalid'}}</div>
{{else}}
<div class="instructions">{{i18n 'discourse_donations.email_instructions'}}</div>
{{/if}}
</div>
</div>
{{#if create_accounts}}
<div class="control-group">
<label class="control-label" for="card-element">{{i18n 'user.username.title'}}</label>
<div class="controls">
{{text-field value=username}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="card-element">{{i18n 'user.name.title'}}</label>
<div class="controls">
{{text-field value=name}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="card-element">{{i18n 'user.password.title'}}</label>
<div class="controls">
{{input type="password" value=password}}
</div>
</div>
{{/if}}
{{/unless}}
<div class="control-group save-button">
<div class="controls">
{{#d-button action="submitStripeCard" class="btn btn-primary btn-payment"}}
{{#if create_accounts}}
{{i18n 'discourse_donations.submit_with_create_account'}}
{{else}}
{{i18n 'discourse_donations.submit'}}
{{/if}}
{{/d-button}}
{{#if transactionInProgress}}
{{loading-spinner size="small"}}
{{/if}}
{{#each result as |message|}}
<p>{{{message}}}</p>
{{/each}}
{{#if success}}
<p>{{i18n 'discourse_donations.messages.success'}}</p>
{{/if}}
</div>
</div>
@@ -1,40 +0,0 @@
<h3>{{i18n 'discourse_donations.title' site_name=siteSettings.title}}</h3>
<div class="donations-page-description">
{{cook-text siteSettings.discourse_donations_page_description}}
</div>
<div class="donations-page-payment">
{{donation-form
charges=charges
subscriptions=subscriptions
onCompleteTransation=(action "stripeTransationCompleteCtr")
}}
</div>
<div class="donations-page-donations">
<h3>{{i18n 'discourse_donations.donations.title'}}</h3>
{{#if loadingDonations}}
<span>{{i18n 'discourse_donations.donations.loading'}}</span>
{{loading-spinner size='small'}}
{{else}}
{{#if currentUser}}
{{#if hasDonations}}
{{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}}
</div>
@@ -1,11 +0,0 @@
{{#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}}
<div class="modal-footer">
{{d-button action='confirm' label='yes_value' class='btn-primary'}}
{{d-button action='cancel' label='no_value'}}
</div>

Some files were not shown because too many files have changed in this diff Show More