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`.
This commit is contained in:
Justin DiRose 2021-01-31 14:17:44 -06:00 committed by GitHub
parent 3f8fca3246
commit 072b558d40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 80 additions and 33 deletions

View File

@ -93,7 +93,10 @@ module DiscourseSubscriptions
name: params[:name], name: params[:name],
active: params[:active], active: params[:active],
statement_descriptor: params[:statement_descriptor], statement_descriptor: params[:statement_descriptor],
metadata: { description: params.dig(:metadata, :description) } metadata: {
description: params.dig(:metadata, :description),
repurchaseable: params.dig(:metadata, :repurchaseable)
}
} }
end end
end end

View File

@ -136,7 +136,8 @@ module DiscourseSubscriptions
id: product[:id], id: product[:id],
name: product[:name], name: product[:name],
description: PrettyText.cook(product[:metadata][:description]), 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 end

View File

@ -0,0 +1,5 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["product"],
});

View File

@ -3,6 +3,7 @@ import Subscription from "discourse/plugins/discourse-subscriptions/discourse/mo
import Transaction from "discourse/plugins/discourse-subscriptions/discourse/models/transaction"; import Transaction from "discourse/plugins/discourse-subscriptions/discourse/models/transaction";
import I18n from "I18n"; import I18n from "I18n";
import { not } from "@ember/object/computed"; import { not } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({ export default Controller.extend({
selectedPlan: null, selectedPlan: null,
@ -24,6 +25,15 @@ export default Controller.extend({
bootbox.alert(I18n.t(`discourse_subscriptions.${path}`)); 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) { createSubscription(plan) {
return this.stripe.createToken(this.get("cardElement")).then((result) => { return this.stripe.createToken(this.get("cardElement")).then((result) => {
if (result.error) { if (result.error) {

View File

@ -23,6 +23,13 @@
{{i18n 'discourse_subscriptions.admin.products.product.statement_descriptor_help'}} {{i18n 'discourse_subscriptions.admin.products.product.statement_descriptor_help'}}
</div> </div>
</p> </p>
<p>
<label for="repurchaseable">{{i18n 'discourse_subscriptions.admin.products.product.repurchaseable'}}</label>
{{input type="checkbox" name="repurchaseable" checked=model.product.metadata.repurchaseable}}
<div class="control-instructions">
{{i18n 'discourse_subscriptions.admin.products.product.repurchase_help'}}
</div>
</p>
<p> <p>
<label for="active">{{i18n 'discourse_subscriptions.admin.products.product.active'}}</label> <label for="active">{{i18n 'discourse_subscriptions.admin.products.product.active'}}</label>
{{input type="checkbox" name="active" checked=model.product.active}} {{input type="checkbox" name="active" checked=model.product.active}}

View File

@ -0,0 +1,32 @@
<h2>{{product.name}}</h2>
<p class="product-description">
{{html-safe product.description}}
</p>
{{#if isLoggedIn}}
<div class="product-purchase">
{{#if product.repurchaseable}}
{{#if product.subscribed}}
<span class="purchased">&#x2713; {{i18n 'discourse_subscriptions.subscribe.purchased'}}</span>
{{#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}}
<span class="purchased">&#x2713; {{i18n 'discourse_subscriptions.subscribe.purchased'}}</span>
{{#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}}
</div>
{{/if}}

View File

@ -2,28 +2,6 @@
<p>{{i18n 'discourse_subscriptions.subscribe.no_products'}}</p> <p>{{i18n 'discourse_subscriptions.subscribe.no_products'}}</p>
{{else}} {{else}}
{{#each products as |product|}} {{#each products as |product|}}
<div class="product"> {{product-item product=product isLoggedIn=isLoggedIn}}
<h2>{{product.name}}</h2>
<p class="product-description">
{{html-safe product.description}}
</p>
{{#if isLoggedIn}}
<div class="product-purchase">
{{#if product.subscribed}}
<span class="purchased">&#x2713; {{i18n 'discourse_subscriptions.subscribe.purchased'}}</span>
{{#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}}
</div>
{{/if}}
</div>
{{/each}} {{/each}}
{{/if}} {{/if}}

View File

@ -12,7 +12,7 @@
</p> </p>
</div> </div>
<div class="section-column"> <div class="section-column">
{{#unless model.product.subscribed}} {{#if canPurchase}}
<h2> <h2>
{{i18n 'discourse_subscriptions.subscribe.card.title'}} {{i18n 'discourse_subscriptions.subscribe.card.title'}}
</h2> </h2>
@ -45,6 +45,6 @@
}} }}
{{/if}} {{/if}}
{{/unless}} {{/if}}
</div> </div>
</div> </div>

View File

@ -111,6 +111,8 @@ en:
plan_help: Create a pricing plan to subscribe customers to this product. plan_help: Create a pricing plan to subscribe customers to this product.
description: Description description: Description
description_help: This describes your subscription product. description_help: This describes your subscription product.
repurchaseable: Repurchaseable?
repurchase_help: Allow product and associated plans to be purchased multiple times
active: Active active: Active
active_help: Toggle this off until your product is ready for consumers. active_help: Toggle this off until your product is ready for consumers.
created_at: Created created_at: Created

View File

@ -81,9 +81,15 @@ module DiscourseSubscriptions
post "/s/admin/products.json", params: { statement_descriptor: '' } post "/s/admin/products.json", params: { statement_descriptor: '' }
end end
it 'has a description' do it 'has metadata' do
::Stripe::Product.expects(:create).with(has_entry(metadata: { description: 'Oi, I think he just said bless be all the bignoses!' })) ::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!' } }
post "/s/admin/products.json", params: {
metadata: {
description: 'Oi, I think he just said bless be all the bignoses!',
repurchaseable: 'false'
}
}
end end
end end

View File

@ -12,7 +12,8 @@ module DiscourseSubscriptions
id: "prodct_23456", id: "prodct_23456",
name: "Very Special Product", name: "Very Special Product",
metadata: { 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, otherstuff: true,
} }
@ -48,7 +49,8 @@ module DiscourseSubscriptions
"id" => "prodct_23456", "id" => "prodct_23456",
"name" => "Very Special Product", "name" => "Very Special Product",
"description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), "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 end
@ -82,7 +84,8 @@ module DiscourseSubscriptions
"id" => "prodct_23456", "id" => "prodct_23456",
"name" => "Very Special Product", "name" => "Very Special Product",
"description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), "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" => [ "plans" => [
{ "currency" => "aud", "id" => "plan_id123", "recurring" => { "interval" => "year" }, "unit_amount" => 1220 }, { "currency" => "aud", "id" => "plan_id123", "recurring" => { "interval" => "year" }, "unit_amount" => 1220 },