From 072b558d40d4be331e5d4a428042f9a8ce5ef896 Mon Sep 17 00:00:00 2001 From: Justin DiRose Date: Sun, 31 Jan 2021 14:17:44 -0600 Subject: [PATCH] FEATURE: Give option to repurchase products multiple times (#46) Feature requested here: https://meta.discourse.org/t/subscriptions-allow-users-to-purchase-one-time-products-multiple-times/173732/ There may be cases where a site admin wants to allow the repurchasing of a product. This implements the functionality by adding a repurchaseable toggle in the admin screen when creating a product. This saves an attribute to the Stripe product metadata. When a user has already purchased an item with this toggle enabled, they will be able to purchase it again when browsing to `/s`. --- .../admin/products_controller.rb | 5 ++- .../subscribe_controller.rb | 3 +- .../discourse/components/product-item.js.es6 | 5 +++ .../discourse/controllers/s-show.js.es6 | 10 ++++++ ...-discourse-subscriptions-products-show.hbs | 7 ++++ .../templates/components/product-item.hbs | 32 +++++++++++++++++++ .../templates/components/product-list.hbs | 24 +------------- .../discourse/templates/s/show.hbs | 4 +-- config/locales/client.en.yml | 2 ++ .../admin/products_controller_spec.rb | 12 +++++-- spec/requests/subscribe_controller_spec.rb | 9 ++++-- 11 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 assets/javascripts/discourse/components/product-item.js.es6 create mode 100644 assets/javascripts/discourse/templates/components/product-item.hbs diff --git a/app/controllers/discourse_subscriptions/admin/products_controller.rb b/app/controllers/discourse_subscriptions/admin/products_controller.rb index 399931a..5962637 100644 --- a/app/controllers/discourse_subscriptions/admin/products_controller.rb +++ b/app/controllers/discourse_subscriptions/admin/products_controller.rb @@ -93,7 +93,10 @@ module DiscourseSubscriptions name: params[:name], active: params[:active], statement_descriptor: params[:statement_descriptor], - metadata: { description: params.dig(:metadata, :description) } + metadata: { + description: params.dig(:metadata, :description), + repurchaseable: params.dig(:metadata, :repurchaseable) + } } end end diff --git a/app/controllers/discourse_subscriptions/subscribe_controller.rb b/app/controllers/discourse_subscriptions/subscribe_controller.rb index c5dd55f..eb88e71 100644 --- a/app/controllers/discourse_subscriptions/subscribe_controller.rb +++ b/app/controllers/discourse_subscriptions/subscribe_controller.rb @@ -136,7 +136,8 @@ module DiscourseSubscriptions id: product[:id], name: product[:name], description: PrettyText.cook(product[:metadata][:description]), - subscribed: current_user_products.include?(product[:id]) + subscribed: current_user_products.include?(product[:id]), + repurchaseable: product[:metadata][:repurchaseable] } end diff --git a/assets/javascripts/discourse/components/product-item.js.es6 b/assets/javascripts/discourse/components/product-item.js.es6 new file mode 100644 index 0000000..f943e9b --- /dev/null +++ b/assets/javascripts/discourse/components/product-item.js.es6 @@ -0,0 +1,5 @@ +import Component from "@ember/component"; + +export default Component.extend({ + classNames: ["product"], +}); diff --git a/assets/javascripts/discourse/controllers/s-show.js.es6 b/assets/javascripts/discourse/controllers/s-show.js.es6 index c79bc0f..79a56bc 100644 --- a/assets/javascripts/discourse/controllers/s-show.js.es6 +++ b/assets/javascripts/discourse/controllers/s-show.js.es6 @@ -3,6 +3,7 @@ import Subscription from "discourse/plugins/discourse-subscriptions/discourse/mo import Transaction from "discourse/plugins/discourse-subscriptions/discourse/models/transaction"; import I18n from "I18n"; import { not } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; export default Controller.extend({ selectedPlan: null, @@ -24,6 +25,15 @@ export default Controller.extend({ bootbox.alert(I18n.t(`discourse_subscriptions.${path}`)); }, + @discourseComputed("model.product.repurchaseable", "model.product.subscribed") + canPurchase(repurchaseable, subscribed) { + if (!repurchaseable && subscribed) { + return false; + } + + return true; + }, + createSubscription(plan) { return this.stripe.createToken(this.get("cardElement")).then((result) => { if (result.error) { 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 index d01ab69..d15cf40 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs @@ -23,6 +23,13 @@ {{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="active" checked=model.product.active}} diff --git a/assets/javascripts/discourse/templates/components/product-item.hbs b/assets/javascripts/discourse/templates/components/product-item.hbs new file mode 100644 index 0000000..dec3022 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/product-item.hbs @@ -0,0 +1,32 @@ +

{{product.name}}

+ +

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

+ +{{#if isLoggedIn}} +
+ {{#if product.repurchaseable}} + {{#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}} + {{/if}} + {{#link-to "s.show" product.id class="btn btn-primary"}} + {{i18n 'discourse_subscriptions.subscribe.title'}} + {{/link-to}} + {{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 "s.show" product.id disabled=product.subscribed class="btn btn-primary"}} + {{i18n 'discourse_subscriptions.subscribe.title'}} + {{/link-to}} + {{/if}} + {{/if}} +
+{{/if}} diff --git a/assets/javascripts/discourse/templates/components/product-list.hbs b/assets/javascripts/discourse/templates/components/product-list.hbs index 9b48aa6..e45fc36 100644 --- a/assets/javascripts/discourse/templates/components/product-list.hbs +++ b/assets/javascripts/discourse/templates/components/product-list.hbs @@ -2,28 +2,6 @@

{{i18n 'discourse_subscriptions.subscribe.no_products'}}

{{else}} {{#each products as |product|}} -
-

{{product.name}}

- -

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

- - {{#if isLoggedIn}} -
- {{#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 "s.show" product.id disabled=product.subscribed class="btn btn-primary"}} - {{i18n 'discourse_subscriptions.subscribe.title'}} - {{/link-to}} - {{/if}} -
- {{/if}} -
+ {{product-item product=product isLoggedIn=isLoggedIn}} {{/each}} - {{/if}} diff --git a/assets/javascripts/discourse/templates/s/show.hbs b/assets/javascripts/discourse/templates/s/show.hbs index 3d24d5d..9de2ee7 100644 --- a/assets/javascripts/discourse/templates/s/show.hbs +++ b/assets/javascripts/discourse/templates/s/show.hbs @@ -12,7 +12,7 @@

- {{#unless model.product.subscribed}} + {{#if canPurchase}}

{{i18n 'discourse_subscriptions.subscribe.card.title'}}

@@ -45,6 +45,6 @@ }} {{/if}} - {{/unless}} + {{/if}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f2eea12..eeefdbb 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -111,6 +111,8 @@ en: plan_help: Create a pricing plan to subscribe customers to this product. description: Description description_help: This describes your subscription product. + repurchaseable: Repurchaseable? + repurchase_help: Allow product and associated plans to be purchased multiple times active: Active active_help: Toggle this off until your product is ready for consumers. created_at: Created diff --git a/spec/requests/admin/products_controller_spec.rb b/spec/requests/admin/products_controller_spec.rb index 45e2cd5..ec7aec6 100644 --- a/spec/requests/admin/products_controller_spec.rb +++ b/spec/requests/admin/products_controller_spec.rb @@ -81,9 +81,15 @@ module DiscourseSubscriptions post "/s/admin/products.json", params: { statement_descriptor: '' } end - it 'has a description' do - ::Stripe::Product.expects(:create).with(has_entry(metadata: { description: 'Oi, I think he just said bless be all the bignoses!' })) - post "/s/admin/products.json", params: { metadata: { description: 'Oi, I think he just said bless be all the bignoses!' } } + it 'has metadata' do + ::Stripe::Product.expects(:create).with(has_entry(metadata: { description: 'Oi, I think he just said bless be all the bignoses!', repurchaseable: 'false' })) + + post "/s/admin/products.json", params: { + metadata: { + description: 'Oi, I think he just said bless be all the bignoses!', + repurchaseable: 'false' + } + } end end diff --git a/spec/requests/subscribe_controller_spec.rb b/spec/requests/subscribe_controller_spec.rb index 0ad4287..f90cfe8 100644 --- a/spec/requests/subscribe_controller_spec.rb +++ b/spec/requests/subscribe_controller_spec.rb @@ -12,7 +12,8 @@ module DiscourseSubscriptions id: "prodct_23456", name: "Very Special Product", metadata: { - description: "Many people listened to my phone call with the Ukrainian President while it was being made" + description: "Many people listened to my phone call with the Ukrainian President while it was being made", + repurchaseable: false }, otherstuff: true, } @@ -48,7 +49,8 @@ module DiscourseSubscriptions "id" => "prodct_23456", "name" => "Very Special Product", "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), - "subscribed" => false + "subscribed" => false, + "repurchaseable" => false, }]) end @@ -82,7 +84,8 @@ module DiscourseSubscriptions "id" => "prodct_23456", "name" => "Very Special Product", "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), - "subscribed" => false + "subscribed" => false, + "repurchaseable" => false }, "plans" => [ { "currency" => "aud", "id" => "plan_id123", "recurring" => { "interval" => "year" }, "unit_amount" => 1220 },