FEATURE: Add new stripe based pricing table (#202)
This commit adds an optional new stripe based pricing table. If the user is logged in, the email field will be prepopulated with the users email. The pricing table can be configured in the stripe dashboard. Once the discourse_subscriptions_pricing_table setting is filled with the pricing table embed code from the stripe dashboard, the pricing table will be displayed on /s/subscriptions For more details see https://stripe.com/docs/payments/checkout/pricing-table --------- Co-authored-by: spirobel <spirobel@protonmail.com>
This commit is contained in:
parent
dcde03d7c4
commit
45754baa00
|
@ -9,6 +9,7 @@ module DiscourseSubscriptions
|
||||||
|
|
||||||
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]
|
||||||
|
@ -27,6 +28,40 @@ module DiscourseSubscriptions
|
||||||
end
|
end
|
||||||
|
|
||||||
case event[:type]
|
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.created"
|
||||||
when "customer.subscription.updated"
|
when "customer.subscription.updated"
|
||||||
customer =
|
customer =
|
||||||
|
|
|
@ -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
|
|
@ -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(`<stripe-pricing-table
|
||||||
|
pricing-table-id="${pricingTableId}"
|
||||||
|
publishable-key="${publishableKey}"
|
||||||
|
customer-email="${this.email}"></stripe-pricing-table>`);
|
||||||
|
} else {
|
||||||
|
return htmlSafe(`<stripe-pricing-table
|
||||||
|
pricing-table-id="${pricingTableId}"
|
||||||
|
publishable-key="${publishableKey}"
|
||||||
|
></stripe-pricing-table>`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return I18n.t("discourse_subscriptions.subscribe.no_products");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
|
@ -8,11 +8,14 @@ export default {
|
||||||
const siteSettings = container.lookup("service:site-settings");
|
const siteSettings = container.lookup("service:site-settings");
|
||||||
const isNavLinkEnabled =
|
const isNavLinkEnabled =
|
||||||
siteSettings.discourse_subscriptions_extra_nav_subscribe;
|
siteSettings.discourse_subscriptions_extra_nav_subscribe;
|
||||||
|
const isPricingTableEnabled =
|
||||||
|
siteSettings.discourse_subscriptions_pricing_table_enabled;
|
||||||
|
const subscribeHref = isPricingTableEnabled ? "/s/subscriptions" : "/s";
|
||||||
if (isNavLinkEnabled) {
|
if (isNavLinkEnabled) {
|
||||||
api.addNavigationBarItem({
|
api.addNavigationBarItem({
|
||||||
name: "subscribe",
|
name: "subscribe",
|
||||||
displayName: I18n.t("discourse_subscriptions.navigation.subscribe"),
|
displayName: I18n.t("discourse_subscriptions.navigation.subscribe"),
|
||||||
href: "/s",
|
href: subscribeHref,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export default function () {
|
export default function () {
|
||||||
|
this.route("subscriptions", { path: "/s/subscriptions" });
|
||||||
this.route("subscribe", { path: "/s" }, function () {
|
this.route("subscribe", { path: "/s" }, function () {
|
||||||
this.route("show", { path: "/:subscription-id" });
|
this.route("show", { path: "/:subscription-id" });
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="container">
|
||||||
|
{{pricingTable}}
|
||||||
|
</div>
|
|
@ -7,6 +7,8 @@ en:
|
||||||
site_settings:
|
site_settings:
|
||||||
discourse_subscriptions_enabled: Enable the Discourse Subscriptions plugin.
|
discourse_subscriptions_enabled: Enable the Discourse Subscriptions plugin.
|
||||||
discourse_subscriptions_extra_nav_subscribe: Show the subscribe button in the primary navigation
|
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_public_key: Stripe Publishable Key
|
||||||
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
|
||||||
|
|
|
@ -4,6 +4,12 @@ discourse_subscriptions:
|
||||||
discourse_subscriptions_extra_nav_subscribe:
|
discourse_subscriptions_extra_nav_subscribe:
|
||||||
default: false
|
default: false
|
||||||
client: true
|
client: true
|
||||||
|
discourse_subscriptions_pricing_table_id:
|
||||||
|
default: ''
|
||||||
|
client: true
|
||||||
|
discourse_subscriptions_pricing_table_enabled:
|
||||||
|
default: false
|
||||||
|
client: true
|
||||||
discourse_subscriptions_public_key:
|
discourse_subscriptions_public_key:
|
||||||
default: ''
|
default: ''
|
||||||
client: true
|
client: true
|
||||||
|
|
|
@ -22,6 +22,10 @@ register_html_builder("server:before-head-close") do |controller|
|
||||||
"<script src='https://js.stripe.com/v3/' nonce='#{controller.helpers.csp_nonce_placeholder}'></script>"
|
"<script src='https://js.stripe.com/v3/' nonce='#{controller.helpers.csp_nonce_placeholder}'></script>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
register_html_builder("server:before-head-close") do |controller|
|
||||||
|
"<script async src='https://js.stripe.com/v3/pricing-table.js' nonce='#{controller.helpers.csp_nonce_placeholder}'></script>"
|
||||||
|
end
|
||||||
|
|
||||||
extend_content_security_policy(script_src: %w[https://js.stripe.com/v3/ https://hooks.stripe.com])
|
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"
|
add_admin_route "discourse_subscriptions.admin_navigation", "discourse-subscriptions.products"
|
||||||
|
|
|
@ -43,6 +43,128 @@ RSpec.describe DiscourseSubscriptions::HooksController do
|
||||||
}
|
}
|
||||||
end
|
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
|
describe "customer.subscription.updated" do
|
||||||
before do
|
before do
|
||||||
event = { type: "customer.subscription.updated", data: event_data }
|
event = { type: "customer.subscription.updated", data: event_data }
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue