mirror of
https://github.com/discourse/discourse-subscriptions.git
synced 2025-02-16 16:34:43 +00:00
buttons for selecting price and one time payment
This commit is contained in:
parent
e27b55ea6f
commit
57fb508514
31
app/controllers/payments_controller.rb
Normal file
31
app/controllers/payments_controller.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseSubscriptions
|
||||||
|
class PaymentsController < ::ApplicationController
|
||||||
|
include DiscourseSubscriptions::Stripe
|
||||||
|
|
||||||
|
skip_before_action :verify_authenticity_token, only: [:create]
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def create
|
||||||
|
begin
|
||||||
|
payment = ::Stripe::PaymentIntent.create(
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
payment_method: params[:payment_method],
|
||||||
|
amount: params[:amount],
|
||||||
|
currency: params[:currency],
|
||||||
|
confirm: true
|
||||||
|
)
|
||||||
|
|
||||||
|
render_json_dump payment
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
render_json_error e.message
|
||||||
|
rescue ::Stripe::CardError => e
|
||||||
|
render_json_error 'Card Declined'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,5 +1,18 @@
|
|||||||
|
import { equal } from "@ember/object/computed";
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
planButtonSelected: equal("planTypeIsSelected", true),
|
||||||
|
paymentButtonSelected: equal("planTypeIsSelected", false),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
selectPlans() {
|
||||||
|
this.set("planTypeIsSelected", true);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPayments() {
|
||||||
|
this.set("planTypeIsSelected", false);
|
||||||
|
},
|
||||||
|
|
||||||
clickPlan(plan) {
|
clickPlan(plan) {
|
||||||
this.plans.map(p => p.set("selected", false));
|
this.plans.map(p => p.set("selected", false));
|
||||||
plan.set("selected", true);
|
plan.set("selected", true);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import computed from "ember-addons/ember-computed-decorators";
|
import computed from "discourse-common/utils/decorators";
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
|
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
|
||||||
|
import Payment from "discourse/plugins/discourse-subscriptions/discourse/models/payment";
|
||||||
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
|
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
|
||||||
|
import computed from "discourse-common/utils/decorators";
|
||||||
|
import { i18n } from "discourse/lib/computed";
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
|
planTypeIsSelected: true,
|
||||||
|
|
||||||
|
@computed("planTypeIsSelected")
|
||||||
|
type(planTypeIsSelected) {
|
||||||
|
return planTypeIsSelected ? "plans" : "payment";
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("type")
|
||||||
|
buttonText(type) {
|
||||||
|
return I18n.t(`discourse_subscriptions.${this.get("type")}.payment_button`);
|
||||||
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set(
|
this.set(
|
||||||
@ -13,54 +28,83 @@ export default Ember.Controller.extend({
|
|||||||
this.set("cardElement", elements.create("card", { hidePostalCode: true }));
|
this.set("cardElement", elements.create("card", { hidePostalCode: true }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
alert(path) {
|
||||||
|
bootbox.alert(I18n.t(`discourse_subscriptions.${path}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
createPayment(plan) {
|
||||||
|
return this.stripe
|
||||||
|
.createPaymentMethod("card", this.get("cardElement"))
|
||||||
|
.then(result => {
|
||||||
|
const payment = Payment.create({
|
||||||
|
payment_method: result.paymentMethod.id,
|
||||||
|
amount: plan.get("amount"),
|
||||||
|
currency: plan.get("currency")
|
||||||
|
});
|
||||||
|
|
||||||
|
return payment.save();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createSubsciption(plan) {
|
||||||
|
return this.stripe.createToken(this.get("cardElement")).then(result => {
|
||||||
|
if (result.error) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const customer = Customer.create({ source: result.token.id });
|
||||||
|
|
||||||
|
return customer.save().then(c => {
|
||||||
|
const subscription = Subscription.create({
|
||||||
|
customer: c.id,
|
||||||
|
plan: plan.get("id")
|
||||||
|
});
|
||||||
|
|
||||||
|
return subscription.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
stripePaymentHandler() {
|
stripePaymentHandler() {
|
||||||
this.set("loading", true);
|
this.set("loading", true);
|
||||||
|
const type = this.get("type");
|
||||||
const plan = this.get("model.plans")
|
const plan = this.get("model.plans")
|
||||||
.filterBy("selected")
|
.filterBy("selected")
|
||||||
.get("firstObject");
|
.get("firstObject");
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
bootbox.alert(
|
this.alert(`${type}.validate.payment_options.required`);
|
||||||
I18n.t(
|
|
||||||
"discourse_subscriptions.transactions.payment.validate.plan.required"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.set("loading", false);
|
this.set("loading", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stripe.createToken(this.get("cardElement")).then(result => {
|
let transaction;
|
||||||
if (result.error) {
|
|
||||||
bootbox.alert(result.error.message);
|
if (this.planTypeIsSelected) {
|
||||||
|
transaction = this.createSubsciption(plan);
|
||||||
|
} else {
|
||||||
|
transaction = this.createPayment(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.then(result => {
|
||||||
|
if (result.error) {
|
||||||
|
bootbox.alert(result.error.message || result.error);
|
||||||
|
} else {
|
||||||
|
this.alert(`${type}.success`);
|
||||||
|
this.transitionToRoute(
|
||||||
|
"user.subscriptions",
|
||||||
|
Discourse.User.current().username.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(result => {
|
||||||
|
bootbox.alert(result.errorThrown);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
this.set("loading", false);
|
this.set("loading", false);
|
||||||
} else {
|
});
|
||||||
const customer = Customer.create({ source: result.token.id });
|
|
||||||
|
|
||||||
customer.save().then(c => {
|
|
||||||
const subscription = Subscription.create({
|
|
||||||
customer: c.id,
|
|
||||||
plan: plan.get("id")
|
|
||||||
});
|
|
||||||
|
|
||||||
subscription
|
|
||||||
.save()
|
|
||||||
.then(() => {
|
|
||||||
bootbox.alert(
|
|
||||||
I18n.t("discourse_subscriptions.transactions.payment.success")
|
|
||||||
);
|
|
||||||
this.transitionToRoute(
|
|
||||||
"user.subscriptions",
|
|
||||||
Discourse.User.current().username.toLowerCase()
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.set("loading", false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
15
assets/javascripts/discourse/models/payment.js.es6
Normal file
15
assets/javascripts/discourse/models/payment.js.es6
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const Payment = Discourse.Model.extend({
|
||||||
|
save() {
|
||||||
|
const data = {
|
||||||
|
payment_method: this.payment_method,
|
||||||
|
amount: this.amount,
|
||||||
|
currency: this.currency
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax("/s/payments", { method: "post", data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Payment;
|
@ -1,27 +1,35 @@
|
|||||||
|
|
||||||
{{#ds-button
|
<div class="subscribe-buttons">
|
||||||
id="discourse-subscriptions-payment-type-plan"
|
{{#ds-button
|
||||||
selected=true
|
id="discourse-subscriptions-payment-type-plan"
|
||||||
class="btn-discourse-subscriptions-payment-type"
|
selected=planButtonSelected
|
||||||
}}
|
action="selectPlans"
|
||||||
{{i18n "discourse_subscriptions.plans.purchase"}}
|
class="btn-discourse-subscriptions-payment-type"
|
||||||
{{/ds-button}}
|
}}
|
||||||
|
{{i18n "discourse_subscriptions.plans.purchase"}}
|
||||||
|
{{/ds-button}}
|
||||||
|
|
||||||
{{#ds-button
|
{{#ds-button
|
||||||
id="discourse-subscriptions-payment-type-payment"
|
id="discourse-subscriptions-payment-type-payment"
|
||||||
selected=false
|
selected=paymentButtonSelected
|
||||||
class="btn-discourse-subscriptions-payment-type"
|
action="selectPayments"
|
||||||
}}
|
class="btn-discourse-subscriptions-payment-type"
|
||||||
{{i18n "discourse_subscriptions.payment.purchase"}}
|
}}
|
||||||
{{/ds-button}}
|
{{i18n "discourse_subscriptions.payment.purchase"}}
|
||||||
|
{{/ds-button}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{i18n "discourse_subscriptions.plans.select"}}
|
{{#if planTypeIsSelected}}
|
||||||
|
{{i18n "discourse_subscriptions.plans.select"}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n "discourse_subscriptions.payment.select"}}
|
||||||
|
{{/if}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div id="subscribe-buttons">
|
<div class="subscribe-buttons">
|
||||||
{{#each plans as |plan|}}
|
{{#each plans as |plan|}}
|
||||||
{{#ds-button
|
{{#ds-button
|
||||||
action="clickPlan"
|
action="clickPlan"
|
||||||
@ -30,7 +38,11 @@
|
|||||||
class="btn-discourse-subscriptions-subscribe"
|
class="btn-discourse-subscriptions-subscribe"
|
||||||
}}
|
}}
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
{{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.interval)}}
|
{{#if planTypeIsSelected}}
|
||||||
|
{{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.interval)}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n "discourse_subscriptions.payment.interval"}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<span class="amount">
|
<span class="amount">
|
||||||
{{format-currency plan.currency plan.amountDollars}}
|
{{format-currency plan.currency plan.amountDollars}}
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
{{i18n 'discourse_subscriptions.subscribe.card.title'}}
|
{{i18n 'discourse_subscriptions.subscribe.card.title'}}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{{payment-options plans=model.plans}}
|
{{payment-options
|
||||||
|
plans=model.plans
|
||||||
|
planTypeIsSelected=planTypeIsSelected
|
||||||
|
}}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
@ -27,7 +30,7 @@
|
|||||||
disabled=loading
|
disabled=loading
|
||||||
action="stripePaymentHandler"
|
action="stripePaymentHandler"
|
||||||
class="btn btn-primary btn-payment"}}
|
class="btn btn-primary btn-payment"}}
|
||||||
{{i18n 'discourse_subscriptions.subscribe.buttons.subscribe'}}
|
{{buttonText}}
|
||||||
{{/d-button}}
|
{{/d-button}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#subscribe-buttons {
|
.subscribe-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
|
||||||
|
@ -18,26 +18,35 @@ en:
|
|||||||
title: Discourse Subscriptions
|
title: Discourse Subscriptions
|
||||||
admin_navigation: Subscriptions
|
admin_navigation: Subscriptions
|
||||||
optional: Optional
|
optional: Optional
|
||||||
transactions:
|
|
||||||
payment:
|
|
||||||
success: Your payment was successful
|
|
||||||
validate:
|
|
||||||
plan:
|
|
||||||
required: Please select a subscription plan.
|
|
||||||
navigation:
|
navigation:
|
||||||
subscriptions: Subscriptions
|
subscriptions: Subscriptions
|
||||||
subscribe: Subscribe
|
subscribe: Subscribe
|
||||||
billing: Billing
|
billing: Billing
|
||||||
plans:
|
plans:
|
||||||
select: Select subscription plan
|
|
||||||
purchase: Purchase a subscription
|
purchase: Purchase a subscription
|
||||||
|
select: Select subscription plan
|
||||||
interval:
|
interval:
|
||||||
adverb:
|
adverb:
|
||||||
week: Weekly
|
week: Weekly
|
||||||
month: Monthly
|
month: Monthly
|
||||||
year: Yearly
|
year: Yearly
|
||||||
|
payment_button:
|
||||||
|
Subscribe
|
||||||
|
success: Thank you! Your subscription has been created.
|
||||||
|
validate:
|
||||||
|
payment_options:
|
||||||
|
required: Please select a subscription plan.
|
||||||
payment:
|
payment:
|
||||||
purchase: Make just one payment
|
purchase: Make just one payment
|
||||||
|
select: Select a payment option
|
||||||
|
interval:
|
||||||
|
One payment
|
||||||
|
payment_button:
|
||||||
|
Pay Once
|
||||||
|
success: Thank you!
|
||||||
|
validate:
|
||||||
|
payment_options:
|
||||||
|
required: Please select a payment option.
|
||||||
one_time:
|
one_time:
|
||||||
heading:
|
heading:
|
||||||
payment: Make a Payment
|
payment: Make a Payment
|
||||||
|
@ -18,6 +18,7 @@ DiscourseSubscriptions::Engine.routes.draw do
|
|||||||
|
|
||||||
resources :customers, only: [:create]
|
resources :customers, only: [:create]
|
||||||
resources :invoices, only: [:index]
|
resources :invoices, only: [:index]
|
||||||
|
resources :payments, only: [:create]
|
||||||
resources :patrons, only: [:index, :create]
|
resources :patrons, only: [:index, :create]
|
||||||
resources :plans, only: [:index]
|
resources :plans, only: [:index]
|
||||||
resources :products, only: [:index, :show]
|
resources :products, only: [:index, :show]
|
||||||
|
@ -62,6 +62,7 @@ after_initialize do
|
|||||||
"../app/controllers/invoices_controller",
|
"../app/controllers/invoices_controller",
|
||||||
"../app/controllers/patrons_controller",
|
"../app/controllers/patrons_controller",
|
||||||
"../app/controllers/plans_controller",
|
"../app/controllers/plans_controller",
|
||||||
|
"../app/controllers/payments_controller",
|
||||||
"../app/controllers/products_controller",
|
"../app/controllers/products_controller",
|
||||||
"../app/controllers/subscriptions_controller",
|
"../app/controllers/subscriptions_controller",
|
||||||
"../app/models/customer",
|
"../app/models/customer",
|
||||||
|
40
spec/requests/payments_controller_spec.rb
Normal file
40
spec/requests/payments_controller_spec.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscourseSubscriptions
|
||||||
|
RSpec.describe PaymentsController do
|
||||||
|
context "not authenticated" do
|
||||||
|
it "does not create a payment intent" do
|
||||||
|
::Stripe::PaymentIntent.expects(:create).never
|
||||||
|
post "/s/payments.json", params: { }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "authenticated" do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create" do
|
||||||
|
it "creates a payment intent" do
|
||||||
|
::Stripe::PaymentIntent.expects(:create).with(
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
payment_method: 'pm_123',
|
||||||
|
amount: '999',
|
||||||
|
currency: 'gdp',
|
||||||
|
confirm: true
|
||||||
|
)
|
||||||
|
|
||||||
|
post "/s/payments.json", params: {
|
||||||
|
payment_method: 'pm_123',
|
||||||
|
amount: 999,
|
||||||
|
currency: 'gdp'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -18,7 +18,7 @@ componentTest("Discourse Subscriptions payment options have no plans", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentTest("Discourse Subscriptions payment options has content", {
|
componentTest("Discourse Subscriptions payment options has content", {
|
||||||
template: `{{payment-options plans=plans}}`,
|
template: `{{payment-options plans=plans planTypeIsSelected=planTypeIsSelected}}`,
|
||||||
|
|
||||||
async test(assert) {
|
async test(assert) {
|
||||||
this.set("plans", [
|
this.set("plans", [
|
||||||
@ -26,21 +26,8 @@ componentTest("Discourse Subscriptions payment options has content", {
|
|||||||
{ currency: "gdp", interval: "month", amountDollars: "9.99" }
|
{ currency: "gdp", interval: "month", amountDollars: "9.99" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(
|
this.set("planTypeIsSelected", true);
|
||||||
find(".btn-discourse-subscriptions-payment-type").length,
|
|
||||||
2,
|
|
||||||
"The payment type buttons are shown"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
|
|
||||||
1,
|
|
||||||
"The plan payment type button is selected"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
|
|
||||||
0,
|
|
||||||
"The single payment type button is not selected"
|
|
||||||
);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".btn-discourse-subscriptions-payment-type").length,
|
find(".btn-discourse-subscriptions-payment-type").length,
|
||||||
2,
|
2,
|
||||||
@ -73,22 +60,40 @@ componentTest("Discourse Subscriptions payment options has content", {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest("Discourse Subscriptions payment options plan is selected", {
|
componentTest("Discourse Subscriptions payment type plan", {
|
||||||
template: `{{payment-options plans=plans}}`,
|
template: `{{payment-options plans=plans planTypeIsSelected=planTypeIsSelected}}`,
|
||||||
|
|
||||||
beforeEach() {},
|
|
||||||
|
|
||||||
async test(assert) {
|
async test(assert) {
|
||||||
this.set("plans", [
|
this.set("plans", [
|
||||||
EmberObject.create({
|
{ currency: "aud", interval: "year", amountDollars: "44.99" }
|
||||||
currency: "aud",
|
|
||||||
interval: "year",
|
|
||||||
amountDollars: "44.99"
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await click(".btn-discourse-subscriptions-subscribe:first-child");
|
this.set("planTypeIsSelected", true);
|
||||||
|
|
||||||
assert.ok(this.get("plans.firstObject.selected"), "it selected the plan");
|
assert.equal(
|
||||||
|
find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
|
||||||
|
1,
|
||||||
|
"The plan type button is selected"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
|
||||||
|
0,
|
||||||
|
"The payment type button is not selected"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click("#discourse-subscriptions-payment-type-payment");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
|
||||||
|
0,
|
||||||
|
"The plan type button is selected"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
|
||||||
|
1,
|
||||||
|
"The payment type button is not selected"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user