From b00c1a427d4ae68ec1113e4352cee0fba9229fb3 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 4 Jan 2024 14:45:51 +0100 Subject: [PATCH 1/5] Add x-discourse-subscriptions into discourse-subscription-server --- .../user_authorizations_controller.rb | 11 ++ .../components/create-coupon-form.hbs | 47 +++++ .../modal/subscription-invoices.hbs | 22 +++ .../components/modal/subscription-invoices.js | 6 + .../discourse/components/payment-plan.hbs | 12 ++ .../discourse/components/product-item.hbs | 37 ++++ .../components/subscription-data.gjs | 21 +++ .../components/subscription-domain.gjs | 31 ++++ .../components/subscription-domains.gjs | 40 ++++ .../components/subscription-invoices-btn.gjs | 23 +++ .../components/subscriptions-banner.hbs | 1 + .../components/subscriptions-banner.js | 16 ++ .../subscriptions-banner-container.hbs | 1 + .../subscription-server-initializer.js | 106 +++++++++++ assets/javascripts/discourse/lib/products.js | 34 ++++ .../discourse/routes/subscribe-alias.js | 7 + .../routes/user-billing-authorizations.js | 9 + .../discourse/subscriptions-route-map.js | 3 + ...-discourse-subscriptions-products-show.hbs | 175 ++++++++++++++++++ .../discourse/templates/user/billing.hbs | 29 +++ .../templates/user/billing/authorizations.hbs | 13 ++ .../discourse/user-subscriptions-route-map.js | 13 ++ assets/stylesheets/common/common.scss | 73 ++++++++ config/locales/client.en.yml | 31 +++- config/routes.rb | 2 + config/settings.yml | 15 ++ coverage/.last_run.json | 5 - ...scriptions_coupons_controller_extension.rb | 29 +++ ...criptions_products_controller_extension.rb | 31 ++++ ...iptions_subscriber_controller_extension.rb | 22 +++ plugin.rb | 42 +++++ .../subscription_server/user_spec.rb | 9 + .../user_authorizations_controller_spec.rb | 41 ++++ 33 files changed, 951 insertions(+), 6 deletions(-) create mode 100644 app/controllers/subscription_server/user_authorizations_controller.rb create mode 100644 assets/javascripts/discourse/components/create-coupon-form.hbs create mode 100644 assets/javascripts/discourse/components/modal/subscription-invoices.hbs create mode 100644 assets/javascripts/discourse/components/modal/subscription-invoices.js create mode 100644 assets/javascripts/discourse/components/payment-plan.hbs create mode 100644 assets/javascripts/discourse/components/product-item.hbs create mode 100644 assets/javascripts/discourse/components/subscription-data.gjs create mode 100644 assets/javascripts/discourse/components/subscription-domain.gjs create mode 100644 assets/javascripts/discourse/components/subscription-domains.gjs create mode 100644 assets/javascripts/discourse/components/subscription-invoices-btn.gjs create mode 100644 assets/javascripts/discourse/components/subscriptions-banner.hbs create mode 100644 assets/javascripts/discourse/components/subscriptions-banner.js create mode 100644 assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs create mode 100644 assets/javascripts/discourse/initializers/subscription-server-initializer.js create mode 100644 assets/javascripts/discourse/lib/products.js create mode 100644 assets/javascripts/discourse/routes/subscribe-alias.js create mode 100644 assets/javascripts/discourse/routes/user-billing-authorizations.js create mode 100644 assets/javascripts/discourse/subscriptions-route-map.js create mode 100644 assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs create mode 100644 assets/javascripts/discourse/templates/user/billing.hbs create mode 100644 assets/javascripts/discourse/templates/user/billing/authorizations.hbs create mode 100644 assets/javascripts/discourse/user-subscriptions-route-map.js create mode 100644 assets/stylesheets/common/common.scss delete mode 100644 coverage/.last_run.json create mode 100644 extensions/discourse_subscriptions_coupons_controller_extension.rb create mode 100644 extensions/discourse_subscriptions_products_controller_extension.rb create mode 100644 extensions/discourse_subscriptions_subscriber_controller_extension.rb create mode 100644 spec/requests/subscription_server/user_authorizations_controller_spec.rb diff --git a/app/controllers/subscription_server/user_authorizations_controller.rb b/app/controllers/subscription_server/user_authorizations_controller.rb new file mode 100644 index 0000000..0baed79 --- /dev/null +++ b/app/controllers/subscription_server/user_authorizations_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SubscriptionServer::UserAuthorizationsController < ApplicationController + before_action :ensure_logged_in + + def destroy + params.require(:domain) + current_user.remove_subscription_domain(params[:domain]) + render json: success_json + end +end diff --git a/assets/javascripts/discourse/components/create-coupon-form.hbs b/assets/javascripts/discourse/components/create-coupon-form.hbs new file mode 100644 index 0000000..741bca3 --- /dev/null +++ b/assets/javascripts/discourse/components/create-coupon-form.hbs @@ -0,0 +1,47 @@ +
+
+

+ + {{input type="text" name="promo_code" value=promoCode}} +

+

+ + {{combo-box + content=products + value=productId + onChange=(action (mut productId)) + options=(hash + maximum=1 + ) + }} +

+

+ + {{combo-box + content=discountTypes + value=discountType + onChange=(action (mut discountType)) + }} + {{input class="discount-amount" type="text" name="amount" value=discount}} +

+

+ + {{input type="checkbox" name="active" checked=active}} +

+
+ + {{d-button + action=(action "createNewCoupon") + label="discourse_subscriptions.admin.coupons.create" + title="discourse_subscriptions.admin.coupons.create" + icon="plus" + class="btn-primary btn btn-icon"}} + {{d-button + action=(action "cancelCreate") + label="cancel" + title="cancel" + icon="times" + class="btn btn-icon"}} +
diff --git a/assets/javascripts/discourse/components/modal/subscription-invoices.hbs b/assets/javascripts/discourse/components/modal/subscription-invoices.hbs new file mode 100644 index 0000000..bae5c6a --- /dev/null +++ b/assets/javascripts/discourse/components/modal/subscription-invoices.hbs @@ -0,0 +1,22 @@ + + <:body> + {{i18n "discourse_subscriptions.user.invoices.modal.description" email=this.currentUser.email}} + + <:footer> + + {{d-icon "external-link-alt"}} + + {{i18n "discourse_subscriptions.user.invoices.modal.btn"}} + + + + + \ No newline at end of file diff --git a/assets/javascripts/discourse/components/modal/subscription-invoices.js b/assets/javascripts/discourse/components/modal/subscription-invoices.js new file mode 100644 index 0000000..108c548 --- /dev/null +++ b/assets/javascripts/discourse/components/modal/subscription-invoices.js @@ -0,0 +1,6 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; + +export default class SubscriptionInvoices extends Component { + @service currentUser; +} diff --git a/assets/javascripts/discourse/components/payment-plan.hbs b/assets/javascripts/discourse/components/payment-plan.hbs new file mode 100644 index 0000000..873fa73 --- /dev/null +++ b/assets/javascripts/discourse/components/payment-plan.hbs @@ -0,0 +1,12 @@ +
+

+ {{#if recurringPlan}} + {{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.recurring.interval)}} + {{else}} + {{i18n "discourse_subscriptions.one_time_payment"}} + {{/if}} +

+
+ + {{format-currency plan.currency plan.amountDollars}} + \ No newline at end of file diff --git a/assets/javascripts/discourse/components/product-item.hbs b/assets/javascripts/discourse/components/product-item.hbs new file mode 100644 index 0000000..33fba67 --- /dev/null +++ b/assets/javascripts/discourse/components/product-item.hbs @@ -0,0 +1,37 @@ +

{{product.name}}

+ +

+ {{html-safe product.description}} +

+ +{{#if isLoggedIn}} +
+ {{#if product.custom}} + + {{product.btnLabel}} + + {{else}} + {{#if product.repurchaseable}} + {{#link-to "subscribe.show" product.id class="btn btn-primary"}} + {{i18n "discourse_subscriptions.subscribe.title"}} + {{/link-to}} + {{#if product.subscribed}} + {{#link-to "user.billing.subscriptions" currentUser.username class="billing-link"}} + {{i18n "discourse_subscriptions.subscribe.view_past"}} + {{/link-to}} + {{/if}} + {{else}} + {{#if product.subscribed}} + ✓ {{i18n "discourse_subscriptions.subscribe.purchased"}} + {{#link-to "user.billing.subscriptions" currentUser.username class="billing-link"}} + {{i18n "discourse_subscriptions.subscribe.go_to_billing"}} + {{/link-to}} + {{else}} + {{#link-to "subscribe.show" product.id disabled=product.subscribed class="btn btn-primary"}} + {{i18n "discourse_subscriptions.subscribe.title"}} + {{/link-to}} + {{/if}} + {{/if}} + {{/if}} +
+{{/if}} diff --git a/assets/javascripts/discourse/components/subscription-data.gjs b/assets/javascripts/discourse/components/subscription-data.gjs new file mode 100644 index 0000000..261f07d --- /dev/null +++ b/assets/javascripts/discourse/components/subscription-data.gjs @@ -0,0 +1,21 @@ +import Component from "@glimmer/component"; +import SubscriptionDomains from "./subscription-domains"; + +export default class SubscriptionDomain extends Component { + +} diff --git a/assets/javascripts/discourse/components/subscription-domain.gjs b/assets/javascripts/discourse/components/subscription-domain.gjs new file mode 100644 index 0000000..2f6392e --- /dev/null +++ b/assets/javascripts/discourse/components/subscription-domain.gjs @@ -0,0 +1,31 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; +import { tracked } from "@glimmer/tracking"; + +export default class SubscriptionDomain extends Component { + @tracked removing; + + @action + removeDomain() { + this.removing = true; + this.args.remove(this.args.domain) + .finally(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } + this.removing = false; + }) + } + + +} \ No newline at end of file diff --git a/assets/javascripts/discourse/components/subscription-domains.gjs b/assets/javascripts/discourse/components/subscription-domains.gjs new file mode 100644 index 0000000..27c5038 --- /dev/null +++ b/assets/javascripts/discourse/components/subscription-domains.gjs @@ -0,0 +1,40 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; +import { tracked } from "@glimmer/tracking"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ajax } from "discourse/lib/ajax"; +import SubscriptionDomain from "./subscription-domain"; + +export default class SubscriptionDomains extends Component { + @tracked domains; + + constructor() { + super(...arguments); + this.domains = this.args.domains; + } + + @action + removeDomain(domain) { + return ajax('/subscription-server/user-authorizations', { + type: 'DELETE', + data: { + domain + } + }) + .catch(popupAjaxError) + .then(() => { + this.domains = this.domains.filter((d) => (d !== domain)); + }); + } + + +} \ No newline at end of file diff --git a/assets/javascripts/discourse/components/subscription-invoices-btn.gjs b/assets/javascripts/discourse/components/subscription-invoices-btn.gjs new file mode 100644 index 0000000..b32fba7 --- /dev/null +++ b/assets/javascripts/discourse/components/subscription-invoices-btn.gjs @@ -0,0 +1,23 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import SubscriptionInvoicesModal from "./modal/subscription-invoices"; + +export default class SubscriptionInvoicesBtn extends Component { + @service modal; + + @action + showModal() { + this.modal.show(SubscriptionInvoicesModal, { model: this.args }); + } + + +} diff --git a/assets/javascripts/discourse/components/subscriptions-banner.hbs b/assets/javascripts/discourse/components/subscriptions-banner.hbs new file mode 100644 index 0000000..8d15644 --- /dev/null +++ b/assets/javascripts/discourse/components/subscriptions-banner.hbs @@ -0,0 +1 @@ +{{text}} diff --git a/assets/javascripts/discourse/components/subscriptions-banner.js b/assets/javascripts/discourse/components/subscriptions-banner.js new file mode 100644 index 0000000..a6eaeb5 --- /dev/null +++ b/assets/javascripts/discourse/components/subscriptions-banner.js @@ -0,0 +1,16 @@ +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNameBindings: [":subscriptions-banner", "showBanner:visible"], + + @discourseComputed("currentPath", "text") + showBanner(currentPath, text) { + return currentPath.includes('subscribe') && text && text.length > 2; + }, + + @discourseComputed() + text() { + return this.siteSettings.custom_wizard_subscription_banner; + } +}); diff --git a/assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs b/assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs new file mode 100644 index 0000000..b4f64b0 --- /dev/null +++ b/assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs @@ -0,0 +1 @@ +{{subscriptions-banner currentPath=currentPath}} diff --git a/assets/javascripts/discourse/initializers/subscription-server-initializer.js b/assets/javascripts/discourse/initializers/subscription-server-initializer.js new file mode 100644 index 0000000..61a6746 --- /dev/null +++ b/assets/javascripts/discourse/initializers/subscription-server-initializer.js @@ -0,0 +1,106 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; +import discourseComputed from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ajax } from "discourse/lib/ajax"; +import { customProducts, productOrder } from '../lib/products'; + +export default { + name: 'subscription-servier-initializer', + initialize() { + withPluginApi('0.8.30', api => { + api.modifyClass('component:payment-plan', { + pluginId: 'discourse-subscription-server', + classNameBindings: [':btn-pavilion-subscribe', 'selectedClass'], + tagName: "div", + + click() { + this.clickPlan(this.plan); + } + }); + + api.modifyClass('route:admin-plugins-discourse-subscriptions-coupons', { + pluginId: 'discourse-subscription-server', + + afterModel() { + const AdminProduct = requirejs("discourse/plugins/discourse-subscriptions/discourse/models/admin-product").default; + return AdminProduct.findAll().then(products => { + this.set('products', products); + }); + }, + + setupController(controller, model) { + controller.setProperties({ + model, + products: this.products + }) + } + }); + + api.modifyClass('controller:admin-plugins-discourse-subscriptions-coupons', { + pluginId: 'discourse-subscription-server', + + actions: { + createNewCoupon(params) { + const data = { + promo: params.promo, + discount_type: params.discount_type, + discount: params.discount, + active: params.active, + applies_to_products: params.applies_to_products + }; + + return ajax("/s/admin/coupons", { + method: "post", + data, + }) + .then(() => { + this.send("closeCreateForm"); + this.send("reloadModel"); + }) + .catch(popupAjaxError); + } + } + }); + + const couponController = api._lookupContainer('controller:admin-plugins-discourse-subscriptions-coupons'); + api.modifyClass('component:create-coupon-form', { + pluginId: 'discourse-subscription-server', + + @discourseComputed + products() { + return couponController.get('products'); + }, + + actions: { + createNewCoupon() { + const createParams = { + promo: this.promoCode, + discount_type: this.discountType, + discount: this.discount, + active: this.active, + applies_to_products: [this.productId] + }; + this.create(createParams); + }, + }, + }); + + api.modifyClass('route:subscribe-index', { + pluginId: 'discourse-subscription-server', + + setupController(controller, model) { + const stripeProducts = model; + const Product = requirejs("discourse/plugins/discourse-subscriptions/discourse/models/product").default; + const nonStripeProducts = customProducts().map((product) => Product.create(product)); + const products = stripeProducts + .concat(nonStripeProducts) + .filter(p => (!p.hidden)) + .sort(function(a,b) { + return productOrder.indexOf(a.name) - productOrder.indexOf(b.name); + }); + controller.set('model', products); + } + }); + }) + } +} diff --git a/assets/javascripts/discourse/lib/products.js b/assets/javascripts/discourse/lib/products.js new file mode 100644 index 0000000..090f0e6 --- /dev/null +++ b/assets/javascripts/discourse/lib/products.js @@ -0,0 +1,34 @@ +import { helperContext } from "discourse-common/lib/helpers"; + +const customProducts = () => { + const siteSettings = helperContext().siteSettings; + + return [ + { + name: 'Custom Wizard Community', + description: `

${siteSettings.custom_wizard_community_subscription_description}

`, + btnLabel: 'Apply', + btnHref: siteSettings.custom_wizard_community_subscription_href, + custom: true, + }, + { + name: 'Custom Wizard Enterprise', + description: `

${siteSettings.custom_wizard_enterprise_subscription_description}

`, + btnLabel: 'Contact Us', + btnHref: siteSettings.custom_wizard_enterprise_subscription_href, + custom: true, + } + ]; +} + +const productOrder = [ + 'Custom Wizard Community', + 'Custom Wizard Small Business', + 'Custom Wizard Business', + 'Custom Wizard Enterprise' +]; + +export { + customProducts, + productOrder +} diff --git a/assets/javascripts/discourse/routes/subscribe-alias.js b/assets/javascripts/discourse/routes/subscribe-alias.js new file mode 100644 index 0000000..8642190 --- /dev/null +++ b/assets/javascripts/discourse/routes/subscribe-alias.js @@ -0,0 +1,7 @@ +import Route from "@ember/routing/route"; + +export default Route.extend({ + afterModel() { + return this.replaceWith("subscribe"); + } +}); diff --git a/assets/javascripts/discourse/routes/user-billing-authorizations.js b/assets/javascripts/discourse/routes/user-billing-authorizations.js new file mode 100644 index 0000000..fefdd59 --- /dev/null +++ b/assets/javascripts/discourse/routes/user-billing-authorizations.js @@ -0,0 +1,9 @@ +import Route from "@ember/routing/route"; + +export default Route.extend({ + templateName: "user/billing/authorizations", + + model() { + return this.modelFor('user'); + } +}); diff --git a/assets/javascripts/discourse/subscriptions-route-map.js b/assets/javascripts/discourse/subscriptions-route-map.js new file mode 100644 index 0000000..0227b0a --- /dev/null +++ b/assets/javascripts/discourse/subscriptions-route-map.js @@ -0,0 +1,3 @@ +export default function () { + this.route("subscribeAlias", { path: "/subscribe" }); +} diff --git a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs new file mode 100644 index 0000000..8883530 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs @@ -0,0 +1,175 @@ +

{{i18n "discourse_subscriptions.admin.products.title"}}

+ +
+

+ + {{input type="text" name="name" value=model.product.name}} +

+ +

+ + + {{textarea + name="description" + value=model.product.metadata.description + class="discourse-subscriptions-admin-textarea" + }} + +

+ {{i18n "discourse_subscriptions.admin.products.product.description_help"}} +
+

+ +

+ + + {{input + type="text" + name="statement_descriptor" + value=model.product.statement_descriptor + }} + +

+ {{i18n + "discourse_subscriptions.admin.products.product.statement_descriptor_help" + }} +
+

+ +

+ + + {{input + type="checkbox" + name="repurchaseable" + checked=model.product.metadata.repurchaseable + }} + +

+ {{i18n "discourse_subscriptions.admin.products.product.repurchase_help"}} +
+

+ +

+ + + {{input + type="checkbox" + name="hidden" + checked=model.product.metadata.hidden + }} + +

+ {{i18n "discourse_subscriptions.admin.products.product.hidden_help"}} +
+

+ +

+ + + {{input type="checkbox" name="active" checked=model.product.active}} + +

+ {{i18n "discourse_subscriptions.admin.products.product.active_help"}} +
+

+
+ +{{#unless model.product.isNew}} +

{{i18n "discourse_subscriptions.admin.plans.title"}}

+ +

+ + + + + + + + + + + + + {{#each model.plans as |plan|}} + + + + + + + + + + {{else}} + + + + {{/each}} + +
{{i18n "discourse_subscriptions.admin.plans.plan.nickname"}}{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}{{i18n "discourse_subscriptions.admin.plans.plan.created_at"}}{{i18n "discourse_subscriptions.admin.plans.plan.group"}}{{i18n "discourse_subscriptions.admin.plans.plan.active"}} + {{i18n "discourse_subscriptions.admin.plans.plan.amount"}} + + {{#link-to + "adminPlugins.discourse-subscriptions.products.show.plans.show" + model.product.id + "new" + class="btn" + }} + {{i18n "discourse_subscriptions.admin.plans.operations.add"}} + {{/link-to}} +
{{plan.nickname}}{{plan.recurring.interval}}{{format-unix-date plan.created}}{{plan.metadata.group_name}}{{plan.active}} + {{format-currency plan.currency plan.amountDollars}} + + {{#link-to + "adminPlugins.discourse-subscriptions.products.show.plans.show" + model.product.id + plan.id + class="btn no-text btn-icon" + }} + {{d-icon "far-edit"}} + {{/link-to}} +
+
+ {{i18n + "discourse_subscriptions.admin.products.product.plan_help" + }} +
+

+{{/unless}} + +
+ {{d-button label="cancel" action=(action "cancelProduct") icon="times"}} + + {{#if model.product.isNew}} + {{d-button + label="discourse_subscriptions.admin.products.operations.create" + action=(action "createProduct") + icon="plus" + class="btn btn-primary" + }} + {{else}} + {{d-button + label="discourse_subscriptions.admin.products.operations.update" + action=(action "updateProduct") + icon="check" + class="btn btn-primary" + }} + {{/if}} +
+ +{{outlet}} diff --git a/assets/javascripts/discourse/templates/user/billing.hbs b/assets/javascripts/discourse/templates/user/billing.hbs new file mode 100644 index 0000000..36241a1 --- /dev/null +++ b/assets/javascripts/discourse/templates/user/billing.hbs @@ -0,0 +1,29 @@ +{{#d-section + pageClass="user-billing" + class="user-secondary-navigation" + scrollTop="false" +}} +
+ + + + +
  • + +
  • +
    +
    +{{/d-section}} + +
    + {{outlet}} +
    diff --git a/assets/javascripts/discourse/templates/user/billing/authorizations.hbs b/assets/javascripts/discourse/templates/user/billing/authorizations.hbs new file mode 100644 index 0000000..75ad03d --- /dev/null +++ b/assets/javascripts/discourse/templates/user/billing/authorizations.hbs @@ -0,0 +1,13 @@ + + + + + + + + + {{#each model.subscription_domains as |data|}} + + {{/each}} + +
    {{i18n "discourse_subscriptions.user.authorizations.resource"}}{{i18n "discourse_subscriptions.user.authorizations.products"}}{{i18n "discourse_subscriptions.user.authorizations.domains"}}{{i18n "discourse_subscriptions.user.authorizations.domain_limit"}}
    diff --git a/assets/javascripts/discourse/user-subscriptions-route-map.js b/assets/javascripts/discourse/user-subscriptions-route-map.js new file mode 100644 index 0000000..47fc38c --- /dev/null +++ b/assets/javascripts/discourse/user-subscriptions-route-map.js @@ -0,0 +1,13 @@ +export default { + resource: "user", + path: "users/:username", + map() { + this.route("billing", function () { + this.route("payments"); + this.route("authorizations"); + this.route("subscriptions", function () { + this.route("card", { path: "/card/:stripe-subscription-id" }); + }); + }); + }, +}; diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss new file mode 100644 index 0000000..1ca0a3d --- /dev/null +++ b/assets/stylesheets/common/common.scss @@ -0,0 +1,73 @@ +.btn-pavilion-subscribe { + border: 2px solid var(--tertiary); + padding: 2em; + margin-right: 2em; + cursor: pointer; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + line-height: unset; + + &.selected { + background-color: var(--tertiary); + color: var(--secondary); + } +} + +.discourse-subscriptions-section-columns { + margin: 20px 0; + padding: 20px 0; +} + +.product-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(48.5%, 1fr)); + grid-gap: 2em; + margin: 2em 0 4em 0; + + .product { + margin: 0; + width: unset; + } +} + +.subscriptions-banner { + display: none; + padding: 1rem; + margin-bottom: 1rem; + background-color: rgba(255, 200, 0, 0.25); + + &.visible { + display: block; + } +} + +.subscription-data { + ul { + list-style: none; + margin: 0; + } + td { + vertical-align: top; + } +} + +.subscription-domain { + display: flex; + gap: 1em; + align-items: center; + + .remove-domain { + background-color: unset; + } +} + +.discourse-subscriptions-invoices-btn { + margin-left: auto; + + .d-button-label { + color: var(--secondary) !important; + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4334ed8..73635f9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1,8 +1,37 @@ en: + site_settings: + custom_wizard_community_subscription_description: Description of the community subscription + custom_wizard_enterprise_subscription_description: Description of the enterprise subscription + custom_wizard_community_subscription_href: Community subscription href + custom_wizard_enterprise_subscription_href: Enterprise subscription href + custom_wizard_subscription_banner: Notice to show on subscription routes js: discourse_subscriptions: admin: products: product: plugin_name: Plugin Name - plugin_name_help: Name of plugin subscription is for. If filled, will generate API Key with subscription. \ No newline at end of file + plugin_name_help: Name of plugin subscription is for. If filled, will generate API Key with subscription. + navigation: + authorizations: Authorizations + user: + authorizations: + resource: Resource + products: Products + domains: Domains + domain_limit: Domain Limit + remove_domain: + title: Remove domain + invoices: + btn: + label: "Invoices" + title: "Your subscription invoices" + modal: + title: Your Subscription Invoices + description: Click "Customer Portal" to go to Pavilion's Stripe Customer Portal where you can access your invoices. Sign in using %{email}. + btn: "Customer Portal" + admin: + products: + product: + hidden: Hidden + hidden_help: "Don't display the product in the subscriptions index." \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 6700d56..37441fd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,8 +4,10 @@ get '' => 'server#index', defaults: { format: 'json' } get 'user-subscriptions' => 'user_subscriptions#index', defaults: { format: 'json' } get 'messages' => 'messages#index', defaults: { format: 'json' } + delete 'user-authorizations' => 'user_authorizations#destroy', defaults: { format: 'json' } end Discourse::Application.routes.append do mount ::SubscriptionServer::Engine, at: "/subscription-server" + get '/subscribe' => 'discourse_subscriptions/subscribe#index' end diff --git a/config/settings.yml b/config/settings.yml index 2a21e8a..73ef249 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -4,3 +4,18 @@ plugins: subscription_server_subscriptions: type: list default: '' + custom_wizard_community_subscription_description: + client: true + default: 'Custom Wizard Community Subscription Description' + custom_wizard_enterprise_subscription_description: + client: true + default: 'Custom Wizard Enterprise Subscription Description' + custom_wizard_community_subscription_href: + client: true + default: 'https://coop.pavilion.tech/w/community-subscription' + custom_wizard_enterprise_subscription_href: + client: true + default: 'https://calendly.com/book-pavilion/initial-consult' + custom_wizard_subscription_banner: + client: true + default: '' \ No newline at end of file diff --git a/coverage/.last_run.json b/coverage/.last_run.json deleted file mode 100644 index dc24454..0000000 --- a/coverage/.last_run.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "result": { - "line": 97.75 - } -} diff --git a/extensions/discourse_subscriptions_coupons_controller_extension.rb b/extensions/discourse_subscriptions_coupons_controller_extension.rb new file mode 100644 index 0000000..42c2b8f --- /dev/null +++ b/extensions/discourse_subscriptions_coupons_controller_extension.rb @@ -0,0 +1,29 @@ +module DiscourseSubscriptionsCouponsControllerExtension + def create + params.require([:promo, :discount_type, :discount, :active, :applies_to_products]) + begin + coupon_params = { + duration: 'forever', + max_redemptions: params[:max_redemptions] || 1, + applies_to: { + products: params[:applies_to_products] + } + } + + case params[:discount_type] + when 'amount' + coupon_params[:amount_off] = params[:discount].to_i * 100 + coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency + when 'percent' + coupon_params[:percent_off] = params[:discount] + end + + coupon = ::Stripe::Coupon.create(coupon_params) + promo_code = ::Stripe::PromotionCode.create({ coupon: coupon[:id], code: params[:promo] }) if coupon.present? + + render_json_dump promo_code + rescue ::Stripe::InvalidRequestError => e + render_json_error e.message + end + end +end \ No newline at end of file diff --git a/extensions/discourse_subscriptions_products_controller_extension.rb b/extensions/discourse_subscriptions_products_controller_extension.rb new file mode 100644 index 0000000..d56e1c3 --- /dev/null +++ b/extensions/discourse_subscriptions_products_controller_extension.rb @@ -0,0 +1,31 @@ +module DiscourseSubscriptionsProductsControllerExtension + def show + begin + product = ::Stripe::Product.retrieve(params[:id]) + + if product[:metadata][:hidden].present? + product[:metadata][:hidden] = ActiveRecord::Type::Boolean.new.cast(product[:metadata][:hidden]) + end + + render_json_dump product + + rescue ::Stripe::InvalidRequestError => e + render_json_error e.message + end + end + + private def product_params + params.permit! + + { + name: params[:name], + active: params[:active], + statement_descriptor: params[:statement_descriptor], + metadata: { + description: params.dig(:metadata, :description), + repurchaseable: params.dig(:metadata, :repurchaseable), + hidden: params.dig(:metadata, :hidden) + } + } + end +end \ No newline at end of file diff --git a/extensions/discourse_subscriptions_subscriber_controller_extension.rb b/extensions/discourse_subscriptions_subscriber_controller_extension.rb new file mode 100644 index 0000000..e8cdd9e --- /dev/null +++ b/extensions/discourse_subscriptions_subscriber_controller_extension.rb @@ -0,0 +1,22 @@ +module DiscourseSubscriptionsSubscribeControllerExtension + private def serialize_product(product) + { + id: product[:id], + name: product[:name], + description: PrettyText.cook(product[:metadata][:description]), + subscribed: current_user_products.include?(product[:id]), + repurchaseable: product[:metadata][:repurchaseable], + hidden: ActiveRecord::Type::Boolean.new.cast(product[:metadata][:hidden]) + } + end + + private def serialize_plans(plans) + plans[:data].reduce([]) do |result, p| + plan = p.to_h + if plan[:nickname] != "hidden" + result << plan.slice(:id, :unit_amount, :currency, :type, :recurring, :nickname) + end + result + end.sort_by { |plan| plan[:amount] } + end +end \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index 7c94329..a77a944 100644 --- a/plugin.rb +++ b/plugin.rb @@ -8,6 +8,7 @@ # contact_emails: development@pavilion.tech enabled_site_setting :subscription_server_enabled +register_asset "stylesheets/common/common.scss" after_initialize do %w[ @@ -21,10 +22,14 @@ ../lib/subscription_server/extensions/user_api_keys_controller.rb ../config/routes.rb ../app/controllers/subscription_server/user_subscriptions_controller.rb + ../app/controllers/subscription_server/user_authorizations_controller.rb ../app/controllers/subscription_server/messages_controller.rb ../app/controllers/subscription_server/server_controller.rb ../app/serializers/subscription_server/message_serializer.rb ../app/serializers/subscription_server/subscription_serializer.rb + ../extensions/discourse_subscriptions_coupons_controller_extension.rb + ../extensions/discourse_subscriptions_products_controller_extension.rb + ../extensions/discourse_subscriptions_subscriber_controller_extension.rb ].each do |path| load File.expand_path(path, __FILE__) end @@ -62,6 +67,17 @@ save_custom_fields(true) end + add_to_class(:user, :remove_subscription_domain) do |domain| + self._custom_fields.where( + "name LIKE '#{SubscriptionServer::UserSubscriptions::DOMAINS_KEY_PREFIX}%'" + ).each do |field| + value_arr = field.value.split('|') + value_arr = value_arr.reject { |d| d === domain } + field.value = value_arr.join('|') + field.save! + end + end + add_to_class(:user, :subscription_product_domains) do |resource_name, provider_name, product_id| key = subscription_product_domain_key(resource_name, provider_name, product_id) product_domains = custom_fields[key] @@ -94,4 +110,30 @@ result end end + + add_to_serializer(:user, :subscription_domains) { user.subscription_domains } + + ## DiscourseSubscription extensions. discourse-subscriptions plugin must be above this plugin in app.yml + DiscourseSubscriptions::SubscribeController.prepend DiscourseSubscriptionsSubscribeControllerExtension + DiscourseSubscriptions::Admin::CouponsController.prepend DiscourseSubscriptionsCouponsControllerExtension + DiscourseSubscriptions::Admin::ProductsController.prepend DiscourseSubscriptionsProductsControllerExtension + + require 'csv' + module ::DiscourseSubscriptions + def self.class_update_subscriptions_from_csv(path) + rows = CSV.read(path) + return unless rows.present? + + rows.each do |row| + DiscourseSubscriptions::Subscription + .joins(:customer) + .where("discourse_subscriptions_customers.customer_id = :customer_id AND discourse_subscriptions_subscriptions.external_id = :subscription_id", + customer_id: row[0].strip, + subscription_id: row[1].strip + ).update_all( + external_id: row[2].strip + ) + end + end + end end diff --git a/spec/components/subscription_server/user_spec.rb b/spec/components/subscription_server/user_spec.rb index 6ad6b53..b95eb25 100644 --- a/spec/components/subscription_server/user_spec.rb +++ b/spec/components/subscription_server/user_spec.rb @@ -46,4 +46,13 @@ expect(user.subscription_domains.first[:domains]).to eq([domain]) expect(user.subscription_domains.first[:domain_limit]).to eq(1) end + + it "#remove_subscription_domain" do + user.add_subscription_product_domain(domain, resource, provider, product_id) + user.add_subscription_product_domain(domain, resource, provider, "prod_12345") + user.remove_subscription_domain(domain) + user.reload + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, product_id)]).to eq("") + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, "prod_12345")]).to eq("") + end end diff --git a/spec/requests/subscription_server/user_authorizations_controller_spec.rb b/spec/requests/subscription_server/user_authorizations_controller_spec.rb new file mode 100644 index 0000000..f9c25d4 --- /dev/null +++ b/spec/requests/subscription_server/user_authorizations_controller_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +describe SubscriptionServer::UserAuthorizationsController do + let(:user) { Fabricate(:user) } + let(:provider) { "stripe" } + let(:product_id) { "prod_CBTNpi3fqWWkq0" } + let(:product_slug) { "business" } + let(:resource) { "custom_wizard" } + let(:domain) { "demo.pavilion.tech" } + + it "requires a user" do + delete "/subscription-server/user-authorizations" + expect(response.status).to eq(403) + end + + context "with a user" do + before do + sign_in(user) + end + + describe "#destroy" do + + it "requires a domain" do + delete "/subscription-server/user-authorizations" + expect(response.status).to eq(400) + end + + it "removes the domain from all of the user's products" do + user.add_subscription_product_domain(domain, resource, provider, product_id) + user.add_subscription_product_domain(domain, resource, provider, "prod_12345") + + delete "/subscription-server/user-authorizations", params: { domain: domain } + expect(response.status).to eq(200) + + user.reload + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, product_id)]).to eq("") + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, "prod_12345")]).to eq("") + end + end + end +end From a379e33ca5963470af225ccb68fd5c73c0a32cda Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 10 Jan 2024 16:39:09 +0100 Subject: [PATCH 2/5] Move client assets to theme component --- .../components/create-coupon-form.hbs | 47 ----- .../modal/subscription-invoices.hbs | 22 --- .../components/modal/subscription-invoices.js | 6 - .../discourse/components/payment-plan.hbs | 12 -- .../discourse/components/product-item.hbs | 37 ---- .../components/subscription-data.gjs | 21 --- .../components/subscription-domain.gjs | 31 ---- .../components/subscription-domains.gjs | 40 ---- .../components/subscription-invoices-btn.gjs | 23 --- .../components/subscriptions-banner.hbs | 1 - .../components/subscriptions-banner.js | 16 -- .../subscriptions-banner-container.hbs | 1 - .../subscription-server-initializer.js | 106 ----------- assets/javascripts/discourse/lib/products.js | 34 ---- .../discourse/routes/subscribe-alias.js | 7 - .../routes/user-billing-authorizations.js | 9 - .../discourse/subscriptions-route-map.js | 3 - ...-discourse-subscriptions-products-show.hbs | 175 ------------------ .../discourse/templates/user/billing.hbs | 29 --- .../templates/user/billing/authorizations.hbs | 13 -- .../discourse/user-subscriptions-route-map.js | 13 -- assets/stylesheets/common/common.scss | 73 -------- config/locales/client.en.yml | 37 ---- config/settings.yml | 19 +- plugin.rb | 7 +- 25 files changed, 9 insertions(+), 773 deletions(-) delete mode 100644 assets/javascripts/discourse/components/create-coupon-form.hbs delete mode 100644 assets/javascripts/discourse/components/modal/subscription-invoices.hbs delete mode 100644 assets/javascripts/discourse/components/modal/subscription-invoices.js delete mode 100644 assets/javascripts/discourse/components/payment-plan.hbs delete mode 100644 assets/javascripts/discourse/components/product-item.hbs delete mode 100644 assets/javascripts/discourse/components/subscription-data.gjs delete mode 100644 assets/javascripts/discourse/components/subscription-domain.gjs delete mode 100644 assets/javascripts/discourse/components/subscription-domains.gjs delete mode 100644 assets/javascripts/discourse/components/subscription-invoices-btn.gjs delete mode 100644 assets/javascripts/discourse/components/subscriptions-banner.hbs delete mode 100644 assets/javascripts/discourse/components/subscriptions-banner.js delete mode 100644 assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs delete mode 100644 assets/javascripts/discourse/initializers/subscription-server-initializer.js delete mode 100644 assets/javascripts/discourse/lib/products.js delete mode 100644 assets/javascripts/discourse/routes/subscribe-alias.js delete mode 100644 assets/javascripts/discourse/routes/user-billing-authorizations.js delete mode 100644 assets/javascripts/discourse/subscriptions-route-map.js delete mode 100644 assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs delete mode 100644 assets/javascripts/discourse/templates/user/billing.hbs delete mode 100644 assets/javascripts/discourse/templates/user/billing/authorizations.hbs delete mode 100644 assets/javascripts/discourse/user-subscriptions-route-map.js delete mode 100644 assets/stylesheets/common/common.scss delete mode 100644 config/locales/client.en.yml diff --git a/assets/javascripts/discourse/components/create-coupon-form.hbs b/assets/javascripts/discourse/components/create-coupon-form.hbs deleted file mode 100644 index 741bca3..0000000 --- a/assets/javascripts/discourse/components/create-coupon-form.hbs +++ /dev/null @@ -1,47 +0,0 @@ -
    -
    -

    - - {{input type="text" name="promo_code" value=promoCode}} -

    -

    - - {{combo-box - content=products - value=productId - onChange=(action (mut productId)) - options=(hash - maximum=1 - ) - }} -

    -

    - - {{combo-box - content=discountTypes - value=discountType - onChange=(action (mut discountType)) - }} - {{input class="discount-amount" type="text" name="amount" value=discount}} -

    -

    - - {{input type="checkbox" name="active" checked=active}} -

    -
    - - {{d-button - action=(action "createNewCoupon") - label="discourse_subscriptions.admin.coupons.create" - title="discourse_subscriptions.admin.coupons.create" - icon="plus" - class="btn-primary btn btn-icon"}} - {{d-button - action=(action "cancelCreate") - label="cancel" - title="cancel" - icon="times" - class="btn btn-icon"}} -
    diff --git a/assets/javascripts/discourse/components/modal/subscription-invoices.hbs b/assets/javascripts/discourse/components/modal/subscription-invoices.hbs deleted file mode 100644 index bae5c6a..0000000 --- a/assets/javascripts/discourse/components/modal/subscription-invoices.hbs +++ /dev/null @@ -1,22 +0,0 @@ - - <:body> - {{i18n "discourse_subscriptions.user.invoices.modal.description" email=this.currentUser.email}} - - <:footer> - - {{d-icon "external-link-alt"}} - - {{i18n "discourse_subscriptions.user.invoices.modal.btn"}} - - - - - \ No newline at end of file diff --git a/assets/javascripts/discourse/components/modal/subscription-invoices.js b/assets/javascripts/discourse/components/modal/subscription-invoices.js deleted file mode 100644 index 108c548..0000000 --- a/assets/javascripts/discourse/components/modal/subscription-invoices.js +++ /dev/null @@ -1,6 +0,0 @@ -import Component from "@glimmer/component"; -import { inject as service } from "@ember/service"; - -export default class SubscriptionInvoices extends Component { - @service currentUser; -} diff --git a/assets/javascripts/discourse/components/payment-plan.hbs b/assets/javascripts/discourse/components/payment-plan.hbs deleted file mode 100644 index 873fa73..0000000 --- a/assets/javascripts/discourse/components/payment-plan.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
    -

    - {{#if recurringPlan}} - {{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.recurring.interval)}} - {{else}} - {{i18n "discourse_subscriptions.one_time_payment"}} - {{/if}} -

    -
    - - {{format-currency plan.currency plan.amountDollars}} - \ No newline at end of file diff --git a/assets/javascripts/discourse/components/product-item.hbs b/assets/javascripts/discourse/components/product-item.hbs deleted file mode 100644 index 33fba67..0000000 --- a/assets/javascripts/discourse/components/product-item.hbs +++ /dev/null @@ -1,37 +0,0 @@ -

    {{product.name}}

    - -

    - {{html-safe product.description}} -

    - -{{#if isLoggedIn}} -
    - {{#if product.custom}} - - {{product.btnLabel}} - - {{else}} - {{#if product.repurchaseable}} - {{#link-to "subscribe.show" product.id class="btn btn-primary"}} - {{i18n "discourse_subscriptions.subscribe.title"}} - {{/link-to}} - {{#if product.subscribed}} - {{#link-to "user.billing.subscriptions" currentUser.username class="billing-link"}} - {{i18n "discourse_subscriptions.subscribe.view_past"}} - {{/link-to}} - {{/if}} - {{else}} - {{#if product.subscribed}} - ✓ {{i18n "discourse_subscriptions.subscribe.purchased"}} - {{#link-to "user.billing.subscriptions" currentUser.username class="billing-link"}} - {{i18n "discourse_subscriptions.subscribe.go_to_billing"}} - {{/link-to}} - {{else}} - {{#link-to "subscribe.show" product.id disabled=product.subscribed class="btn btn-primary"}} - {{i18n "discourse_subscriptions.subscribe.title"}} - {{/link-to}} - {{/if}} - {{/if}} - {{/if}} -
    -{{/if}} diff --git a/assets/javascripts/discourse/components/subscription-data.gjs b/assets/javascripts/discourse/components/subscription-data.gjs deleted file mode 100644 index 261f07d..0000000 --- a/assets/javascripts/discourse/components/subscription-data.gjs +++ /dev/null @@ -1,21 +0,0 @@ -import Component from "@glimmer/component"; -import SubscriptionDomains from "./subscription-domains"; - -export default class SubscriptionDomain extends Component { - -} diff --git a/assets/javascripts/discourse/components/subscription-domain.gjs b/assets/javascripts/discourse/components/subscription-domain.gjs deleted file mode 100644 index 2f6392e..0000000 --- a/assets/javascripts/discourse/components/subscription-domain.gjs +++ /dev/null @@ -1,31 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import DButton from "discourse/components/d-button"; -import { tracked } from "@glimmer/tracking"; - -export default class SubscriptionDomain extends Component { - @tracked removing; - - @action - removeDomain() { - this.removing = true; - this.args.remove(this.args.domain) - .finally(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - this.removing = false; - }) - } - - -} \ No newline at end of file diff --git a/assets/javascripts/discourse/components/subscription-domains.gjs b/assets/javascripts/discourse/components/subscription-domains.gjs deleted file mode 100644 index 27c5038..0000000 --- a/assets/javascripts/discourse/components/subscription-domains.gjs +++ /dev/null @@ -1,40 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import DButton from "discourse/components/d-button"; -import { tracked } from "@glimmer/tracking"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { ajax } from "discourse/lib/ajax"; -import SubscriptionDomain from "./subscription-domain"; - -export default class SubscriptionDomains extends Component { - @tracked domains; - - constructor() { - super(...arguments); - this.domains = this.args.domains; - } - - @action - removeDomain(domain) { - return ajax('/subscription-server/user-authorizations', { - type: 'DELETE', - data: { - domain - } - }) - .catch(popupAjaxError) - .then(() => { - this.domains = this.domains.filter((d) => (d !== domain)); - }); - } - - -} \ No newline at end of file diff --git a/assets/javascripts/discourse/components/subscription-invoices-btn.gjs b/assets/javascripts/discourse/components/subscription-invoices-btn.gjs deleted file mode 100644 index b32fba7..0000000 --- a/assets/javascripts/discourse/components/subscription-invoices-btn.gjs +++ /dev/null @@ -1,23 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import DButton from "discourse/components/d-button"; -import SubscriptionInvoicesModal from "./modal/subscription-invoices"; - -export default class SubscriptionInvoicesBtn extends Component { - @service modal; - - @action - showModal() { - this.modal.show(SubscriptionInvoicesModal, { model: this.args }); - } - - -} diff --git a/assets/javascripts/discourse/components/subscriptions-banner.hbs b/assets/javascripts/discourse/components/subscriptions-banner.hbs deleted file mode 100644 index 8d15644..0000000 --- a/assets/javascripts/discourse/components/subscriptions-banner.hbs +++ /dev/null @@ -1 +0,0 @@ -{{text}} diff --git a/assets/javascripts/discourse/components/subscriptions-banner.js b/assets/javascripts/discourse/components/subscriptions-banner.js deleted file mode 100644 index a6eaeb5..0000000 --- a/assets/javascripts/discourse/components/subscriptions-banner.js +++ /dev/null @@ -1,16 +0,0 @@ -import Component from "@ember/component"; -import discourseComputed from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNameBindings: [":subscriptions-banner", "showBanner:visible"], - - @discourseComputed("currentPath", "text") - showBanner(currentPath, text) { - return currentPath.includes('subscribe') && text && text.length > 2; - }, - - @discourseComputed() - text() { - return this.siteSettings.custom_wizard_subscription_banner; - } -}); diff --git a/assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs b/assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs deleted file mode 100644 index b4f64b0..0000000 --- a/assets/javascripts/discourse/connectors/top-notices/subscriptions-banner-container.hbs +++ /dev/null @@ -1 +0,0 @@ -{{subscriptions-banner currentPath=currentPath}} diff --git a/assets/javascripts/discourse/initializers/subscription-server-initializer.js b/assets/javascripts/discourse/initializers/subscription-server-initializer.js deleted file mode 100644 index 61a6746..0000000 --- a/assets/javascripts/discourse/initializers/subscription-server-initializer.js +++ /dev/null @@ -1,106 +0,0 @@ -import { withPluginApi } from "discourse/lib/plugin-api"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { ajax } from "discourse/lib/ajax"; -import { customProducts, productOrder } from '../lib/products'; - -export default { - name: 'subscription-servier-initializer', - initialize() { - withPluginApi('0.8.30', api => { - api.modifyClass('component:payment-plan', { - pluginId: 'discourse-subscription-server', - classNameBindings: [':btn-pavilion-subscribe', 'selectedClass'], - tagName: "div", - - click() { - this.clickPlan(this.plan); - } - }); - - api.modifyClass('route:admin-plugins-discourse-subscriptions-coupons', { - pluginId: 'discourse-subscription-server', - - afterModel() { - const AdminProduct = requirejs("discourse/plugins/discourse-subscriptions/discourse/models/admin-product").default; - return AdminProduct.findAll().then(products => { - this.set('products', products); - }); - }, - - setupController(controller, model) { - controller.setProperties({ - model, - products: this.products - }) - } - }); - - api.modifyClass('controller:admin-plugins-discourse-subscriptions-coupons', { - pluginId: 'discourse-subscription-server', - - actions: { - createNewCoupon(params) { - const data = { - promo: params.promo, - discount_type: params.discount_type, - discount: params.discount, - active: params.active, - applies_to_products: params.applies_to_products - }; - - return ajax("/s/admin/coupons", { - method: "post", - data, - }) - .then(() => { - this.send("closeCreateForm"); - this.send("reloadModel"); - }) - .catch(popupAjaxError); - } - } - }); - - const couponController = api._lookupContainer('controller:admin-plugins-discourse-subscriptions-coupons'); - api.modifyClass('component:create-coupon-form', { - pluginId: 'discourse-subscription-server', - - @discourseComputed - products() { - return couponController.get('products'); - }, - - actions: { - createNewCoupon() { - const createParams = { - promo: this.promoCode, - discount_type: this.discountType, - discount: this.discount, - active: this.active, - applies_to_products: [this.productId] - }; - this.create(createParams); - }, - }, - }); - - api.modifyClass('route:subscribe-index', { - pluginId: 'discourse-subscription-server', - - setupController(controller, model) { - const stripeProducts = model; - const Product = requirejs("discourse/plugins/discourse-subscriptions/discourse/models/product").default; - const nonStripeProducts = customProducts().map((product) => Product.create(product)); - const products = stripeProducts - .concat(nonStripeProducts) - .filter(p => (!p.hidden)) - .sort(function(a,b) { - return productOrder.indexOf(a.name) - productOrder.indexOf(b.name); - }); - controller.set('model', products); - } - }); - }) - } -} diff --git a/assets/javascripts/discourse/lib/products.js b/assets/javascripts/discourse/lib/products.js deleted file mode 100644 index 090f0e6..0000000 --- a/assets/javascripts/discourse/lib/products.js +++ /dev/null @@ -1,34 +0,0 @@ -import { helperContext } from "discourse-common/lib/helpers"; - -const customProducts = () => { - const siteSettings = helperContext().siteSettings; - - return [ - { - name: 'Custom Wizard Community', - description: `

    ${siteSettings.custom_wizard_community_subscription_description}

    `, - btnLabel: 'Apply', - btnHref: siteSettings.custom_wizard_community_subscription_href, - custom: true, - }, - { - name: 'Custom Wizard Enterprise', - description: `

    ${siteSettings.custom_wizard_enterprise_subscription_description}

    `, - btnLabel: 'Contact Us', - btnHref: siteSettings.custom_wizard_enterprise_subscription_href, - custom: true, - } - ]; -} - -const productOrder = [ - 'Custom Wizard Community', - 'Custom Wizard Small Business', - 'Custom Wizard Business', - 'Custom Wizard Enterprise' -]; - -export { - customProducts, - productOrder -} diff --git a/assets/javascripts/discourse/routes/subscribe-alias.js b/assets/javascripts/discourse/routes/subscribe-alias.js deleted file mode 100644 index 8642190..0000000 --- a/assets/javascripts/discourse/routes/subscribe-alias.js +++ /dev/null @@ -1,7 +0,0 @@ -import Route from "@ember/routing/route"; - -export default Route.extend({ - afterModel() { - return this.replaceWith("subscribe"); - } -}); diff --git a/assets/javascripts/discourse/routes/user-billing-authorizations.js b/assets/javascripts/discourse/routes/user-billing-authorizations.js deleted file mode 100644 index fefdd59..0000000 --- a/assets/javascripts/discourse/routes/user-billing-authorizations.js +++ /dev/null @@ -1,9 +0,0 @@ -import Route from "@ember/routing/route"; - -export default Route.extend({ - templateName: "user/billing/authorizations", - - model() { - return this.modelFor('user'); - } -}); diff --git a/assets/javascripts/discourse/subscriptions-route-map.js b/assets/javascripts/discourse/subscriptions-route-map.js deleted file mode 100644 index 0227b0a..0000000 --- a/assets/javascripts/discourse/subscriptions-route-map.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function () { - this.route("subscribeAlias", { path: "/subscribe" }); -} diff --git a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs deleted file mode 100644 index 8883530..0000000 --- a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs +++ /dev/null @@ -1,175 +0,0 @@ -

    {{i18n "discourse_subscriptions.admin.products.title"}}

    - -
    -

    - - {{input type="text" name="name" value=model.product.name}} -

    - -

    - - - {{textarea - name="description" - value=model.product.metadata.description - class="discourse-subscriptions-admin-textarea" - }} - -

    - {{i18n "discourse_subscriptions.admin.products.product.description_help"}} -
    -

    - -

    - - - {{input - type="text" - name="statement_descriptor" - value=model.product.statement_descriptor - }} - -

    - {{i18n - "discourse_subscriptions.admin.products.product.statement_descriptor_help" - }} -
    -

    - -

    - - - {{input - type="checkbox" - name="repurchaseable" - checked=model.product.metadata.repurchaseable - }} - -

    - {{i18n "discourse_subscriptions.admin.products.product.repurchase_help"}} -
    -

    - -

    - - - {{input - type="checkbox" - name="hidden" - checked=model.product.metadata.hidden - }} - -

    - {{i18n "discourse_subscriptions.admin.products.product.hidden_help"}} -
    -

    - -

    - - - {{input type="checkbox" name="active" checked=model.product.active}} - -

    - {{i18n "discourse_subscriptions.admin.products.product.active_help"}} -
    -

    -
    - -{{#unless model.product.isNew}} -

    {{i18n "discourse_subscriptions.admin.plans.title"}}

    - -

    - - - - - - - - - - - - - {{#each model.plans as |plan|}} - - - - - - - - - - {{else}} - - - - {{/each}} - -
    {{i18n "discourse_subscriptions.admin.plans.plan.nickname"}}{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}{{i18n "discourse_subscriptions.admin.plans.plan.created_at"}}{{i18n "discourse_subscriptions.admin.plans.plan.group"}}{{i18n "discourse_subscriptions.admin.plans.plan.active"}} - {{i18n "discourse_subscriptions.admin.plans.plan.amount"}} - - {{#link-to - "adminPlugins.discourse-subscriptions.products.show.plans.show" - model.product.id - "new" - class="btn" - }} - {{i18n "discourse_subscriptions.admin.plans.operations.add"}} - {{/link-to}} -
    {{plan.nickname}}{{plan.recurring.interval}}{{format-unix-date plan.created}}{{plan.metadata.group_name}}{{plan.active}} - {{format-currency plan.currency plan.amountDollars}} - - {{#link-to - "adminPlugins.discourse-subscriptions.products.show.plans.show" - model.product.id - plan.id - class="btn no-text btn-icon" - }} - {{d-icon "far-edit"}} - {{/link-to}} -
    -
    - {{i18n - "discourse_subscriptions.admin.products.product.plan_help" - }} -
    -

    -{{/unless}} - -
    - {{d-button label="cancel" action=(action "cancelProduct") icon="times"}} - - {{#if model.product.isNew}} - {{d-button - label="discourse_subscriptions.admin.products.operations.create" - action=(action "createProduct") - icon="plus" - class="btn btn-primary" - }} - {{else}} - {{d-button - label="discourse_subscriptions.admin.products.operations.update" - action=(action "updateProduct") - icon="check" - class="btn btn-primary" - }} - {{/if}} -
    - -{{outlet}} diff --git a/assets/javascripts/discourse/templates/user/billing.hbs b/assets/javascripts/discourse/templates/user/billing.hbs deleted file mode 100644 index 36241a1..0000000 --- a/assets/javascripts/discourse/templates/user/billing.hbs +++ /dev/null @@ -1,29 +0,0 @@ -{{#d-section - pageClass="user-billing" - class="user-secondary-navigation" - scrollTop="false" -}} -
    - - - - -
  • - -
  • -
    -
    -{{/d-section}} - -
    - {{outlet}} -
    diff --git a/assets/javascripts/discourse/templates/user/billing/authorizations.hbs b/assets/javascripts/discourse/templates/user/billing/authorizations.hbs deleted file mode 100644 index 75ad03d..0000000 --- a/assets/javascripts/discourse/templates/user/billing/authorizations.hbs +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - {{#each model.subscription_domains as |data|}} - - {{/each}} - -
    {{i18n "discourse_subscriptions.user.authorizations.resource"}}{{i18n "discourse_subscriptions.user.authorizations.products"}}{{i18n "discourse_subscriptions.user.authorizations.domains"}}{{i18n "discourse_subscriptions.user.authorizations.domain_limit"}}
    diff --git a/assets/javascripts/discourse/user-subscriptions-route-map.js b/assets/javascripts/discourse/user-subscriptions-route-map.js deleted file mode 100644 index 47fc38c..0000000 --- a/assets/javascripts/discourse/user-subscriptions-route-map.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - resource: "user", - path: "users/:username", - map() { - this.route("billing", function () { - this.route("payments"); - this.route("authorizations"); - this.route("subscriptions", function () { - this.route("card", { path: "/card/:stripe-subscription-id" }); - }); - }); - }, -}; diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss deleted file mode 100644 index 1ca0a3d..0000000 --- a/assets/stylesheets/common/common.scss +++ /dev/null @@ -1,73 +0,0 @@ -.btn-pavilion-subscribe { - border: 2px solid var(--tertiary); - padding: 2em; - margin-right: 2em; - cursor: pointer; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - line-height: unset; - - &.selected { - background-color: var(--tertiary); - color: var(--secondary); - } -} - -.discourse-subscriptions-section-columns { - margin: 20px 0; - padding: 20px 0; -} - -.product-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(48.5%, 1fr)); - grid-gap: 2em; - margin: 2em 0 4em 0; - - .product { - margin: 0; - width: unset; - } -} - -.subscriptions-banner { - display: none; - padding: 1rem; - margin-bottom: 1rem; - background-color: rgba(255, 200, 0, 0.25); - - &.visible { - display: block; - } -} - -.subscription-data { - ul { - list-style: none; - margin: 0; - } - td { - vertical-align: top; - } -} - -.subscription-domain { - display: flex; - gap: 1em; - align-items: center; - - .remove-domain { - background-color: unset; - } -} - -.discourse-subscriptions-invoices-btn { - margin-left: auto; - - .d-button-label { - color: var(--secondary) !important; - } -} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml deleted file mode 100644 index 73635f9..0000000 --- a/config/locales/client.en.yml +++ /dev/null @@ -1,37 +0,0 @@ -en: - site_settings: - custom_wizard_community_subscription_description: Description of the community subscription - custom_wizard_enterprise_subscription_description: Description of the enterprise subscription - custom_wizard_community_subscription_href: Community subscription href - custom_wizard_enterprise_subscription_href: Enterprise subscription href - custom_wizard_subscription_banner: Notice to show on subscription routes - js: - discourse_subscriptions: - admin: - products: - product: - plugin_name: Plugin Name - plugin_name_help: Name of plugin subscription is for. If filled, will generate API Key with subscription. - navigation: - authorizations: Authorizations - user: - authorizations: - resource: Resource - products: Products - domains: Domains - domain_limit: Domain Limit - remove_domain: - title: Remove domain - invoices: - btn: - label: "Invoices" - title: "Your subscription invoices" - modal: - title: Your Subscription Invoices - description: Click "Customer Portal" to go to Pavilion's Stripe Customer Portal where you can access your invoices. Sign in using %{email}. - btn: "Customer Portal" - admin: - products: - product: - hidden: Hidden - hidden_help: "Don't display the product in the subscriptions index." \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index 73ef249..d7d9bc5 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,21 +1,8 @@ plugins: - subscription_server_enabled: true + subscription_server_enabled: + client: true + default: true subscription_server_supplier_name: '' subscription_server_subscriptions: type: list - default: '' - custom_wizard_community_subscription_description: - client: true - default: 'Custom Wizard Community Subscription Description' - custom_wizard_enterprise_subscription_description: - client: true - default: 'Custom Wizard Enterprise Subscription Description' - custom_wizard_community_subscription_href: - client: true - default: 'https://coop.pavilion.tech/w/community-subscription' - custom_wizard_enterprise_subscription_href: - client: true - default: 'https://calendly.com/book-pavilion/initial-consult' - custom_wizard_subscription_banner: - client: true default: '' \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index a77a944..d34edef 100644 --- a/plugin.rb +++ b/plugin.rb @@ -8,7 +8,6 @@ # contact_emails: development@pavilion.tech enabled_site_setting :subscription_server_enabled -register_asset "stylesheets/common/common.scss" after_initialize do %w[ @@ -136,4 +135,10 @@ def self.class_update_subscriptions_from_csv(path) end end end + + on(:after_plugin_activation) do + Discourse.plugins.sort_by! do |plugin| + plugin.name == 'discourse-subscription-server' ? 1 : 0 + end + end end From 10154c91c1a6e7e3905b4085c3db7ba1a6112595 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 10 Jan 2024 16:41:50 +0100 Subject: [PATCH 3/5] Rubocop --- Gemfile.lock | 49 +++++++++++++++++++ ...scriptions_coupons_controller_extension.rb | 3 +- ...criptions_products_controller_extension.rb | 3 +- ...iptions_subscriber_controller_extension.rb | 3 +- 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..ef27598 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,49 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.6.3) + language_server-protocol (3.17.0.3) + parallel (1.23.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc + racc (1.7.3) + rainbow (3.1.1) + regexp_parser (2.8.2) + rexml (3.2.6) + rubocop (1.57.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.4) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.19.0) + rubocop (~> 1.41) + rubocop-discourse (3.4.1) + rubocop (>= 1.1.0) + rubocop-rspec (>= 2.0.0) + rubocop-factory_bot (2.24.0) + rubocop (~> 1.33) + rubocop-rspec (2.25.0) + rubocop (~> 1.40) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) + unicode-display_width (2.5.0) + +PLATFORMS + arm64-darwin-22 + +DEPENDENCIES + rubocop-discourse + +BUNDLED WITH + 2.4.13 diff --git a/extensions/discourse_subscriptions_coupons_controller_extension.rb b/extensions/discourse_subscriptions_coupons_controller_extension.rb index 42c2b8f..094110e 100644 --- a/extensions/discourse_subscriptions_coupons_controller_extension.rb +++ b/extensions/discourse_subscriptions_coupons_controller_extension.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module DiscourseSubscriptionsCouponsControllerExtension def create params.require([:promo, :discount_type, :discount, :active, :applies_to_products]) @@ -26,4 +27,4 @@ def create render_json_error e.message end end -end \ No newline at end of file +end diff --git a/extensions/discourse_subscriptions_products_controller_extension.rb b/extensions/discourse_subscriptions_products_controller_extension.rb index d56e1c3..9fde5c5 100644 --- a/extensions/discourse_subscriptions_products_controller_extension.rb +++ b/extensions/discourse_subscriptions_products_controller_extension.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module DiscourseSubscriptionsProductsControllerExtension def show begin @@ -28,4 +29,4 @@ def show } } end -end \ No newline at end of file +end diff --git a/extensions/discourse_subscriptions_subscriber_controller_extension.rb b/extensions/discourse_subscriptions_subscriber_controller_extension.rb index e8cdd9e..614d787 100644 --- a/extensions/discourse_subscriptions_subscriber_controller_extension.rb +++ b/extensions/discourse_subscriptions_subscriber_controller_extension.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module DiscourseSubscriptionsSubscribeControllerExtension private def serialize_product(product) { @@ -19,4 +20,4 @@ module DiscourseSubscriptionsSubscribeControllerExtension result end.sort_by { |plan| plan[:amount] } end -end \ No newline at end of file +end From 74886e7b6101e41044753e3d78b92940cf792115 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 10 Jan 2024 16:44:38 +0100 Subject: [PATCH 4/5] Update Gemfile.lock --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index ef27598..4dc8879 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,6 +41,7 @@ GEM PLATFORMS arm64-darwin-22 + x86_64-linux DEPENDENCIES rubocop-discourse From bc762eea4c4c19ec4d4e8f5dd4da087ea3ab7283 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 10 Jan 2024 17:36:14 +0100 Subject: [PATCH 5/5] Update file loading --- lib/subscription_server/provider.rb | 2 +- lib/subscription_server/providers/stripe.rb | 4 ++ plugin.rb | 64 +++++++++++---------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/lib/subscription_server/provider.rb b/lib/subscription_server/provider.rb index 3936c9a..25ca4d4 100644 --- a/lib/subscription_server/provider.rb +++ b/lib/subscription_server/provider.rb @@ -6,7 +6,7 @@ class SubscriptionServer::Provider attr_reader :user - def initialize(user) + def initialize(user = nil) @user = user end diff --git a/lib/subscription_server/providers/stripe.rb b/lib/subscription_server/providers/stripe.rb index 5da8e7a..47309c9 100644 --- a/lib/subscription_server/providers/stripe.rb +++ b/lib/subscription_server/providers/stripe.rb @@ -45,4 +45,8 @@ def subscriptions(product_ids, resource_name) result end end + + def self.discourse_subscriptions_installed? + new.discourse_subscriptions_installed? + end end diff --git a/plugin.rb b/plugin.rb index d34edef..9420fe2 100644 --- a/plugin.rb +++ b/plugin.rb @@ -9,6 +9,13 @@ enabled_site_setting :subscription_server_enabled +DiscourseEvent.on(:after_plugin_activation) do + sorted_pugins = Discourse.plugins.sort_by do |p| + p&.name == 'discourse-subscription-server' ? 1 : 0 + end + Discourse.instance_variable_set(:@plugins, sorted_pugins) +end + after_initialize do %w[ ../lib/subscription_server/engine.rb @@ -26,9 +33,6 @@ ../app/controllers/subscription_server/server_controller.rb ../app/serializers/subscription_server/message_serializer.rb ../app/serializers/subscription_server/subscription_serializer.rb - ../extensions/discourse_subscriptions_coupons_controller_extension.rb - ../extensions/discourse_subscriptions_products_controller_extension.rb - ../extensions/discourse_subscriptions_subscriber_controller_extension.rb ].each do |path| load File.expand_path(path, __FILE__) end @@ -112,33 +116,35 @@ add_to_serializer(:user, :subscription_domains) { user.subscription_domains } - ## DiscourseSubscription extensions. discourse-subscriptions plugin must be above this plugin in app.yml - DiscourseSubscriptions::SubscribeController.prepend DiscourseSubscriptionsSubscribeControllerExtension - DiscourseSubscriptions::Admin::CouponsController.prepend DiscourseSubscriptionsCouponsControllerExtension - DiscourseSubscriptions::Admin::ProductsController.prepend DiscourseSubscriptionsProductsControllerExtension - - require 'csv' - module ::DiscourseSubscriptions - def self.class_update_subscriptions_from_csv(path) - rows = CSV.read(path) - return unless rows.present? - - rows.each do |row| - DiscourseSubscriptions::Subscription - .joins(:customer) - .where("discourse_subscriptions_customers.customer_id = :customer_id AND discourse_subscriptions_subscriptions.external_id = :subscription_id", - customer_id: row[0].strip, - subscription_id: row[1].strip - ).update_all( - external_id: row[2].strip - ) - end + if SubscriptionServer::Stripe.discourse_subscriptions_installed? + %w[ + ../extensions/discourse_subscriptions_coupons_controller_extension.rb + ../extensions/discourse_subscriptions_products_controller_extension.rb + ../extensions/discourse_subscriptions_subscriber_controller_extension.rb + ].each do |path| + load File.expand_path(path, __FILE__) end - end - - on(:after_plugin_activation) do - Discourse.plugins.sort_by! do |plugin| - plugin.name == 'discourse-subscription-server' ? 1 : 0 + DiscourseSubscriptions::SubscribeController.prepend DiscourseSubscriptionsSubscribeControllerExtension + DiscourseSubscriptions::Admin::CouponsController.prepend DiscourseSubscriptionsCouponsControllerExtension + DiscourseSubscriptions::Admin::ProductsController.prepend DiscourseSubscriptionsProductsControllerExtension + + require 'csv' + module ::DiscourseSubscriptions + def self.class_update_subscriptions_from_csv(path) + rows = CSV.read(path) + return unless rows.present? + + rows.each do |row| + DiscourseSubscriptions::Subscription + .joins(:customer) + .where("discourse_subscriptions_customers.customer_id = :customer_id AND discourse_subscriptions_subscriptions.external_id = :subscription_id", + customer_id: row[0].strip, + subscription_id: row[1].strip + ).update_all( + external_id: row[2].strip + ) + end + end end end end