Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf83c0693c | ||
|
|
c7d7e14217 | ||
|
|
9e5f61832d | ||
|
|
46f9fea9e2 | ||
|
|
fe53f2cbc0 | ||
|
|
7107966af6 | ||
|
|
33d033c23b | ||
|
|
4771d81bfa | ||
|
|
100b276c9d | ||
|
|
3dd59a60da | ||
|
|
b0af86f12b | ||
|
|
854b3f223f | ||
|
|
9e38a69dbb | ||
|
|
208ecb66a3 | ||
|
|
832c52d5d7 | ||
|
|
cc43d32af7 | ||
|
|
da48acad9a | ||
|
|
721c4f28f1 | ||
|
|
b4f7be4274 | ||
|
|
001a25c63c | ||
|
|
a780c77f6c | ||
|
|
02ba5e1ff2 | ||
|
|
2c46e3cffb | ||
|
|
73db0b208a | ||
|
|
cdd797c68f | ||
|
|
1811b5955d | ||
|
|
d82b34080b | ||
|
|
9263d97d64 | ||
|
|
692f410ab5 | ||
|
|
3149828c22 | ||
|
|
c7a35e1c89 | ||
|
|
12cac3827c | ||
|
|
9a0369f272 | ||
|
|
7b15f57af3 | ||
|
|
ce2f0899c6 | ||
|
|
dff47e2a3a | ||
|
|
f40d6f9cad | ||
|
|
185071aa03 | ||
|
|
f47400a9b1 | ||
|
|
eecb17698f | ||
|
|
937d099692 | ||
|
|
ef820ca2e0 | ||
|
|
f1b9c967c3 | ||
|
|
dd846ae87d | ||
|
|
9c5ec86de7 | ||
|
|
fa5e95860b | ||
|
|
700c5e7727 | ||
|
|
a2c70deeff | ||
|
|
71643a023d | ||
|
|
e92dd39928 | ||
|
|
f0b4984cee | ||
|
|
52cae7b1a9 | ||
|
|
0f52101f2e | ||
|
|
2824f955c2 | ||
|
|
bb96d77056 | ||
|
|
41c443ab23 | ||
|
|
403c966514 | ||
|
|
572aa0f4a9 | ||
|
|
c02193943b | ||
|
|
0fca89a1f7 | ||
|
|
3cc113973f | ||
|
|
a3b4521a76 | ||
|
|
e9817406f1 | ||
|
|
381e866009 | ||
|
|
804f5303f4 | ||
|
|
cac4bdd4ff | ||
|
|
542af4cf16 | ||
|
|
512f37a1d3 | ||
|
|
d63c84eca5 | ||
|
|
e132913db4 | ||
|
|
5eba613f8f | ||
|
|
66e8857c20 | ||
|
|
aaa4baec8a | ||
|
|
b1920caf7d | ||
|
|
7ee0b696ba | ||
|
|
6f54494a3b | ||
|
|
45754baa00 | ||
|
|
dcde03d7c4 | ||
|
|
d63f241e0d | ||
|
|
8f1b332f9e | ||
|
|
9190001a3c | ||
|
|
0d28c9a56d | ||
|
|
7361721ec0 | ||
|
|
87659e237b | ||
|
|
7b8a41ad0d | ||
|
|
0dcf17de59 | ||
|
|
2cfded9a77 | ||
|
|
d33cf2921e | ||
|
|
a3883d2a9a |
@@ -1,2 +1,7 @@
|
|||||||
|
< 3.5.0.beta5-dev: 7309c937dddb1933ac66aa2d3e13d071060d30ff
|
||||||
|
< 3.5.0.beta1-dev: b4f7be4274ada9400238f8692e705669034c1709
|
||||||
|
< 3.4.0.beta2-dev: 692f410ab59e329d0aaf537cd772698f83c32a8a
|
||||||
|
< 3.4.0.beta1-dev: bb96d77056a9ef842e01e46fd980320d427073ad
|
||||||
|
< 3.3.0.beta1-dev: 0dcf17de59bdf82c0233bb630dad0ce407d1ae76
|
||||||
3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43
|
3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43
|
||||||
2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826
|
2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"globals": {
|
|
||||||
"Stripe": true
|
|
||||||
},
|
|
||||||
"extends": "eslint-config-discourse"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@discourse/lint-configs/prettier");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@discourse/lint-configs/template-lint");
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: ["ember-template-lint-plugin-discourse"],
|
|
||||||
extends: "discourse:recommended",
|
|
||||||
};
|
|
||||||
+83
-24
@@ -7,36 +7,95 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
ast (2.4.2)
|
activesupport (8.0.2)
|
||||||
json (2.6.2)
|
base64
|
||||||
parallel (1.22.1)
|
benchmark (>= 0.3)
|
||||||
parser (3.1.2.1)
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
securerandom (>= 0.3)
|
||||||
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
|
uri (>= 0.13.1)
|
||||||
|
ast (2.4.3)
|
||||||
|
base64 (0.3.0)
|
||||||
|
benchmark (0.4.1)
|
||||||
|
bigdecimal (3.2.0)
|
||||||
|
concurrent-ruby (1.3.5)
|
||||||
|
connection_pool (2.5.3)
|
||||||
|
drb (2.2.3)
|
||||||
|
i18n (1.14.7)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
json (2.12.2)
|
||||||
|
language_server-protocol (3.17.0.5)
|
||||||
|
lint_roller (1.1.0)
|
||||||
|
logger (1.7.0)
|
||||||
|
minitest (5.25.5)
|
||||||
|
parallel (1.27.0)
|
||||||
|
parser (3.3.8.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
prettier_print (1.2.0)
|
racc
|
||||||
|
prettier_print (1.2.1)
|
||||||
|
prism (1.4.0)
|
||||||
|
racc (1.8.1)
|
||||||
|
rack (3.1.16)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.6.0)
|
regexp_parser (2.10.0)
|
||||||
rexml (3.2.5)
|
rubocop (1.75.8)
|
||||||
rubocop (1.36.0)
|
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.1.2.1)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-ast (>= 1.20.1, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.21.0)
|
rubocop-ast (1.44.1)
|
||||||
parser (>= 3.1.1.0)
|
parser (>= 3.3.7.2)
|
||||||
rubocop-discourse (3.0)
|
prism (~> 1.4)
|
||||||
rubocop (>= 1.1.0)
|
rubocop-capybara (2.22.1)
|
||||||
rubocop-rspec (>= 2.0.0)
|
lint_roller (~> 1.1)
|
||||||
rubocop-rspec (2.13.2)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
rubocop (~> 1.33)
|
rubocop-discourse (3.12.1)
|
||||||
ruby-progressbar (1.11.0)
|
activesupport (>= 6.1)
|
||||||
syntax_tree (5.1.0)
|
lint_roller (>= 1.1.0)
|
||||||
|
rubocop (>= 1.73.2)
|
||||||
|
rubocop-capybara (>= 2.22.0)
|
||||||
|
rubocop-factory_bot (>= 2.27.0)
|
||||||
|
rubocop-rails (>= 2.30.3)
|
||||||
|
rubocop-rspec (>= 3.0.1)
|
||||||
|
rubocop-rspec_rails (>= 2.31.0)
|
||||||
|
rubocop-factory_bot (2.27.1)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-rails (2.32.0)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
|
rubocop-rspec (3.6.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-rspec_rails (2.31.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-rspec (~> 3.5)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
|
securerandom (0.4.1)
|
||||||
|
syntax_tree (6.2.0)
|
||||||
prettier_print (>= 1.2.0)
|
prettier_print (>= 1.2.0)
|
||||||
unicode-display_width (2.3.0)
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
unicode-display_width (3.1.4)
|
||||||
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
|
unicode-emoji (4.0.4)
|
||||||
|
uri (1.0.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@@ -47,4 +106,4 @@ DEPENDENCIES
|
|||||||
translations-manager!
|
translations-manager!
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.4
|
2.6.9
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Discourse Subscriptions Plugin
|
# Discourse Subscriptions Plugin
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
|
||||||
|
|
||||||
Discourse Subscriptions allows site owners to sell recurring and one-time purchase subscriptions that grant access to a group on a Discourse instance.
|
Discourse Subscriptions allows site owners to sell recurring and one-time purchase subscriptions that grant access to a group on a Discourse instance.
|
||||||
|
|
||||||
For more information, please see: https://meta.discourse.org/t/discourse-subscriptions-plugin/140818
|
For more information, please see: https://meta.discourse.org/t/discourse-subscriptions-plugin/140818
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ module DiscourseSubscriptions
|
|||||||
class CouponsController < ::Admin::AdminController
|
class CouponsController < ::Admin::AdminController
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
include DiscourseSubscriptions::Group
|
include DiscourseSubscriptions::Group
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ module DiscourseSubscriptions
|
|||||||
class PlansController < ::Admin::AdminController
|
class PlansController < ::Admin::AdminController
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ module DiscourseSubscriptions
|
|||||||
class ProductsController < ::Admin::AdminController
|
class ProductsController < ::Admin::AdminController
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
module DiscourseSubscriptions
|
module DiscourseSubscriptions
|
||||||
module Admin
|
module Admin
|
||||||
class SubscriptionsController < ::Admin::AdminController
|
class SubscriptionsController < ::Admin::AdminController
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
include DiscourseSubscriptions::Group
|
include DiscourseSubscriptions::Group
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
@@ -51,7 +53,7 @@ module DiscourseSubscriptions
|
|||||||
params.require(:id)
|
params.require(:id)
|
||||||
begin
|
begin
|
||||||
refund_subscription(params[:id]) if params[:refund]
|
refund_subscription(params[:id]) if params[:refund]
|
||||||
subscription = ::Stripe::Subscription.delete(params[:id])
|
subscription = ::Stripe::Subscription.cancel(params[:id])
|
||||||
|
|
||||||
customer =
|
customer =
|
||||||
Customer.find_by(
|
Customer.find_by(
|
||||||
@@ -59,11 +61,8 @@ module DiscourseSubscriptions
|
|||||||
customer_id: subscription[:customer],
|
customer_id: subscription[:customer],
|
||||||
)
|
)
|
||||||
|
|
||||||
Subscription.delete_by(external_id: params[:id])
|
|
||||||
|
|
||||||
if customer
|
if customer
|
||||||
user = ::User.find(customer.user_id)
|
user = ::User.find(customer.user_id)
|
||||||
customer.delete
|
|
||||||
group = plan_group(subscription[:plan])
|
group = plan_group(subscription[:plan])
|
||||||
group.remove(user) if group
|
group.remove(user) if group
|
||||||
end
|
end
|
||||||
@@ -81,6 +80,7 @@ module DiscourseSubscriptions
|
|||||||
expand: ["data.plan.product"],
|
expand: ["data.plan.product"],
|
||||||
limit: PAGE_LIMIT,
|
limit: PAGE_LIMIT,
|
||||||
starting_after: start,
|
starting_after: start,
|
||||||
|
status: "all",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
module DiscourseSubscriptions
|
module DiscourseSubscriptions
|
||||||
class AdminController < ::Admin::AdminController
|
class AdminController < ::Admin::AdminController
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
def index
|
def index
|
||||||
head 200
|
head 200
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ module DiscourseSubscriptions
|
|||||||
include DiscourseSubscriptions::Group
|
include DiscourseSubscriptions::Group
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
layout false
|
layout false
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
skip_before_action :check_xhr
|
skip_before_action :check_xhr
|
||||||
skip_before_action :redirect_to_login_if_required
|
skip_before_action :redirect_to_login_if_required
|
||||||
skip_before_action :verify_authenticity_token, only: [:create]
|
skip_before_action :verify_authenticity_token, only: [:create]
|
||||||
@@ -25,48 +28,121 @@ module DiscourseSubscriptions
|
|||||||
end
|
end
|
||||||
|
|
||||||
case event[:type]
|
case event[:type]
|
||||||
|
when "checkout.session.completed"
|
||||||
|
checkout_session = event[:data][:object]
|
||||||
|
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_verbose_logging
|
||||||
|
Rails.logger.warn("checkout.session.completed data: #{checkout_session}")
|
||||||
|
end
|
||||||
|
email = checkout_session[:customer_email]
|
||||||
|
|
||||||
|
return head 200 if checkout_session[:status] != "complete"
|
||||||
|
return render_json_error "email not found" if !email
|
||||||
|
|
||||||
|
if checkout_session[:customer].nil?
|
||||||
|
customer = ::Stripe::Customer.create({ email: email })
|
||||||
|
customer_id = customer[:id]
|
||||||
|
else
|
||||||
|
customer_id = checkout_session[:customer]
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_verbose_logging
|
||||||
|
Rails.logger.warn("Looking up user with email: #{email}")
|
||||||
|
end
|
||||||
|
|
||||||
|
user = ::User.find_by_username_or_email(email)
|
||||||
|
|
||||||
|
return render_json_error "user not found" if !user
|
||||||
|
|
||||||
|
discourse_customer = Customer.create(user_id: user.id, customer_id: customer_id)
|
||||||
|
|
||||||
|
subscription = checkout_session[:subscription]
|
||||||
|
|
||||||
|
if subscription.present?
|
||||||
|
Subscription.create(customer_id: discourse_customer.id, external_id: subscription)
|
||||||
|
end
|
||||||
|
|
||||||
|
line_items =
|
||||||
|
::Stripe::Checkout::Session.list_line_items(checkout_session[:id], { limit: 1 })
|
||||||
|
item = line_items[:data].first
|
||||||
|
|
||||||
|
group = plan_group(item[:price])
|
||||||
|
group.add(user) unless group.nil?
|
||||||
|
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_verbose_logging
|
||||||
|
Rails.logger.warn("Line item with group name meta data: #{item[:price]}")
|
||||||
|
if group.nil?
|
||||||
|
Rails.logger.warn("Group not found or not listed in metadata!")
|
||||||
|
else
|
||||||
|
Rails.logger.warn("Group: #{group.name}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
discourse_customer.product_id = item[:price][:product]
|
||||||
|
discourse_customer.save!
|
||||||
|
|
||||||
|
if !subscription.nil?
|
||||||
|
::Stripe::Subscription.update(
|
||||||
|
subscription,
|
||||||
|
{ metadata: { user_id: user.id, username: user.username } },
|
||||||
|
)
|
||||||
|
end
|
||||||
when "customer.subscription.created"
|
when "customer.subscription.created"
|
||||||
when "customer.subscription.updated"
|
when "customer.subscription.updated"
|
||||||
customer =
|
subscription = event[:data][:object]
|
||||||
Customer.find_by(
|
status = subscription[:status]
|
||||||
customer_id: event[:data][:object][:customer],
|
return head 200 if !%w[complete active].include?(status)
|
||||||
product_id: event[:data][:object][:plan][:product],
|
|
||||||
)
|
customer = find_active_customer(subscription[:customer], subscription[:plan][:product])
|
||||||
|
|
||||||
return render_json_error "customer not found" if !customer
|
return render_json_error "customer not found" if !customer
|
||||||
return head 200 if event[:data][:object][:status] != "complete"
|
|
||||||
|
update_status(customer.id, subscription[:id], status)
|
||||||
|
|
||||||
user = ::User.find_by(id: customer.user_id)
|
user = ::User.find_by(id: customer.user_id)
|
||||||
return render_json_error "user not found" if !user
|
return render_json_error "user not found" if !user
|
||||||
|
|
||||||
if group = plan_group(event[:data][:object][:plan])
|
if group = plan_group(subscription[:plan])
|
||||||
group.add(user)
|
group.add(user)
|
||||||
end
|
end
|
||||||
when "customer.subscription.deleted"
|
when "customer.subscription.deleted"
|
||||||
customer =
|
subscription = event[:data][:object]
|
||||||
Customer.find_by(
|
|
||||||
customer_id: event[:data][:object][:customer],
|
customer = find_active_customer(subscription[:customer], subscription[:plan][:product])
|
||||||
product_id: event[:data][:object][:plan][:product],
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_json_error "customer not found" if !customer
|
return render_json_error "customer not found" if !customer
|
||||||
|
|
||||||
Subscription.find_by(
|
update_status(customer.id, subscription[:id], subscription[:status])
|
||||||
customer_id: customer.id,
|
|
||||||
external_id: event[:data][:object][:id],
|
|
||||||
)&.destroy!
|
|
||||||
|
|
||||||
user = ::User.find(customer.user_id)
|
user = ::User.find(customer.user_id)
|
||||||
return render_json_error "user not found" if !user
|
return render_json_error "user not found" if !user
|
||||||
|
|
||||||
if group = plan_group(event[:data][:object][:plan])
|
if group = plan_group(subscription[:plan])
|
||||||
group.remove(user)
|
group.remove(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
customer.destroy!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
head 200
|
head 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def update_status(customer_id, subscription_id, status)
|
||||||
|
discourse_subscription =
|
||||||
|
Subscription.find_by(customer_id: customer_id, external_id: subscription_id)
|
||||||
|
discourse_subscription.update(status: status) if discourse_subscription
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_active_customer(customer_id, product_id)
|
||||||
|
Customer
|
||||||
|
.joins(:subscriptions)
|
||||||
|
.where(customer_id: customer_id, product_id: product_id)
|
||||||
|
.where(
|
||||||
|
Subscription.arel_table[:status].eq(nil).or(
|
||||||
|
Subscription.arel_table[:status].not_eq("canceled"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.first
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseSubscriptions
|
||||||
|
class PricingtableController < ::ApplicationController
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
|
def index
|
||||||
|
head 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,9 @@ module DiscourseSubscriptions
|
|||||||
class SubscribeController < ::ApplicationController
|
class SubscribeController < ::ApplicationController
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
include DiscourseSubscriptions::Group
|
include DiscourseSubscriptions::Group
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
requires_login except: %i[index contributors show]
|
requires_login except: %i[index contributors show]
|
||||||
|
|
||||||
@@ -82,14 +85,19 @@ module DiscourseSubscriptions
|
|||||||
|
|
||||||
promo_code_id = promo_code[:id] if promo_code
|
promo_code_id = promo_code[:id] if promo_code
|
||||||
|
|
||||||
transaction =
|
subscription_params = {
|
||||||
::Stripe::Subscription.create(
|
customer: customer[:id],
|
||||||
customer: customer[:id],
|
items: [{ price: params[:plan] }],
|
||||||
items: [{ price: params[:plan] }],
|
metadata: metadata_user,
|
||||||
metadata: metadata_user,
|
trial_period_days: trial_days,
|
||||||
trial_period_days: trial_days,
|
promotion_code: promo_code_id,
|
||||||
promotion_code: promo_code_id,
|
}
|
||||||
)
|
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_automatic_tax
|
||||||
|
subscription_params[:automatic_tax] = { enabled: true }
|
||||||
|
end
|
||||||
|
|
||||||
|
transaction = ::Stripe::Subscription.create(subscription_params)
|
||||||
|
|
||||||
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[
|
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[
|
||||||
:status
|
:status
|
||||||
@@ -97,16 +105,28 @@ module DiscourseSubscriptions
|
|||||||
else
|
else
|
||||||
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] &&
|
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] &&
|
||||||
promo_code[:coupon][:id]
|
promo_code[:coupon][:id]
|
||||||
|
|
||||||
|
invoice_params = { customer: customer[:id] }
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_automatic_tax
|
||||||
|
invoice_params[:automatic_tax] = { enabled: true }
|
||||||
|
end
|
||||||
|
invoice = ::Stripe::Invoice.create(invoice_params)
|
||||||
|
|
||||||
invoice_item =
|
invoice_item =
|
||||||
::Stripe::InvoiceItem.create(
|
::Stripe::InvoiceItem.create(
|
||||||
customer: customer[:id],
|
customer: customer[:id],
|
||||||
price: params[:plan],
|
price: params[:plan],
|
||||||
discounts: [{ coupon: coupon_id }],
|
discounts: [{ coupon: coupon_id }],
|
||||||
|
invoice: invoice[:id],
|
||||||
)
|
)
|
||||||
invoice = ::Stripe::Invoice.create(customer: customer[:id])
|
|
||||||
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id])
|
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id])
|
||||||
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] ==
|
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] ==
|
||||||
"open"
|
"open"
|
||||||
|
if payment_intent.nil?
|
||||||
|
return(
|
||||||
|
render_json_error I18n.t("js.discourse_subscriptions.subscribe.transaction_error")
|
||||||
|
)
|
||||||
|
end
|
||||||
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] ==
|
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] ==
|
||||||
"successful"
|
"successful"
|
||||||
end
|
end
|
||||||
@@ -147,7 +167,11 @@ module DiscourseSubscriptions
|
|||||||
)
|
)
|
||||||
|
|
||||||
if transaction[:object] == "subscription"
|
if transaction[:object] == "subscription"
|
||||||
Subscription.create(customer_id: customer.id, external_id: transaction[:id])
|
Subscription.create(
|
||||||
|
customer_id: customer.id,
|
||||||
|
external_id: transaction[:id],
|
||||||
|
status: transaction[:status],
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -166,7 +190,17 @@ module DiscourseSubscriptions
|
|||||||
def current_user_products
|
def current_user_products
|
||||||
return [] if current_user.nil?
|
return [] if current_user.nil?
|
||||||
|
|
||||||
Customer.select(:product_id).where(user_id: current_user.id).map { |c| c.product_id }.compact
|
Customer
|
||||||
|
.joins(:subscriptions)
|
||||||
|
.where(user_id: current_user.id)
|
||||||
|
.where(
|
||||||
|
Subscription.arel_table[:status].eq(nil).or(
|
||||||
|
Subscription.arel_table[:status].not_eq("canceled"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.select(:product_id)
|
||||||
|
.distinct
|
||||||
|
.pluck(:product_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_plans(plans)
|
def serialize_plans(plans)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ module DiscourseSubscriptions
|
|||||||
module User
|
module User
|
||||||
class PaymentsController < ::ApplicationController
|
class PaymentsController < ::ApplicationController
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
requires_login
|
requires_login
|
||||||
|
|
||||||
@@ -24,10 +27,31 @@ module DiscourseSubscriptions
|
|||||||
payments = ::Stripe::PaymentIntent.list(customer: customer_id)
|
payments = ::Stripe::PaymentIntent.list(customer: customer_id)
|
||||||
payments_from_invoices =
|
payments_from_invoices =
|
||||||
payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
|
payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
|
||||||
data = data | payments_from_invoices
|
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_verbose_logging
|
||||||
|
Rails.logger.warn("Payments from invoices: #{payments_from_invoices}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pricing table one-off purchases do not have invoices
|
||||||
|
payments_without_invoices =
|
||||||
|
payments[:data].select { |payment| payment[:invoice].nil? }
|
||||||
|
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_verbose_logging
|
||||||
|
Rails.logger.warn("Payments without invoices: #{payments_without_invoices}")
|
||||||
|
end
|
||||||
|
|
||||||
|
data = data | payments_from_invoices | payments_without_invoices
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if SiteSetting.discourse_subscriptions_pricing_table_enabled && current_user.email
|
||||||
|
related_guest_payments = fetch_guest_payments(current_user.email)
|
||||||
|
if SiteSetting.discourse_subscriptions_enable_verbose_logging
|
||||||
|
Rails.logger.warn("Related guest payments: #{related_guest_payments}")
|
||||||
|
end
|
||||||
|
data = data | related_guest_payments
|
||||||
|
end
|
||||||
|
|
||||||
data = data.sort_by { |pmt| pmt[:created] }.reverse
|
data = data.sort_by { |pmt| pmt[:created] }.reverse
|
||||||
|
|
||||||
render_json_dump data
|
render_json_dump data
|
||||||
@@ -39,12 +63,13 @@ module DiscourseSubscriptions
|
|||||||
private
|
private
|
||||||
|
|
||||||
def parse_invoices(all_invoices, product_ids)
|
def parse_invoices(all_invoices, product_ids)
|
||||||
invoices_with_products =
|
all_invoices[:data].select do |invoice|
|
||||||
all_invoices[:data].select do |invoice|
|
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
|
||||||
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
|
if invoice_lines
|
||||||
invoice_product_id = parse_invoice_lines(invoice_lines)
|
invoice_product_id = parse_invoice_lines(invoice_lines)
|
||||||
product_ids.include?(invoice_product_id)
|
product_ids.include?(invoice_product_id)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_invoice_lines(invoice_lines)
|
def parse_invoice_lines(invoice_lines)
|
||||||
@@ -54,6 +79,40 @@ module DiscourseSubscriptions
|
|||||||
invoice_lines[:plan][:product]
|
invoice_lines[:plan][:product]
|
||||||
invoice_product_id
|
invoice_product_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_guest_payments(email)
|
||||||
|
guest_payments = []
|
||||||
|
starting_after = nil
|
||||||
|
|
||||||
|
begin
|
||||||
|
loop do
|
||||||
|
# Fetch charges in batches of 100, using pagination with starting_after
|
||||||
|
charges =
|
||||||
|
::Stripe::Charge.list(
|
||||||
|
limit: 100,
|
||||||
|
starting_after: starting_after,
|
||||||
|
expand: ["data.payment_intent"],
|
||||||
|
)
|
||||||
|
|
||||||
|
charges[:data].each do |charge|
|
||||||
|
# Check if the charge is associated with the given email and has no customer ID
|
||||||
|
if charge[:billing_details][:email] == email && charge[:customer].nil?
|
||||||
|
guest_payments << charge
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if there are more charges to fetch
|
||||||
|
break if charges[:data].count < 100
|
||||||
|
|
||||||
|
# Set starting_after to the last charge's ID for the next batch
|
||||||
|
starting_after = charges[:data].last[:id]
|
||||||
|
end
|
||||||
|
rescue ::Stripe::StripeError => e
|
||||||
|
render_json_error e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
guest_payments
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ module DiscourseSubscriptions
|
|||||||
class SubscriptionsController < ::ApplicationController
|
class SubscriptionsController < ::ApplicationController
|
||||||
include DiscourseSubscriptions::Stripe
|
include DiscourseSubscriptions::Stripe
|
||||||
include DiscourseSubscriptions::Group
|
include DiscourseSubscriptions::Group
|
||||||
|
|
||||||
|
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
requires_login
|
requires_login
|
||||||
|
|
||||||
@@ -12,6 +15,7 @@ module DiscourseSubscriptions
|
|||||||
begin
|
begin
|
||||||
customer = Customer.where(user_id: current_user.id)
|
customer = Customer.where(user_id: current_user.id)
|
||||||
customer_ids = customer.map { |c| c.id } if customer
|
customer_ids = customer.map { |c| c.id } if customer
|
||||||
|
stripe_customer_ids = customer.map { |c| c.customer_id }.uniq if customer
|
||||||
subscription_ids =
|
subscription_ids =
|
||||||
Subscription.where("customer_id in (?)", customer_ids).pluck(
|
Subscription.where("customer_id in (?)", customer_ids).pluck(
|
||||||
:external_id,
|
:external_id,
|
||||||
@@ -20,18 +24,25 @@ module DiscourseSubscriptions
|
|||||||
subscriptions = []
|
subscriptions = []
|
||||||
|
|
||||||
if subscription_ids
|
if subscription_ids
|
||||||
plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100)
|
prices = []
|
||||||
|
price_params = { limit: 100, expand: ["data.product"] }
|
||||||
|
loop do
|
||||||
|
response = ::Stripe::Price.list(price_params)
|
||||||
|
prices.concat(response[:data])
|
||||||
|
break unless response[:has_more]
|
||||||
|
price_params[:starting_after] = response[:data].last.id
|
||||||
|
end
|
||||||
|
all_subscriptions = []
|
||||||
|
|
||||||
customers =
|
stripe_customer_ids.each do |stripe_customer_id|
|
||||||
::Stripe::Customer.list(email: current_user.email, expand: ["data.subscriptions"])
|
customer_subscriptions =
|
||||||
|
::Stripe::Subscription.list(customer: stripe_customer_id, status: "all")
|
||||||
subscriptions =
|
all_subscriptions.concat(customer_subscriptions[:data])
|
||||||
customers[:data].map { |sub_customer| sub_customer[:subscriptions][:data] }.flatten(1)
|
end
|
||||||
|
|
||||||
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
|
|
||||||
|
|
||||||
|
subscriptions = all_subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
|
||||||
subscriptions.map! do |subscription|
|
subscriptions.map! do |subscription|
|
||||||
plan = plans[:data].find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
|
plan = prices.find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
|
||||||
subscription.to_h.except!(:plan)
|
subscription.to_h.except!(:plan)
|
||||||
subscription.to_h.merge(plan: plan, product: plan[:product].to_h.slice(:id, :name))
|
subscription.to_h.merge(plan: plan, product: plan[:product].to_h.slice(:id, :name))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,3 +13,20 @@ module DiscourseSubscriptions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_subscriptions_customers
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# customer_id :string not null
|
||||||
|
# product_id :string
|
||||||
|
# user_id :bigint
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_discourse_subscriptions_customers_on_customer_id (customer_id)
|
||||||
|
# index_discourse_subscriptions_customers_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
|||||||
@@ -4,3 +4,17 @@ module DiscourseSubscriptions
|
|||||||
class Product < ActiveRecord::Base
|
class Product < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_subscriptions_products
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# external_id :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_discourse_subscriptions_products_on_external_id (external_id)
|
||||||
|
#
|
||||||
|
|||||||
@@ -5,3 +5,20 @@ module DiscourseSubscriptions
|
|||||||
belongs_to :customer
|
belongs_to :customer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_subscriptions_subscriptions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# customer_id :bigint not null
|
||||||
|
# external_id :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# status :string
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_discourse_subscriptions_subscriptions_on_customer_id (customer_id)
|
||||||
|
# index_discourse_subscriptions_subscriptions_on_external_id (external_id)
|
||||||
|
#
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ module DiscourseSubscriptions
|
|||||||
"KR"
|
"KR"
|
||||||
when "sgd"
|
when "sgd"
|
||||||
"S$"
|
"S$"
|
||||||
|
when "zar"
|
||||||
|
"R"
|
||||||
|
when "chf"
|
||||||
|
"CHF"
|
||||||
|
when "pln"
|
||||||
|
"zł"
|
||||||
|
when "czk"
|
||||||
|
"Kč"
|
||||||
|
when "sek"
|
||||||
|
"kr"
|
||||||
else
|
else
|
||||||
"$"
|
"$"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ module DiscourseSubscriptions
|
|||||||
params = {
|
params = {
|
||||||
full_name: I18n.t("js.discourse_subscriptions.campaign.supporters"),
|
full_name: I18n.t("js.discourse_subscriptions.campaign.supporters"),
|
||||||
title: I18n.t("js.discourse_subscriptions.campaign.supporter"),
|
title: I18n.t("js.discourse_subscriptions.campaign.supporter"),
|
||||||
flair_icon: "donate",
|
flair_icon: "circle-dollar-to-slot",
|
||||||
}
|
}
|
||||||
|
|
||||||
group.update(params)
|
group.update(params)
|
||||||
@@ -154,26 +154,30 @@ module DiscourseSubscriptions
|
|||||||
all_invoices =
|
all_invoices =
|
||||||
::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record])
|
::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record])
|
||||||
|
|
||||||
current_set[:last_record] = all_invoices[:data].last[:id] if all_invoices[:data].present?
|
if all_invoices[:data].present?
|
||||||
current_set[:has_more] = all_invoices[:has_more]
|
current_set[:last_record] = all_invoices[:data].last[:id]
|
||||||
|
current_set[:has_more] = all_invoices[:has_more]
|
||||||
|
|
||||||
all_invoices[:data].each do |invoice|
|
all_invoices[:data].each do |invoice|
|
||||||
customer_id = invoice[:customer]
|
customer_id = invoice[:customer]
|
||||||
next if invoice[:paid] != true
|
next if invoice[:paid] != true
|
||||||
line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges
|
line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges
|
||||||
# check if non-subscription and that the plan is active
|
# check if non-subscription and that the plan is active
|
||||||
if line_item[:plan] == nil && line_item[:price] &&
|
if line_item && line_item[:plan] == nil && line_item[:price] &&
|
||||||
line_item[:price][:recurring] == nil && line_item[:price][:active] == true
|
line_item[:price][:recurring] == nil && line_item[:price][:active] == true
|
||||||
product_id = line_item[:price][:product]
|
product_id = line_item[:price][:product]
|
||||||
if product_ids.include? product_id
|
if product_ids.include? product_id
|
||||||
line_data = {
|
line_data = {
|
||||||
customer_id: customer_id,
|
customer_id: customer_id,
|
||||||
product_id: product_id,
|
product_id: product_id,
|
||||||
price: line_item[:price][:unit_amount],
|
price: line_item[:price][:unit_amount],
|
||||||
}
|
}
|
||||||
one_time_payments << line_data
|
one_time_payments << line_data
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
current_set[:has_more] = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,173 +0,0 @@
|
|||||||
{{#if shouldShow}}
|
|
||||||
<div
|
|
||||||
class="campaign-banner"
|
|
||||||
style={{html-safe (concat "box-shadow: 5px 5px #" dropShadowColor)}}
|
|
||||||
>
|
|
||||||
<DButton @icon="times" @action={{this.dismissBanner}} class="close" />
|
|
||||||
|
|
||||||
<div class="campaign-banner-info" style={{html-safe this.bannerInfoStyle}}>
|
|
||||||
{{#if isGoalMet}}
|
|
||||||
<h2 class="campaign-banner-info-header">
|
|
||||||
{{i18n "discourse_subscriptions.campaign.success_title"}}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="campaign-banner-info-description">
|
|
||||||
{{i18n "discourse_subscriptions.campaign.success_body"}}
|
|
||||||
</p>
|
|
||||||
{{else}}
|
|
||||||
<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}}
|
|
||||||
<LinkTo
|
|
||||||
@route="subscribe.show"
|
|
||||||
@model={{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"}}
|
|
||||||
</LinkTo>
|
|
||||||
{{else}}
|
|
||||||
<LinkTo
|
|
||||||
@route="subscribe"
|
|
||||||
class="btn btn-primary campaign-banner-info-button"
|
|
||||||
>
|
|
||||||
{{d-icon "far-heart"}}
|
|
||||||
{{d-icon "heart" class="hover-heart"}}
|
|
||||||
{{i18n "discourse_subscriptions.campaign.button"}}
|
|
||||||
</LinkTo>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="campaign-banner-progress">
|
|
||||||
{{#if isGoalMet}}
|
|
||||||
<div class="fireworks">
|
|
||||||
<div class="before"></div>
|
|
||||||
<div class="after"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="campaign-banner-progress-success"></div>
|
|
||||||
|
|
||||||
{{#if subscriberGoal}}
|
|
||||||
<p class="campaign-banner-progress-description">
|
|
||||||
{{html-safe
|
|
||||||
(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}}
|
|
||||||
<ConditionalLoadingSpinner @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>
|
|
||||||
</ConditionalLoadingSpinner>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
|
||||||
{{#if subscriberGoal}}
|
|
||||||
<progress
|
|
||||||
class="campaign-banner-progress-bar"
|
|
||||||
value={{subscribers}}
|
|
||||||
max={{siteSettings.discourse_subscriptions_campaign_goal}}
|
|
||||||
></progress>
|
|
||||||
|
|
||||||
<p class="campaign-banner-progress-description">
|
|
||||||
{{html-safe
|
|
||||||
(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}}
|
|
||||||
></progress>
|
|
||||||
|
|
||||||
<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}}
|
|
||||||
<ConditionalLoadingSpinner @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>
|
|
||||||
</ConditionalLoadingSpinner>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
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 { later } from "@ember/runloop";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
|
|
||||||
const SIDEBAR_BODY_CLASS = "subscription-campaign-sidebar";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
router: service(),
|
|
||||||
dismissed: false,
|
|
||||||
loading: false,
|
|
||||||
dropShadowColor: setting(
|
|
||||||
"discourse_subscriptions_campaign_banner_shadow_color"
|
|
||||||
),
|
|
||||||
backgroundImageUrl: setting(
|
|
||||||
"discourse_subscriptions_campaign_banner_bg_image"
|
|
||||||
),
|
|
||||||
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"),
|
|
||||||
amountRaised: setting("discourse_subscriptions_campaign_amount_raised"),
|
|
||||||
goalTarget: setting("discourse_subscriptions_campaign_goal"),
|
|
||||||
product: setting("discourse_subscriptions_campaign_product"),
|
|
||||||
showContributors: setting(
|
|
||||||
"discourse_subscriptions_campaign_show_contributors"
|
|
||||||
),
|
|
||||||
classNameBindings: ["isGoalMet:goal-met"],
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.set("contributors", []);
|
|
||||||
|
|
||||||
// add background-image url to stylesheet
|
|
||||||
if (this.backgroundImageUrl) {
|
|
||||||
const backgroundUrl = `url(${this.backgroundImageUrl}`.replace(/\\/g, "");
|
|
||||||
if (
|
|
||||||
document.documentElement.style.getPropertyValue(
|
|
||||||
"--campaign-background-image"
|
|
||||||
) !== backgroundUrl
|
|
||||||
) {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--campaign-background-image",
|
|
||||||
backgroundUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentUser && 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 && !this.site.mobileView) {
|
|
||||||
document.body.classList.add(SIDEBAR_BODY_CLASS);
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// makes sure to only play animation once, & not repeat on reload
|
|
||||||
if (this.isGoalMet) {
|
|
||||||
const successAnimationKey = this.keyValueStore.get(
|
|
||||||
"campaign_success_animation"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!successAnimationKey) {
|
|
||||||
later(() => {
|
|
||||||
this.keyValueStore.set({
|
|
||||||
key: "campaign_success_animation",
|
|
||||||
value: Date.now(),
|
|
||||||
});
|
|
||||||
document.body.classList.add("success-animation-off");
|
|
||||||
}, 7000);
|
|
||||||
} else {
|
|
||||||
document.body.classList.add("success-animation-off");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("backgroundImageUrl")
|
|
||||||
bannerInfoStyle(backgroundImageUrl) {
|
|
||||||
if (!backgroundImageUrl) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `background-image: linear-gradient(
|
|
||||||
0deg,
|
|
||||||
rgba(var(--secondary-rgb), 0.75) 0%,
|
|
||||||
rgba(var(--secondary-rgb), 0.75) 100%),
|
|
||||||
var(--campaign-background-image);
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;`;
|
|
||||||
},
|
|
||||||
|
|
||||||
@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] !== "subscribe";
|
|
||||||
|
|
||||||
if (!this.site.show_campaign_banner) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure not to render above main container when inside a topic
|
|
||||||
if (
|
|
||||||
this.connectorName === "above-main-container" &&
|
|
||||||
currentRoute.includes("topic")
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return showOnRoute && currentUser && enabled && visible;
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("dismissed")
|
|
||||||
_updateBodyClasses() {
|
|
||||||
if (this.dismissed) {
|
|
||||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@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
|
|
||||||
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,92 @@
|
|||||||
|
import Component, { Input } from "@ember/component";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import discourseComputed from "discourse/lib/decorators";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
|
|
||||||
|
export default class CreateCouponForm extends Component {
|
||||||
|
discountType = "amount";
|
||||||
|
discount = null;
|
||||||
|
promoCode = null;
|
||||||
|
active = false;
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
discountTypes() {
|
||||||
|
return [
|
||||||
|
{ id: "amount", name: "Amount" },
|
||||||
|
{ id: "percent", name: "Percent" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
createNewCoupon() {
|
||||||
|
const createParams = {
|
||||||
|
promo: this.promoCode,
|
||||||
|
discount_type: this.discountType,
|
||||||
|
discount: this.discount,
|
||||||
|
active: this.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.create(createParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
cancelCreate() {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="create-coupon-form">
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<p>
|
||||||
|
<label for="promo_code">
|
||||||
|
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
|
||||||
|
</label>
|
||||||
|
<Input @type="text" name="promo_code" @value={{this.promoCode}} />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="amount">
|
||||||
|
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
|
||||||
|
</label>
|
||||||
|
<ComboBox
|
||||||
|
@content={{this.discountTypes}}
|
||||||
|
@value={{this.discountType}}
|
||||||
|
@onChange={{fn (mut this.discountType)}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
class="discount-amount"
|
||||||
|
@type="text"
|
||||||
|
name="amount"
|
||||||
|
@value={{this.discount}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="active">
|
||||||
|
{{i18n "discourse_subscriptions.admin.coupons.active"}}
|
||||||
|
</label>
|
||||||
|
<Input @type="checkbox" name="active" @checked={{this.active}} />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@action={{this.createNewCoupon}}
|
||||||
|
@label="discourse_subscriptions.admin.coupons.create"
|
||||||
|
@title="discourse_subscriptions.admin.coupons.create"
|
||||||
|
@icon="plus"
|
||||||
|
class="btn-primary btn btn-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@action={{this.cancelCreate}}
|
||||||
|
label="cancel"
|
||||||
|
@title="cancel"
|
||||||
|
@icon="xmark"
|
||||||
|
class="btn btn-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<div class="create-coupon-form">
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<p>
|
|
||||||
<label for="promo_code">
|
|
||||||
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
|
|
||||||
</label>
|
|
||||||
<Input @type="text" name="promo_code" @value={{promoCode}} />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<label for="amount">
|
|
||||||
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
|
|
||||||
</label>
|
|
||||||
<ComboBox
|
|
||||||
@content={{discountTypes}}
|
|
||||||
@value={{discountType}}
|
|
||||||
@onChange={{action (mut discountType)}}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
class="discount-amount"
|
|
||||||
@type="text"
|
|
||||||
name="amount"
|
|
||||||
@value={{discount}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<label for="active">
|
|
||||||
{{i18n "discourse_subscriptions.admin.coupons.active"}}
|
|
||||||
</label>
|
|
||||||
<Input @type="checkbox" name="active" @checked={{active}} />
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@action={{action "createNewCoupon"}}
|
|
||||||
@label="discourse_subscriptions.admin.coupons.create"
|
|
||||||
@title="discourse_subscriptions.admin.coupons.create"
|
|
||||||
@icon="plus"
|
|
||||||
class="btn-primary btn btn-icon"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@action={{action "cancelCreate"}}
|
|
||||||
label="cancel"
|
|
||||||
@title="cancel"
|
|
||||||
@icon="times"
|
|
||||||
class="btn btn-icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import Component from "@ember/component";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
@discourseComputed
|
|
||||||
discountTypes() {
|
|
||||||
return [
|
|
||||||
{ id: "amount", name: "Amount" },
|
|
||||||
{ id: "percent", name: "Percent" },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
discountType: "amount",
|
|
||||||
discount: null,
|
|
||||||
promoCode: null,
|
|
||||||
active: false,
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
createNewCoupon() {
|
|
||||||
const createParams = {
|
|
||||||
promo: this.promoCode,
|
|
||||||
discount_type: this.discountType,
|
|
||||||
discount: this.discount,
|
|
||||||
active: this.active,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.create(createParams);
|
|
||||||
},
|
|
||||||
cancelCreate() {
|
|
||||||
this.cancel();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user