diff --git a/app/controllers/discourse_subscriptions/hooks_controller.rb b/app/controllers/discourse_subscriptions/hooks_controller.rb
index e779c6d..cf235e8 100644
--- a/app/controllers/discourse_subscriptions/hooks_controller.rb
+++ b/app/controllers/discourse_subscriptions/hooks_controller.rb
@@ -9,6 +9,7 @@ module DiscourseSubscriptions
layout false
+ before_action :set_api_key
skip_before_action :check_xhr
skip_before_action :redirect_to_login_if_required
skip_before_action :verify_authenticity_token, only: [:create]
@@ -27,6 +28,40 @@ module DiscourseSubscriptions
end
case event[:type]
+ when "checkout.session.completed"
+ checkout_session = event[:data][:object]
+ email = checkout_session[:customer_email]
+
+ return head 200 if checkout_session[:status] != "complete"
+ return render_json_error "customer not found" if checkout_session[:customer].nil?
+
+ customer_id = checkout_session[:customer]
+
+ user = ::User.find_by_username_or_email(email)
+
+ discourse_customer = Customer.find_by(user_id: user.id)
+
+ if discourse_customer.nil?
+ discourse_customer = Customer.create(user_id: user.id, customer_id: customer_id)
+ end
+
+ Subscription.create(
+ customer_id: discourse_customer.id,
+ external_id: checkout_session[:subscription],
+ )
+
+ 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?
+ discourse_customer.product_id = item[:price][:product]
+ discourse_customer.save!
+
+ ::Stripe::Subscription.update(
+ checkout_session[:subscription],
+ { metadata: { user_id: user.id, username: user.username } },
+ )
when "customer.subscription.created"
when "customer.subscription.updated"
customer =
diff --git a/app/controllers/discourse_subscriptions/pricingtable_controller.rb b/app/controllers/discourse_subscriptions/pricingtable_controller.rb
new file mode 100644
index 0000000..eca32e4
--- /dev/null
+++ b/app/controllers/discourse_subscriptions/pricingtable_controller.rb
@@ -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
diff --git a/assets/javascripts/discourse/controllers/subscriptions.js b/assets/javascripts/discourse/controllers/subscriptions.js
new file mode 100644
index 0000000..41b62b9
--- /dev/null
+++ b/assets/javascripts/discourse/controllers/subscriptions.js
@@ -0,0 +1,43 @@
+import Controller from "@ember/controller";
+import { computed } from "@ember/object";
+import { htmlSafe } from "@ember/template";
+import I18n from "I18n";
+
+export default Controller.extend({
+ init() {
+ this._super(...arguments);
+ if (this.currentUser) {
+ this.currentUser
+ .checkEmail()
+ .then(() => this.set("email", this.currentUser.email));
+ }
+ },
+ pricingTable: computed("email", function () {
+ try {
+ const pricingTableId =
+ this.siteSettings.discourse_subscriptions_pricing_table_id;
+ const publishableKey =
+ this.siteSettings.discourse_subscriptions_public_key;
+ const pricingTableEnabled =
+ this.siteSettings.discourse_subscriptions_pricing_table_enabled;
+
+ if (!pricingTableEnabled || !pricingTableId || !publishableKey) {
+ throw new Error("Pricing table not configured");
+ }
+
+ if (this.currentUser) {
+ return htmlSafe(``);
+ } else {
+ return htmlSafe(``);
+ }
+ } catch (error) {
+ return I18n.t("discourse_subscriptions.subscribe.no_products");
+ }
+ }),
+});
diff --git a/assets/javascripts/discourse/initializers/setup-subscriptions.js b/assets/javascripts/discourse/initializers/setup-subscriptions.js
index 42339a9..1c3c190 100644
--- a/assets/javascripts/discourse/initializers/setup-subscriptions.js
+++ b/assets/javascripts/discourse/initializers/setup-subscriptions.js
@@ -8,11 +8,14 @@ export default {
const siteSettings = container.lookup("service:site-settings");
const isNavLinkEnabled =
siteSettings.discourse_subscriptions_extra_nav_subscribe;
+ const isPricingTableEnabled =
+ siteSettings.discourse_subscriptions_pricing_table_enabled;
+ const subscribeHref = isPricingTableEnabled ? "/s/subscriptions" : "/s";
if (isNavLinkEnabled) {
api.addNavigationBarItem({
name: "subscribe",
displayName: I18n.t("discourse_subscriptions.navigation.subscribe"),
- href: "/s",
+ href: subscribeHref,
});
}
diff --git a/assets/javascripts/discourse/subscriptions-route-map.js b/assets/javascripts/discourse/subscriptions-route-map.js
index 2d68b94..cea2fb2 100644
--- a/assets/javascripts/discourse/subscriptions-route-map.js
+++ b/assets/javascripts/discourse/subscriptions-route-map.js
@@ -1,4 +1,5 @@
export default function () {
+ this.route("subscriptions", { path: "/s/subscriptions" });
this.route("subscribe", { path: "/s" }, function () {
this.route("show", { path: "/:subscription-id" });
});
diff --git a/assets/javascripts/discourse/templates/subscriptions.hbs b/assets/javascripts/discourse/templates/subscriptions.hbs
new file mode 100644
index 0000000..4705670
--- /dev/null
+++ b/assets/javascripts/discourse/templates/subscriptions.hbs
@@ -0,0 +1,3 @@
+
+ {{pricingTable}}
+
\ No newline at end of file
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 6d1f10f..c069e93 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -7,6 +7,8 @@ en:
site_settings:
discourse_subscriptions_enabled: Enable the Discourse Subscriptions plugin.
discourse_subscriptions_extra_nav_subscribe: Show the subscribe button in the primary navigation
+ discourse_subscriptions_pricing_table_id: Add the pricing-table-id from the Pricing Table embed code here.
+ discourse_subscriptions_pricing_table_enabled: Subscribe button will show the embedded pricing table and Stripe Checkout will be used.
discourse_subscriptions_public_key: Stripe Publishable Key
discourse_subscriptions_secret_key: Stripe Secret Key
discourse_subscriptions_webhook_secret: Stripe Webhook Secret
diff --git a/config/settings.yml b/config/settings.yml
index 3dbd594..8e77799 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -4,6 +4,12 @@ discourse_subscriptions:
discourse_subscriptions_extra_nav_subscribe:
default: false
client: true
+ discourse_subscriptions_pricing_table_id:
+ default: ''
+ client: true
+ discourse_subscriptions_pricing_table_enabled:
+ default: false
+ client: true
discourse_subscriptions_public_key:
default: ''
client: true
diff --git a/plugin.rb b/plugin.rb
index 0b02690..ec5e583 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -22,6 +22,10 @@ register_html_builder("server:before-head-close") do |controller|
""
end
+register_html_builder("server:before-head-close") do |controller|
+ ""
+end
+
extend_content_security_policy(script_src: %w[https://js.stripe.com/v3/ https://hooks.stripe.com])
add_admin_route "discourse_subscriptions.admin_navigation", "discourse-subscriptions.products"
diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb
index 60a863d..8cc94b7 100644
--- a/spec/requests/hooks_controller_spec.rb
+++ b/spec/requests/hooks_controller_spec.rb
@@ -43,6 +43,128 @@ RSpec.describe DiscourseSubscriptions::HooksController do
}
end
+ let(:checkout_session_completed_data) do
+ {
+ object: {
+ id: "cs_test_a1ENei5A9TGOaEketyV5qweiQR5CyJWHT5j8T3HheQY3uah3RxzKttVUKZ",
+ object: "checkout.session",
+ customer: customer.customer_id,
+ customer_email: user.email,
+ invoice: "in_1P9b7iEYXaQnncSh81AQtuHD",
+ metadata: {
+ },
+ mode: "subscription",
+ payment_status: "paid",
+ status: "complete",
+ submit_type: nil,
+ subscription: "sub_1P9b7iEYXaQnncSh3H3G9d2Y",
+ success_url: "http://localhost:4200/my/billing/subscriptions",
+ url: nil,
+ },
+ }
+ end
+
+ let(:checkout_session_completed_bad_data) do
+ {
+ object: {
+ id: "cs_test_a1ENei5A9TGOaEketyV5qweiQR5CyJWHT5j8T3HheQY3uah3RxzKttVUKZ",
+ object: "checkout.session",
+ customer: nil,
+ customer_email: user.email,
+ invoice: "in_1P9b7iEYXaQnncSh81AQtuHD",
+ metadata: {
+ },
+ mode: "subscription",
+ payment_status: "paid",
+ status: "complete",
+ submit_type: nil,
+ subscription: "sub_1P9b7iEYXaQnncSh3H3G9d2Y",
+ success_url: "http://localhost:4200/my/billing/subscriptions",
+ url: nil,
+ },
+ }
+ end
+
+ let(:list_line_items_data) do
+ {
+ data: [
+ {
+ id: "li_1P9YlFEYXaQnncShFBl7r0na",
+ object: "item",
+ description: "Exclusive Access",
+ price: {
+ id: "price_1OrmlvEYXaQnncShNahrpKvA",
+ metadata: {
+ group_name: group.name,
+ trial_period_days: "0",
+ },
+ nickname: "EA1",
+ product: "prod_PhB6IpGhEX14Hi",
+ },
+ },
+ ],
+ }
+ end
+
+ describe "checkout.session.completed" do
+ before do
+ event = { type: "checkout.session.completed", data: checkout_session_completed_data }
+ ::Stripe::Checkout::Session
+ .stubs(:list_line_items)
+ .with(checkout_session_completed_data[:object][:id], { limit: 1 })
+ .returns(list_line_items_data)
+
+ ::Stripe::Subscription
+ .stubs(:update)
+ .with(
+ checkout_session_completed_data[:object][:subscription],
+ { metadata: { user_id: user.id, username: user.username } },
+ )
+ .returns(
+ {
+ id: checkout_session_completed_data[:object][:subscription],
+ object: "subscription",
+ metadata: {
+ user_id: user.id.to_s,
+ username: user.username,
+ },
+ },
+ )
+
+ ::Stripe::Webhook.stubs(:construct_event).returns(event)
+ end
+
+ it "is successfull" do
+ post "/s/hooks.json"
+ expect(response.status).to eq 200
+ end
+
+ describe "completing the subscription" do
+ it "adds the user to the group when completing the transaction" do
+ expect { post "/s/hooks.json" }.to change { user.groups.count }.by(1)
+
+ expect(response.status).to eq 200
+ end
+ end
+ end
+
+ describe "checkout.session.completed with bad data" do
+ before do
+ event = { type: "checkout.session.completed", data: checkout_session_completed_bad_data }
+ ::Stripe::Checkout::Session
+ .stubs(:list_line_items)
+ .with(checkout_session_completed_data[:object][:id], { limit: 1 })
+ .returns(list_line_items_data)
+
+ ::Stripe::Webhook.stubs(:construct_event).returns(event)
+ end
+
+ it "is returns 422" do
+ post "/s/hooks.json"
+ expect(response.status).to eq 422
+ end
+ end
+
describe "customer.subscription.updated" do
before do
event = { type: "customer.subscription.updated", data: event_data }
diff --git a/spec/system/pricing_table_spec.rb b/spec/system/pricing_table_spec.rb
new file mode 100644
index 0000000..1e2b34a
--- /dev/null
+++ b/spec/system/pricing_table_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+RSpec.describe "Pricing Table", type: :system, js: true do
+ fab!(:admin)
+ fab!(:product) { Fabricate(:product, external_id: "prod_OiK") }
+ let(:dialog) { PageObjects::Components::Dialog.new }
+ let(:product_subscriptions_page) { PageObjects::Pages::AdminSubscriptionProduct.new }
+
+ before do
+ sign_in(admin)
+ SiteSetting.discourse_subscriptions_enabled = true
+ SiteSetting.discourse_subscriptions_extra_nav_subscribe = true
+
+ SiteSetting.discourse_subscriptions_secret_key = "sk_test_51xuu"
+ SiteSetting.discourse_subscriptions_public_key = "pk_test_51xuu"
+
+ SiteSetting.discourse_subscriptions_pricing_table_enabled = true
+
+ # this needs to be stubbed or it will try to make a request to stripe
+ one_product = {
+ id: "prod_OiK",
+ active: true,
+ name: "Tomtom",
+ metadata: {
+ description: "Photos of tomtom",
+ repurchaseable: true,
+ },
+ }
+ ::Stripe::Product.stubs(:list).returns({ data: [one_product] })
+ ::Stripe::Product.stubs(:delete).returns({ id: "prod_OiK" })
+ ::Stripe::Product.stubs(:retrieve).returns(one_product)
+ ::Stripe::Price.stubs(:list).returns({ data: [] })
+ end
+
+ it "Links to the pricing table page" do
+ visit("/")
+
+ link = find("li.nav-item_subscribe a")
+ uri = URI.parse(link[:href])
+ expect(uri.path).to eq("/s/subscriptions")
+ end
+
+ it "Links to the old page when disabled" do
+ SiteSetting.discourse_subscriptions_pricing_table_enabled = false
+ visit("/")
+
+ link = find("li.nav-item_subscribe a")
+ uri = URI.parse(link[:href])
+ expect(uri.path).to eq("/s")
+ end
+
+ it "Old subscribe page still works when disabled" do
+ SiteSetting.discourse_subscriptions_pricing_table_enabled = false
+ visit("/")
+
+ find("li.nav-item_subscribe a").click
+ expect(page).to have_selector("div.title-wrapper h1", text: "Subscribe")
+ end
+
+ it "Shows a message when not setup yet" do
+ visit("/")
+
+ find("li.nav-item_subscribe a").click
+
+ expect(page).to have_selector(
+ "div.container",
+ text: "There are currently no products available.",
+ )
+ end
+
+ # Commenting out for now, not sure how to stub network reqeusts made in the browser to stripe
+ # it "Shows a pricing table when setup" do
+ # SiteSetting.discourse_subscriptions_pricing_table = '{"insert-pricing-table-embed-code"}'
+
+ # visit("/")
+ # find("li.nav-item_subscribe a").click
+
+ # expect(page).to have_selector('stripe-pricing-table')
+ # end
+end