mirror of
https://github.com/discourse/discourse-subscriptions.git
synced 2025-07-06 22:42:11 +00:00
FEATURE: Campaigns (#67)
Co-authored-by: Jordan Vidrine <jordan@jordanvidrine.com> See https://github.com/discourse/discourse-subscriptions/pull/67 for the full description.
This commit is contained in:
parent
dcec7703f8
commit
f596a0f78a
@ -13,7 +13,7 @@ module DiscourseSubscriptions
|
|||||||
products = []
|
products = []
|
||||||
|
|
||||||
if product_ids.present? && is_stripe_configured?
|
if product_ids.present? && is_stripe_configured?
|
||||||
products = ::Stripe::Product.list({ ids: product_ids })
|
products = ::Stripe::Product.list({ ids: product_ids, limit: 100 })
|
||||||
products = products[:data]
|
products = products[:data]
|
||||||
elsif !is_stripe_configured?
|
elsif !is_stripe_configured?
|
||||||
products = nil
|
products = nil
|
||||||
|
@ -5,5 +5,19 @@ module DiscourseSubscriptions
|
|||||||
def index
|
def index
|
||||||
head 200
|
head 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_campaign
|
||||||
|
Jobs.enqueue(:manually_update_campaign_data)
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_campaign
|
||||||
|
begin
|
||||||
|
DiscourseSubscriptions::Campaign.new.create_campaign
|
||||||
|
render json: success_json
|
||||||
|
rescue => e
|
||||||
|
render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,7 +5,7 @@ module DiscourseSubscriptions
|
|||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
include DiscourseSubscriptions::Group
|
include DiscourseSubscriptions::Group
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
requires_login except: [:index, :show]
|
requires_login except: [:index, :contributors, :show]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
begin
|
begin
|
||||||
@ -31,6 +31,18 @@ module DiscourseSubscriptions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def contributors
|
||||||
|
return unless SiteSetting.discourse_subscriptions_campaign_show_contributors
|
||||||
|
contributor_ids = Set.new
|
||||||
|
|
||||||
|
campaign_product = SiteSetting.discourse_subscriptions_campaign_product
|
||||||
|
campaign_product.present? ? contributor_ids.merge(Customer.where(product_id: campaign_product).last(5).pluck(:user_id)) : contributor_ids.merge(Customer.last(5).pluck(:user_id))
|
||||||
|
|
||||||
|
contributors = ::User.where(id: contributor_ids)
|
||||||
|
|
||||||
|
render_serialized(contributors, UserSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
params.require(:id)
|
params.require(:id)
|
||||||
begin
|
begin
|
||||||
|
11
app/jobs/regular/manually_update_campaign_data.rb
Normal file
11
app/jobs/regular/manually_update_campaign_data.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ::Jobs
|
||||||
|
class ManuallyUpdateCampaignData < ::Jobs::Base
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return unless SiteSetting.discourse_subscriptions_campaign_enabled
|
||||||
|
DiscourseSubscriptions::Campaign.new.refresh_data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
app/jobs/scheduled/refresh_subscriptions_campaign_data.rb
Normal file
12
app/jobs/scheduled/refresh_subscriptions_campaign_data.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ::Jobs
|
||||||
|
class RefreshSubscriptionsCampaignData < ::Jobs::Scheduled
|
||||||
|
every 30.minutes
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return unless SiteSetting.discourse_subscriptions_campaign_enabled
|
||||||
|
DiscourseSubscriptions::Campaign.new.refresh_data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
170
app/services/discourse_subscriptions/campaign.rb
Normal file
170
app/services/discourse_subscriptions/campaign.rb
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseSubscriptions
|
||||||
|
class Campaign
|
||||||
|
include DiscourseSubscriptions::Stripe
|
||||||
|
def initialize
|
||||||
|
set_api_key # instantiates Stripe API
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_data
|
||||||
|
product_ids = Set.new(Product.all.pluck(:external_id))
|
||||||
|
|
||||||
|
# if a product id is set for the campaign, we only want to return those results.
|
||||||
|
# if it's blank, return them all.
|
||||||
|
campaign_product = SiteSetting.discourse_subscriptions_campaign_product
|
||||||
|
if campaign_product.present?
|
||||||
|
product_ids = product_ids.include?(campaign_product) ? [campaign_product] : []
|
||||||
|
end
|
||||||
|
|
||||||
|
amount = 0
|
||||||
|
subscriptions = get_subscription_data
|
||||||
|
subscriptions = filter_to_subscriptions_products(subscriptions, product_ids)
|
||||||
|
|
||||||
|
# get number of subscribers
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_subscribers = subscriptions&.length.to_i
|
||||||
|
|
||||||
|
# calculate amount raised
|
||||||
|
subscriptions&.each do |sub|
|
||||||
|
sub_amount = calculate_monthly_amount(sub)
|
||||||
|
amount += sub_amount
|
||||||
|
end
|
||||||
|
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_amount_raised = amount
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_campaign
|
||||||
|
begin
|
||||||
|
group = create_campaign_group
|
||||||
|
product = create_campaign_product
|
||||||
|
create_campaign_prices(product, group)
|
||||||
|
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_enabled = true
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_product = product[:id]
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def create_campaign_group
|
||||||
|
campaign_group = SiteSetting.discourse_subscriptions_campaign_group
|
||||||
|
group = ::Group.find_by_id(campaign_group) if campaign_group.present?
|
||||||
|
|
||||||
|
unless group
|
||||||
|
group = ::Group.create(name: "campaign_supporters")
|
||||||
|
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_group = group[:id]
|
||||||
|
|
||||||
|
params = {
|
||||||
|
full_name: I18n.t('js.discourse_subscriptions.campaign.supporters'),
|
||||||
|
title: I18n.t('js.discourse_subscriptions.campaign.supporter'),
|
||||||
|
flair_icon: "donate"
|
||||||
|
}
|
||||||
|
|
||||||
|
group.update(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
group[:name]
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_campaign_product
|
||||||
|
product_params = {
|
||||||
|
name: I18n.t('js.discourse_subscriptions.campaign.title'),
|
||||||
|
active: true,
|
||||||
|
metadata: {
|
||||||
|
description: I18n.t('js.discourse_subscriptions.campaign.body'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
product = ::Stripe::Product.create(product_params)
|
||||||
|
|
||||||
|
Product.create(external_id: product[:id])
|
||||||
|
|
||||||
|
product
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_campaign_prices(product, group)
|
||||||
|
# hard coded defaults to make setting this up as simple as possible
|
||||||
|
monthly_prices = [3, 5, 10, 25]
|
||||||
|
yearly_prices = [50, 100]
|
||||||
|
|
||||||
|
monthly_prices.each do |price|
|
||||||
|
create_price(product[:id], group, price, "month")
|
||||||
|
end
|
||||||
|
|
||||||
|
yearly_prices.each do |price|
|
||||||
|
create_price(product[:id], group, price, "year")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_price(product_id, group_name, amount, recurrence)
|
||||||
|
price_object = {
|
||||||
|
nickname: "#{amount}/#{recurrence}",
|
||||||
|
unit_amount: amount * 100,
|
||||||
|
product: product_id,
|
||||||
|
currency: SiteSetting.discourse_subscriptions_currency,
|
||||||
|
active: true,
|
||||||
|
recurring: {
|
||||||
|
interval: recurrence
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
group_name: group_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = ::Stripe::Price.create(price_object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_subscription_data
|
||||||
|
subscriptions = []
|
||||||
|
current_set = {
|
||||||
|
has_more: true,
|
||||||
|
last_record: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
until current_set[:has_more] == false
|
||||||
|
current_set = ::Stripe::Subscription.list(
|
||||||
|
expand: ['data.plan.product'],
|
||||||
|
limit: 100,
|
||||||
|
starting_after: current_set[:last_record]
|
||||||
|
)
|
||||||
|
|
||||||
|
current_set[:last_record] = current_set[:data].last[:id] if current_set[:data].present?
|
||||||
|
subscriptions.concat(current_set[:data].to_a)
|
||||||
|
end
|
||||||
|
|
||||||
|
subscriptions
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_to_subscriptions_products(data, ids)
|
||||||
|
valid = data.select do |sub|
|
||||||
|
# cannot .dig stripe objects
|
||||||
|
items = sub[:items][:data][0] if sub[:items] && sub[:items][:data]
|
||||||
|
product = items[:price][:product] if items[:price] && items[:price][:product]
|
||||||
|
|
||||||
|
ids.include?(product)
|
||||||
|
end
|
||||||
|
valid.empty? ? nil : valid
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_monthly_amount(sub)
|
||||||
|
items = sub[:items][:data][0] if sub[:items] && sub[:items][:data]
|
||||||
|
price = items[:price] if items[:price]
|
||||||
|
unit_amount = price[:unit_amount] if price[:unit_amount]
|
||||||
|
recurrence = price[:recurring][:interval] if price[:recurring] && price[:recurring][:interval]
|
||||||
|
|
||||||
|
case recurrence
|
||||||
|
when "day"
|
||||||
|
unit_amount = unit_amount * 30
|
||||||
|
when "week"
|
||||||
|
unit_amount = unit_amount * 4
|
||||||
|
when "year"
|
||||||
|
unit_amount = unit_amount / 12
|
||||||
|
end
|
||||||
|
|
||||||
|
unit_amount
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
120
assets/javascripts/discourse/components/campaign-banner.js.es6
Normal file
120
assets/javascripts/discourse/components/campaign-banner.js.es6
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { action } from "@ember/object";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { equal } from "@ember/object/computed";
|
||||||
|
import { setting } from "discourse/lib/computed";
|
||||||
|
import Component from "@ember/component";
|
||||||
|
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
router: service(),
|
||||||
|
dismissed: false,
|
||||||
|
loading: false,
|
||||||
|
isSidebar: equal(
|
||||||
|
"siteSettings.discourse_subscriptions_campaign_banner_location",
|
||||||
|
"Sidebar"
|
||||||
|
),
|
||||||
|
subscribers: setting("discourse_subscriptions_campaign_subscribers"),
|
||||||
|
subscriberGoal: equal(
|
||||||
|
"siteSettings.discourse_subscriptions_campaign_type",
|
||||||
|
"Subscribers"
|
||||||
|
),
|
||||||
|
currency: setting("discourse_subscriptions_currency"),
|
||||||
|
goalTarget: setting("discourse_subscriptions_campaign_goal"),
|
||||||
|
product: setting("discourse_subscriptions_campaign_product"),
|
||||||
|
showContributors: setting(
|
||||||
|
"discourse_subscriptions_campaign_show_contributors"
|
||||||
|
),
|
||||||
|
classNameBindings: [
|
||||||
|
"isSidebar:campaign-banner-sidebar",
|
||||||
|
"shouldShow:campaign-banner",
|
||||||
|
],
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
this.set("contributors", []);
|
||||||
|
|
||||||
|
if (this.showContributors) {
|
||||||
|
return ajax("/s/contributors", { method: "get" }).then((result) => {
|
||||||
|
this.setProperties({
|
||||||
|
contributors: result,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
if (this.isSidebar && this.shouldShow) {
|
||||||
|
document.body.classList.add("subscription-campaign-sidebar");
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("subscription-campaign-sidebar");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed(
|
||||||
|
"router.currentRouteName",
|
||||||
|
"currentUser",
|
||||||
|
"siteSettings.discourse_subscriptions_campaign_enabled",
|
||||||
|
"visible"
|
||||||
|
)
|
||||||
|
shouldShow(currentRoute, currentUser, enabled, visible) {
|
||||||
|
// do not show on admin or subscriptions pages
|
||||||
|
const showOnRoute =
|
||||||
|
currentRoute !== "discovery.s" &&
|
||||||
|
!currentRoute.split(".")[0].includes("admin") &&
|
||||||
|
currentRoute.split(".")[0] !== "s";
|
||||||
|
|
||||||
|
return showOnRoute && currentUser && enabled && visible;
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes("dismissed")
|
||||||
|
_updateBodyClasses() {
|
||||||
|
if (this.dismissed) {
|
||||||
|
document.body.classList.remove("subscription-campaign-sidebar");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed("dismissed")
|
||||||
|
visible(dismissed) {
|
||||||
|
const dismissedBannerKey = this.keyValueStore.get(
|
||||||
|
"dismissed_campaign_banner"
|
||||||
|
);
|
||||||
|
const threeMonths = 2628000000 * 3;
|
||||||
|
|
||||||
|
const bannerDismissedTime = new Date(dismissedBannerKey);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
return (
|
||||||
|
(!dismissedBannerKey || now - bannerDismissedTime > threeMonths) &&
|
||||||
|
!dismissed
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
amountRaised() {
|
||||||
|
return (
|
||||||
|
this.siteSettings.discourse_subscriptions_campaign_amount_raised / 100
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
isGoalMet() {
|
||||||
|
const currentVolume = this.subscriberGoal
|
||||||
|
? this.subscribers
|
||||||
|
: this.amountRaised;
|
||||||
|
|
||||||
|
return currentVolume >= this.goalTarget;
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
dismissBanner() {
|
||||||
|
this.set("dismissed", true);
|
||||||
|
this.keyValueStore.set({
|
||||||
|
key: "dismissed_campaign_banner",
|
||||||
|
value: Date.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,69 @@
|
|||||||
|
import { action } from "@ember/object";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import Controller from "@ember/controller";
|
||||||
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
stripeConfigured() {
|
||||||
|
return !!this.siteSettings.discourse_subscriptions_public_key;
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
campaignEnabled() {
|
||||||
|
return this.siteSettings.discourse_subscriptions_campaign_enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
campaignProductSet() {
|
||||||
|
return !!this.siteSettings.discourse_subscriptions_campaign_product;
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
triggerManualRefresh() {
|
||||||
|
ajax(`/s/admin/refresh`, {
|
||||||
|
method: "post",
|
||||||
|
}).then(() => {
|
||||||
|
bootbox.alert(
|
||||||
|
I18n.t("discourse_subscriptions.campaign.refresh_page"),
|
||||||
|
() => {
|
||||||
|
this.transitionToRoute(
|
||||||
|
"adminPlugins.discourse-subscriptions.products"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
createOneClickCampaign() {
|
||||||
|
bootbox.confirm(
|
||||||
|
I18n.t("discourse_subscriptions.campaign.confirm_creation"),
|
||||||
|
(result) => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set("loading", true);
|
||||||
|
|
||||||
|
ajax(`/s/admin/create-campaign`, {
|
||||||
|
method: "post",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.set("loading", false);
|
||||||
|
bootbox.alert(
|
||||||
|
I18n.t("discourse_subscriptions.campaign.created"),
|
||||||
|
() => {
|
||||||
|
this.send("showSettings");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
@ -14,7 +14,7 @@ export default Controller.extend({
|
|||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set(
|
this.set(
|
||||||
"stripe",
|
"stripe",
|
||||||
Stripe(Discourse.SiteSettings.discourse_subscriptions_public_key)
|
Stripe(this.siteSettings.discourse_subscriptions_public_key)
|
||||||
);
|
);
|
||||||
const elements = this.get("stripe").elements();
|
const elements = this.get("stripe").elements();
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ export default Controller.extend({
|
|||||||
plan.type === "recurring"
|
plan.type === "recurring"
|
||||||
? "user.billing.subscriptions"
|
? "user.billing.subscriptions"
|
||||||
: "user.billing.payments",
|
: "user.billing.payments",
|
||||||
Discourse.User.current().username.toLowerCase()
|
this.currentUser.username.toLowerCase()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@ export default {
|
|||||||
|
|
||||||
map() {
|
map() {
|
||||||
this.route("discourse-subscriptions", function () {
|
this.route("discourse-subscriptions", function () {
|
||||||
this.route("dashboard");
|
|
||||||
|
|
||||||
this.route("products", function () {
|
this.route("products", function () {
|
||||||
this.route("show", { path: "/:product-id" }, function () {
|
this.route("show", { path: "/:product-id" }, function () {
|
||||||
this.route("plans", function () {
|
this.route("plans", function () {
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
import Route from "@ember/routing/route";
|
import Route from "@ember/routing/route";
|
||||||
|
|
||||||
export default Route.extend({});
|
export default Route.extend({
|
||||||
|
actions: {
|
||||||
|
showSettings() {
|
||||||
|
const controller = this.controllerFor("adminSiteSettings");
|
||||||
|
this.transitionTo("adminSiteSettingsCategory", "plugins").then(() => {
|
||||||
|
controller.set("filter", "plugin:discourse-subscriptions campaign");
|
||||||
|
controller.set("_skipBounce", true);
|
||||||
|
controller.filterContentNow("plugins");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{{#if model.unconfigured}}
|
{{#if model.unconfigured}}
|
||||||
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
||||||
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">Discourse Subscriptions on Meta</a></p>
|
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">{{i18n 'discourse_subscriptions.admin.on_meta'}}</a></p>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if model}}
|
{{#if model}}
|
||||||
<table class="table discourse-patrons-table">
|
<table class="table discourse-patrons-table">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{{#if model.unconfigured }}
|
{{#if model.unconfigured }}
|
||||||
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
||||||
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">Discourse Subscriptions on Meta</a></p>
|
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">{{i18n 'discourse_subscriptions.admin.on_meta'}}</a></p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="btn-right">
|
<p class="btn-right">
|
||||||
{{#link-to 'adminPlugins.discourse-subscriptions.products.show' 'new' class="btn btn-primary"}}
|
{{#link-to 'adminPlugins.discourse-subscriptions.products.show' 'new' class="btn btn-primary"}}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{{#if model.unconfigured}}
|
{{#if model.unconfigured}}
|
||||||
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
||||||
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">Discourse Subscriptions on Meta</a></p>
|
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">{{i18n 'discourse_subscriptions.admin.on_meta'}}</a></p>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#load-more selector=".discourse-patrons-table tr" action=(action "loadMore")}}
|
{{#load-more selector=".discourse-patrons-table tr" action=(action "loadMore")}}
|
||||||
<table class="table discourse-patrons-table">
|
<table class="table discourse-patrons-table">
|
||||||
|
@ -1,8 +1,27 @@
|
|||||||
|
|
||||||
<h2>{{i18n 'discourse_subscriptions.title' site_name=siteSettings.title}}</h2>
|
<h2>{{i18n 'discourse_subscriptions.title' site_name=siteSettings.title}}</h2>
|
||||||
|
|
||||||
|
{{#if stripeConfigured}}
|
||||||
|
<div class="discourse-subscriptions-buttons">
|
||||||
|
{{#if campaignEnabled}}
|
||||||
|
{{d-button
|
||||||
|
label="discourse_subscriptions.campaign.refresh_campaign"
|
||||||
|
icon="sync-alt"
|
||||||
|
action=(action 'triggerManualRefresh')
|
||||||
|
}}
|
||||||
|
{{else}}
|
||||||
|
{{#unless campaignProductSet}}
|
||||||
|
{{d-button
|
||||||
|
label="discourse_subscriptions.campaign.one_click_campaign"
|
||||||
|
icon="plus-square"
|
||||||
|
action=(action 'createOneClickCampaign')
|
||||||
|
isLoading=loading
|
||||||
|
}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
{{!-- {{nav-item route='adminPlugins.discourse-subscriptions.dashboard' label='discourse_subscriptions.admin.dashboard.title'}} --}}
|
|
||||||
{{nav-item route='adminPlugins.discourse-subscriptions.products' label='discourse_subscriptions.admin.products.title'}}
|
{{nav-item route='adminPlugins.discourse-subscriptions.products' label='discourse_subscriptions.admin.products.title'}}
|
||||||
{{nav-item route='adminPlugins.discourse-subscriptions.coupons' label='discourse_subscriptions.admin.coupons.title'}}
|
{{nav-item route='adminPlugins.discourse-subscriptions.coupons' label='discourse_subscriptions.admin.coupons.title'}}
|
||||||
{{nav-item route='adminPlugins.discourse-subscriptions.subscriptions' label='discourse_subscriptions.admin.subscriptions.title'}}
|
{{nav-item route='adminPlugins.discourse-subscriptions.subscriptions' label='discourse_subscriptions.admin.subscriptions.title'}}
|
||||||
@ -13,3 +32,7 @@
|
|||||||
<div id="discourse-subscriptions-admin">
|
<div id="discourse-subscriptions-admin">
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</div>
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
||||||
|
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">{{i18n 'discourse_subscriptions.admin.on_meta'}}</a></p>
|
||||||
|
{{/if}}
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
{{#if shouldShow}}
|
||||||
|
{{d-button
|
||||||
|
icon="times"
|
||||||
|
action="dismissBanner"
|
||||||
|
class="close"
|
||||||
|
}}
|
||||||
|
<div class="campaign-banner-info">
|
||||||
|
<h2 class="campaign-banner-info-header">{{i18n 'discourse_subscriptions.campaign.title'}}</h2>
|
||||||
|
<p class="campaign-banner-info-description">{{i18n 'discourse_subscriptions.campaign.body'}}</p>
|
||||||
|
{{#if product}}
|
||||||
|
{{#link-to "s.show" product disabled=product.subscribed class="btn btn-primary campaign-banner-info-button"}}
|
||||||
|
{{d-icon "far-heart"}} {{d-icon "heart" class="hover-heart"}} {{i18n 'discourse_subscriptions.campaign.button'}}
|
||||||
|
{{/link-to}}
|
||||||
|
{{else}}
|
||||||
|
{{#link-to "s" class="btn btn-primary campaign-banner-info-button"}}
|
||||||
|
{{d-icon "far-heart"}} {{d-icon "heart" class="hover-heart"}} {{i18n 'discourse_subscriptions.campaign.button'}}
|
||||||
|
{{/link-to}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="campaign-banner-progress">
|
||||||
|
{{#if isGoalMet}}
|
||||||
|
<p class="campaign-banner-progress-success">{{i18n 'discourse_subscriptions.campaign.goal'}}!</p>
|
||||||
|
{{#if subscriberGoal}}
|
||||||
|
<p class="campaign-banner-progress-description">
|
||||||
|
{{i18n 'discourse_subscriptions.campaign.goal_comparison' current=subscribers goal=goalTarget}}
|
||||||
|
{{i18n 'discourse_subscriptions.campaign.subscribers'}}
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="campaign-banner-progress-description">
|
||||||
|
{{html-safe (i18n 'discourse_subscriptions.campaign.goal_comparison' current=(format-currency currency amountRaised) goal=(format-currency currency goalTarget))}}
|
||||||
|
{{i18n 'discourse_subscriptions.campaign.raised'}}
|
||||||
|
</p>
|
||||||
|
{{#if showContributors}}
|
||||||
|
{{#conditional-loading-spinner condition=loading size="small"}}
|
||||||
|
<div class="campaign-banner-progress-users">
|
||||||
|
<p class="campaign-banner-progress-users-title">
|
||||||
|
<strong>{{i18n 'discourse_subscriptions.campaign.recent_contributors'}}</strong>
|
||||||
|
</p>
|
||||||
|
<div class="campaign-banner-progress-users-avatars">
|
||||||
|
{{#each contributors as |contributor|}}
|
||||||
|
{{avatar contributor avatarTemplatePath="avatar_template" usernamePath="username" namePath="name" imageSize="small"}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#if subscriberGoal}}
|
||||||
|
<progress class="campaign-banner-progress-bar" value="{{subscribers}}" max="{{siteSettings.discourse_subscriptions_campaign_goal}}"/>
|
||||||
|
<p class="campaign-banner-progress-description">
|
||||||
|
{{i18n 'discourse_subscriptions.campaign.goal_comparison' current=subscribers goal=goalTarget}}
|
||||||
|
{{i18n 'discourse_subscriptions.campaign.subscribers'}}
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<progress class="campaign-banner-progress-bar" value="{{amountRaised}}" max="{{siteSettings.discourse_subscriptions_campaign_goal}}"/>
|
||||||
|
<p class="campaign-banner-progress-description">
|
||||||
|
{{html-safe (i18n 'discourse_subscriptions.campaign.goal_comparison' current=(format-currency currency amountRaised) goal=(format-currency currency goalTarget))}}
|
||||||
|
{{i18n 'discourse_subscriptions.campaign.raised'}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if showContributors}}
|
||||||
|
{{#conditional-loading-spinner condition=loading size="small"}}
|
||||||
|
<div class="campaign-banner-progress-users">
|
||||||
|
<p class="campaign-banner-progress-users-title">
|
||||||
|
<strong>{{i18n 'discourse_subscriptions.campaign.recent_contributors'}}</strong>
|
||||||
|
</p>
|
||||||
|
<div class="campaign-banner-progress-users-avatars">
|
||||||
|
{{#each contributors as |contributor|}}
|
||||||
|
{{avatar contributor avatarTemplatePath="avatar_template" usernamePath="username" namePath="name" imageSize="small"}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
@ -0,0 +1,3 @@
|
|||||||
|
{{#unless (eq siteSettings.discourse_subscriptions_campaign_banner_location "Sidebar")}}
|
||||||
|
{{campaign-banner}}
|
||||||
|
{{/unless}}
|
@ -0,0 +1,3 @@
|
|||||||
|
{{#if (eq siteSettings.discourse_subscriptions_campaign_banner_location "Sidebar")}}
|
||||||
|
{{campaign-banner}}
|
||||||
|
{{/if}}
|
@ -44,7 +44,11 @@
|
|||||||
label="discourse_subscriptions.plans.payment_button"
|
label="discourse_subscriptions.plans.payment_button"
|
||||||
}}
|
}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<h2>{{i18n 'discourse_subscriptions.subscribe.already_purchased'}}</h2>
|
||||||
|
{{#link-to "user.billing.subscriptions" currentUser.username class="btn btn-primary"}}
|
||||||
|
{{i18n 'discourse_subscriptions.subscribe.go_to_billing'}}
|
||||||
|
{{/link-to}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
199
assets/stylesheets/common/campaign.scss
Normal file
199
assets/stylesheets/common/campaign.scss
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
.subscription-campaign-sidebar {
|
||||||
|
#main-outlet
|
||||||
|
.container.list-container
|
||||||
|
.row:nth-of-type(2)
|
||||||
|
.full-width
|
||||||
|
#list-area
|
||||||
|
.contents {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 78% calc(22% - 2em);
|
||||||
|
grid-template-areas: "content sidebar";
|
||||||
|
grid-column-gap: 2em;
|
||||||
|
span:first-of-type {
|
||||||
|
grid-area: sidebar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar Version
|
||||||
|
.campaign-banner.campaign-banner-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
.btn.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
font-size: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.campaign-banner-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
padding: 1em 1em 2em 1em;
|
||||||
|
position: relative;
|
||||||
|
&-header {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0.5em 0 !important;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&-description {
|
||||||
|
width: 100%;
|
||||||
|
font-size: $font-down-1;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.campaign-banner-progress {
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 1em;
|
||||||
|
&-users {
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-size: $font-down-1;
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.campaign-banner-info-button {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
progress[value] {
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
.campaign-banner-progress-description {
|
||||||
|
font-size: $font-down-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non Sidebar version
|
||||||
|
.campaign-banner {
|
||||||
|
display: flex;
|
||||||
|
width: calc(100%);
|
||||||
|
height: max-content;
|
||||||
|
margin: 1em 0 2em 0;
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
position: relative;
|
||||||
|
align-items: stretch;
|
||||||
|
.btn.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: transparent;
|
||||||
|
&:hover {
|
||||||
|
.d-icon {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 40%;
|
||||||
|
padding: 1.5em 2em 1.5em;
|
||||||
|
background-color: var(--primary-very-low);
|
||||||
|
&-header {
|
||||||
|
font-size: $font-up-4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&-success {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&-description {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
&-button {
|
||||||
|
height: 2.5em;
|
||||||
|
padding: 0.75em 1.5em 0.75em 2.75em;
|
||||||
|
position: relative;
|
||||||
|
.d-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 1em;
|
||||||
|
&.hover-heart {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.d-icon.hover-heart {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-progress {
|
||||||
|
width: 60%;
|
||||||
|
padding: 3em 2em 1.5em;
|
||||||
|
&-success {
|
||||||
|
font-size: $font-up-1;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--tertiary);
|
||||||
|
color: var(--secondary);
|
||||||
|
padding: 0.75em;
|
||||||
|
border-radius: 2em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&-description {
|
||||||
|
margin: 0.5em 0 0;
|
||||||
|
font-weight: 300;
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-users {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
margin: 0 0 -2em 0;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-avatars {
|
||||||
|
margin: 0 0 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 2.5em;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 2em;
|
||||||
|
background-color: var(--primary-very-low);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value]::-webkit-progress-bar {
|
||||||
|
background-color: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value]::-webkit-progress-value {
|
||||||
|
background-color: var(--tertiary);
|
||||||
|
border-top-right-radius: 2em;
|
||||||
|
border-bottom-right-radius: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value]::-moz-progress-bar {
|
||||||
|
background-color: var(--tertiary);
|
||||||
|
border-top-right-radius: 2em;
|
||||||
|
border-bottom-right-radius: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,10 @@ textarea[readonly] {
|
|||||||
border-color: #e9e9e9;
|
border-color: #e9e9e9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-plugins.discourse-subscriptions .discourse-subscriptions-buttons {
|
||||||
|
margin: 1em 0 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
#discourse-subscriptions-admin {
|
#discourse-subscriptions-admin {
|
||||||
.btn-right {
|
.btn-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -6,6 +6,12 @@ en:
|
|||||||
discourse_subscriptions_secret_key: Stripe Secret Key
|
discourse_subscriptions_secret_key: Stripe Secret Key
|
||||||
discourse_subscriptions_webhook_secret: Stripe Webhook Secret
|
discourse_subscriptions_webhook_secret: Stripe Webhook Secret
|
||||||
discourse_subscriptions_currency: Default Currency Code. This can be overridden when creating a subscription plan.
|
discourse_subscriptions_currency: Default Currency Code. This can be overridden when creating a subscription plan.
|
||||||
|
discourse_subscriptions_campaign_enabled: Enables a campaign banner to promote supporting this community financially.
|
||||||
|
discourse_subscriptions_campaign_goal: The numerical goal for your support campaign (subscribers or amount raised).
|
||||||
|
discourse_subscriptions_campaign_type: Selects the type of campaign to run (subscribers or monthly amount raised).
|
||||||
|
discourse_subscriptions_campaign_banner_location: Selects the location of the campaign banner (top or sidebar).
|
||||||
|
discourse_subscriptions_campaign_show_contributors: Show avatars of the most recent purchases of subscription products.
|
||||||
|
discourse_subscriptions_campaign_product: The Stripe product id to send supporters when they click the button on the campaign. If this setting is blank, supporters will be directed to the main products page.
|
||||||
errors:
|
errors:
|
||||||
discourse_patrons_amount_must_be_currency: "Currency amounts must be currencies without dollar symbol (eg 1.50)"
|
discourse_patrons_amount_must_be_currency: "Currency amounts must be currencies without dollar symbol (eg 1.50)"
|
||||||
js:
|
js:
|
||||||
@ -23,6 +29,33 @@ en:
|
|||||||
subscribe: Subscribe
|
subscribe: Subscribe
|
||||||
user_activity:
|
user_activity:
|
||||||
payments: Payments
|
payments: Payments
|
||||||
|
campaign:
|
||||||
|
title: Support Our Community
|
||||||
|
body: We need your help to keep this community up and running!
|
||||||
|
subscribers: Subscribers
|
||||||
|
goal_comparison: "<strong>%{current}</strong> of <strong>%{goal}</strong>"
|
||||||
|
raised: Raised
|
||||||
|
button: Support
|
||||||
|
recent_contributors: Recent Contributors
|
||||||
|
goal: We've met our goal
|
||||||
|
one_click_campaign: Auto-Create Support Campaign
|
||||||
|
refresh_campaign: Refresh Campaign Data
|
||||||
|
refresh_page: Campaign data is now syncing from Stripe in the background. Refresh the page in a few moments to load the updated info.
|
||||||
|
supporter: Supporter
|
||||||
|
supporters: Supporters
|
||||||
|
confirm_creation: |-
|
||||||
|
<p>Do you want to automatically create a campaign for your community?</p>
|
||||||
|
|
||||||
|
<p>Continuing will create:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>A group to reward your supporters with a custom title and avatar flair</li>
|
||||||
|
<li>A Stripe product with pre-set prices (monthly and annual) your users can purchase to support your site.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Do you wish to continue?</p>
|
||||||
|
|
||||||
|
created: Your one-click campaign is created and ready to go! Redirecting to the campaign settings.
|
||||||
one_time_payment: One-Time Payment
|
one_time_payment: One-Time Payment
|
||||||
plans:
|
plans:
|
||||||
purchase: Purchase a subscription
|
purchase: Purchase a subscription
|
||||||
@ -75,6 +108,7 @@ en:
|
|||||||
subscribe: Subscribe
|
subscribe: Subscribe
|
||||||
purchased: Purchased
|
purchased: Purchased
|
||||||
go_to_billing: Go to Billing
|
go_to_billing: Go to Billing
|
||||||
|
already_purchased: Thanks so much for your prior purchase of this product!
|
||||||
billing:
|
billing:
|
||||||
name: Full name
|
name: Full name
|
||||||
email: Email
|
email: Email
|
||||||
@ -88,6 +122,7 @@ en:
|
|||||||
success: Go back
|
success: Go back
|
||||||
admin:
|
admin:
|
||||||
unconfigured: 'Stripe is not configured correctly. Please see Discourse Meta for information.'
|
unconfigured: 'Stripe is not configured correctly. Please see Discourse Meta for information.'
|
||||||
|
on_meta: Discourse Subscriptions on Meta
|
||||||
dashboard:
|
dashboard:
|
||||||
title: Dashboard
|
title: Dashboard
|
||||||
table:
|
table:
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
require_dependency "subscriptions_user_constraint"
|
require_dependency "subscriptions_user_constraint"
|
||||||
|
|
||||||
DiscourseSubscriptions::Engine.routes.draw do
|
DiscourseSubscriptions::Engine.routes.draw do
|
||||||
# TODO: namespace this
|
|
||||||
scope 'admin' do
|
scope 'admin' do
|
||||||
get '/' => 'admin#index'
|
get '/' => 'admin#index'
|
||||||
|
post '/refresh' => 'admin#refresh_campaign'
|
||||||
|
post '/create-campaign' => 'admin#create_campaign'
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
@ -22,6 +23,7 @@ DiscourseSubscriptions::Engine.routes.draw do
|
|||||||
|
|
||||||
get '/' => 'subscribe#index'
|
get '/' => 'subscribe#index'
|
||||||
get '.json' => 'subscribe#index'
|
get '.json' => 'subscribe#index'
|
||||||
|
get '/contributors' => 'subscribe#contributors'
|
||||||
get '/:id' => 'subscribe#show'
|
get '/:id' => 'subscribe#show'
|
||||||
post '/create' => 'subscribe#create'
|
post '/create' => 'subscribe#create'
|
||||||
post '/finalize' => 'subscribe#finalize'
|
post '/finalize' => 'subscribe#finalize'
|
||||||
|
@ -28,3 +28,40 @@ plugins:
|
|||||||
- USD
|
- USD
|
||||||
- DKK
|
- DKK
|
||||||
- SGD
|
- SGD
|
||||||
|
discourse_subscriptions_campaign_enabled:
|
||||||
|
client: true
|
||||||
|
default: false
|
||||||
|
discourse_subscriptions_campaign_goal:
|
||||||
|
client: true
|
||||||
|
default: 100
|
||||||
|
discourse_subscriptions_campaign_type:
|
||||||
|
client: true
|
||||||
|
type: enum
|
||||||
|
default: "Amount"
|
||||||
|
choices:
|
||||||
|
- Amount
|
||||||
|
- Subscribers
|
||||||
|
discourse_subscriptions_campaign_banner_location:
|
||||||
|
client: true
|
||||||
|
type: enum
|
||||||
|
default: "Top"
|
||||||
|
choices:
|
||||||
|
- Top
|
||||||
|
- Sidebar
|
||||||
|
discourse_subscriptions_campaign_show_contributors:
|
||||||
|
client: true
|
||||||
|
default: true
|
||||||
|
discourse_subscriptions_campaign_product:
|
||||||
|
client: true
|
||||||
|
default: ""
|
||||||
|
discourse_subscriptions_campaign_amount_raised:
|
||||||
|
client: true
|
||||||
|
default: 0
|
||||||
|
hidden: true
|
||||||
|
discourse_subscriptions_campaign_subscribers:
|
||||||
|
client: true
|
||||||
|
default: 0
|
||||||
|
hidden: true
|
||||||
|
discourse_subscriptions_campaign_group:
|
||||||
|
default: ""
|
||||||
|
hidden: true
|
||||||
|
@ -13,6 +13,7 @@ gem 'stripe', '5.29.0'
|
|||||||
register_asset "stylesheets/common/main.scss"
|
register_asset "stylesheets/common/main.scss"
|
||||||
register_asset "stylesheets/common/layout.scss"
|
register_asset "stylesheets/common/layout.scss"
|
||||||
register_asset "stylesheets/common/subscribe.scss"
|
register_asset "stylesheets/common/subscribe.scss"
|
||||||
|
register_asset "stylesheets/common/campaign.scss"
|
||||||
register_asset "stylesheets/mobile/main.scss"
|
register_asset "stylesheets/mobile/main.scss"
|
||||||
register_svg_icon "far-credit-card" if respond_to?(:register_svg_icon)
|
register_svg_icon "far-credit-card" if respond_to?(:register_svg_icon)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
|||||||
module DiscourseSubscriptions
|
module DiscourseSubscriptions
|
||||||
RSpec.describe SubscribeController do
|
RSpec.describe SubscribeController do
|
||||||
let (:user) { Fabricate(:user) }
|
let (:user) { Fabricate(:user) }
|
||||||
|
let (:campaign_user) { Fabricate(:user) }
|
||||||
|
|
||||||
context "showing products" do
|
context "showing products" do
|
||||||
let(:product) do
|
let(:product) do
|
||||||
@ -73,6 +74,48 @@ module DiscourseSubscriptions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#get_contributors" do
|
||||||
|
before do
|
||||||
|
Fabricate(:product, external_id: "prod_campaign")
|
||||||
|
Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: 'x')
|
||||||
|
Fabricate(:customer, product_id: "prod_campaign", user_id: campaign_user.id, customer_id: 'y')
|
||||||
|
end
|
||||||
|
context 'not showing contributors' do
|
||||||
|
it 'returns nothing if not set to show contributors' do
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_show_contributors = false
|
||||||
|
get "/s/contributors.json"
|
||||||
|
|
||||||
|
data = response.parsed_body
|
||||||
|
expect(data).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'showing contributors' do
|
||||||
|
before do
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_show_contributors = true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters users by campaign product if set' do
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_product = "prod_campaign"
|
||||||
|
|
||||||
|
get "/s/contributors.json"
|
||||||
|
|
||||||
|
data = response.parsed_body
|
||||||
|
expect(data.first["id"]).to eq campaign_user.id
|
||||||
|
expect(data.length).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows all purchases if campaign product not set' do
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_product = nil
|
||||||
|
|
||||||
|
get "/s/contributors.json"
|
||||||
|
|
||||||
|
data = response.parsed_body
|
||||||
|
expect(data.length).to eq 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#show" do
|
describe "#show" do
|
||||||
it 'retrieves the product' do
|
it 'retrieves the product' do
|
||||||
::Stripe::Product.expects(:retrieve).with('prod_walterwhite').returns(product)
|
::Stripe::Product.expects(:retrieve).with('prod_walterwhite').returns(product)
|
||||||
|
110
spec/services/campaign_spec.rb
Normal file
110
spec/services/campaign_spec.rb
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe DiscourseSubscriptions::Campaign do
|
||||||
|
describe 'campaign data is refreshed' do
|
||||||
|
let (:user) { Fabricate(:user) }
|
||||||
|
let(:subscription) do
|
||||||
|
{
|
||||||
|
id: "sub_1234",
|
||||||
|
items: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
price: {
|
||||||
|
product: "prodct_23456",
|
||||||
|
unit_amount: 1000,
|
||||||
|
recurring: {
|
||||||
|
interval: "month"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:product, external_id: "prodct_23456")
|
||||||
|
Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: 'x')
|
||||||
|
SiteSetting.discourse_subscriptions_public_key = "public-key"
|
||||||
|
SiteSetting.discourse_subscriptions_secret_key = "secret-key"
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "refresh_data" do
|
||||||
|
context "for all subscription purchases" do
|
||||||
|
it "refreshes the campaign data properly" do
|
||||||
|
::Stripe::Subscription.expects(:list).returns(data: [subscription], has_more: false)
|
||||||
|
|
||||||
|
DiscourseSubscriptions::Campaign.new.refresh_data
|
||||||
|
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_subscribers).to eq 1
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_amount_raised).to eq 1000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a campaign product set" do
|
||||||
|
let(:user2) { Fabricate(:user) }
|
||||||
|
let(:campaign_subscription) do
|
||||||
|
{
|
||||||
|
id: "sub_5678",
|
||||||
|
items: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
price: {
|
||||||
|
product: "prod_use",
|
||||||
|
unit_amount: 10000,
|
||||||
|
recurring: {
|
||||||
|
interval: "year"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:product, external_id: "prod_use")
|
||||||
|
Fabricate(:customer, product_id: "prod_use", user_id: user2.id, customer_id: 'y')
|
||||||
|
SiteSetting.discourse_subscriptions_campaign_product = "prod_use"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "refreshes campaign data with only the campaign product/subscriptions" do
|
||||||
|
::Stripe::Subscription.expects(:list).returns(data: [subscription, campaign_subscription], has_more: false)
|
||||||
|
|
||||||
|
DiscourseSubscriptions::Campaign.new.refresh_data
|
||||||
|
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_subscribers).to eq 1
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_amount_raised).to eq 833
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "campaign is automatically created" do
|
||||||
|
describe "create_campaign" do
|
||||||
|
it "successfully creates the campaign group, product, and prices" do
|
||||||
|
::Stripe::Product.expects(:create).returns(id: "prod_campaign")
|
||||||
|
::Stripe::Price.expects(:create)
|
||||||
|
::Stripe::Price.expects(:create)
|
||||||
|
::Stripe::Price.expects(:create)
|
||||||
|
::Stripe::Price.expects(:create)
|
||||||
|
::Stripe::Price.expects(:create)
|
||||||
|
::Stripe::Price.expects(:create)
|
||||||
|
|
||||||
|
DiscourseSubscriptions::Campaign.new.create_campaign
|
||||||
|
|
||||||
|
group = Group.find_by(name: "campaign_supporters")
|
||||||
|
|
||||||
|
expect(group[:full_name]).to eq "Supporters"
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_group.to_i).to eq group.id
|
||||||
|
|
||||||
|
expect(DiscourseSubscriptions::Product.where(external_id: "prod_campaign").length).to eq 1
|
||||||
|
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_enabled).to eq true
|
||||||
|
expect(SiteSetting.discourse_subscriptions_campaign_product).to eq "prod_campaign"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user