buttons for selecting price and one time payment
This commit is contained in:
parent
e27b55ea6f
commit
57fb508514
|
@ -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({
|
||||
planButtonSelected: equal("planTypeIsSelected", true),
|
||||
paymentButtonSelected: equal("planTypeIsSelected", false),
|
||||
|
||||
actions: {
|
||||
selectPlans() {
|
||||
this.set("planTypeIsSelected", true);
|
||||
},
|
||||
|
||||
selectPayments() {
|
||||
this.set("planTypeIsSelected", false);
|
||||
},
|
||||
|
||||
clickPlan(plan) {
|
||||
this.plans.map(p => p.set("selected", false));
|
||||
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";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
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 computed from "discourse-common/utils/decorators";
|
||||
import { i18n } from "discourse/lib/computed";
|
||||
|
||||
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() {
|
||||
this._super(...arguments);
|
||||
this.set(
|
||||
|
@ -13,54 +28,83 @@ export default Ember.Controller.extend({
|
|||
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: {
|
||||
stripePaymentHandler() {
|
||||
this.set("loading", true);
|
||||
const type = this.get("type");
|
||||
const plan = this.get("model.plans")
|
||||
.filterBy("selected")
|
||||
.get("firstObject");
|
||||
|
||||
if (!plan) {
|
||||
bootbox.alert(
|
||||
I18n.t(
|
||||
"discourse_subscriptions.transactions.payment.validate.plan.required"
|
||||
)
|
||||
);
|
||||
|
||||
this.alert(`${type}.validate.payment_options.required`);
|
||||
this.set("loading", false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stripe.createToken(this.get("cardElement")).then(result => {
|
||||
if (result.error) {
|
||||
bootbox.alert(result.error.message);
|
||||
let transaction;
|
||||
|
||||
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);
|
||||
} 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
id="discourse-subscriptions-payment-type-plan"
|
||||
selected=true
|
||||
class="btn-discourse-subscriptions-payment-type"
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.plans.purchase"}}
|
||||
{{/ds-button}}
|
||||
<div class="subscribe-buttons">
|
||||
{{#ds-button
|
||||
id="discourse-subscriptions-payment-type-plan"
|
||||
selected=planButtonSelected
|
||||
action="selectPlans"
|
||||
class="btn-discourse-subscriptions-payment-type"
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.plans.purchase"}}
|
||||
{{/ds-button}}
|
||||
|
||||
{{#ds-button
|
||||
id="discourse-subscriptions-payment-type-payment"
|
||||
selected=false
|
||||
class="btn-discourse-subscriptions-payment-type"
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.payment.purchase"}}
|
||||
{{/ds-button}}
|
||||
{{#ds-button
|
||||
id="discourse-subscriptions-payment-type-payment"
|
||||
selected=paymentButtonSelected
|
||||
action="selectPayments"
|
||||
class="btn-discourse-subscriptions-payment-type"
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.payment.purchase"}}
|
||||
{{/ds-button}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
{{i18n "discourse_subscriptions.plans.select"}}
|
||||
{{#if planTypeIsSelected}}
|
||||
{{i18n "discourse_subscriptions.plans.select"}}
|
||||
{{else}}
|
||||
{{i18n "discourse_subscriptions.payment.select"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<div id="subscribe-buttons">
|
||||
<div class="subscribe-buttons">
|
||||
{{#each plans as |plan|}}
|
||||
{{#ds-button
|
||||
action="clickPlan"
|
||||
|
@ -30,7 +38,11 @@
|
|||
class="btn-discourse-subscriptions-subscribe"
|
||||
}}
|
||||
<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>
|
||||
<span class="amount">
|
||||
{{format-currency plan.currency plan.amountDollars}}
|
||||
|
|
|
@ -14,7 +14,10 @@
|
|||
{{i18n 'discourse_subscriptions.subscribe.card.title'}}
|
||||
</h2>
|
||||
|
||||
{{payment-options plans=model.plans}}
|
||||
{{payment-options
|
||||
plans=model.plans
|
||||
planTypeIsSelected=planTypeIsSelected
|
||||
}}
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -27,7 +30,7 @@
|
|||
disabled=loading
|
||||
action="stripePaymentHandler"
|
||||
class="btn btn-primary btn-payment"}}
|
||||
{{i18n 'discourse_subscriptions.subscribe.buttons.subscribe'}}
|
||||
{{buttonText}}
|
||||
{{/d-button}}
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#subscribe-buttons {
|
||||
.subscribe-buttons {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
|
|
|
@ -18,26 +18,35 @@ en:
|
|||
title: Discourse Subscriptions
|
||||
admin_navigation: Subscriptions
|
||||
optional: Optional
|
||||
transactions:
|
||||
payment:
|
||||
success: Your payment was successful
|
||||
validate:
|
||||
plan:
|
||||
required: Please select a subscription plan.
|
||||
navigation:
|
||||
subscriptions: Subscriptions
|
||||
subscribe: Subscribe
|
||||
billing: Billing
|
||||
plans:
|
||||
select: Select subscription plan
|
||||
purchase: Purchase a subscription
|
||||
select: Select subscription plan
|
||||
interval:
|
||||
adverb:
|
||||
week: Weekly
|
||||
month: Monthly
|
||||
year: Yearly
|
||||
payment_button:
|
||||
Subscribe
|
||||
success: Thank you! Your subscription has been created.
|
||||
validate:
|
||||
payment_options:
|
||||
required: Please select a subscription plan.
|
||||
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:
|
||||
heading:
|
||||
payment: Make a Payment
|
||||
|
|
|
@ -18,6 +18,7 @@ DiscourseSubscriptions::Engine.routes.draw do
|
|||
|
||||
resources :customers, only: [:create]
|
||||
resources :invoices, only: [:index]
|
||||
resources :payments, only: [:create]
|
||||
resources :patrons, only: [:index, :create]
|
||||
resources :plans, only: [:index]
|
||||
resources :products, only: [:index, :show]
|
||||
|
|
|
@ -62,6 +62,7 @@ after_initialize do
|
|||
"../app/controllers/invoices_controller",
|
||||
"../app/controllers/patrons_controller",
|
||||
"../app/controllers/plans_controller",
|
||||
"../app/controllers/payments_controller",
|
||||
"../app/controllers/products_controller",
|
||||
"../app/controllers/subscriptions_controller",
|
||||
"../app/models/customer",
|
||||
|
|
|
@ -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", {
|
||||
template: `{{payment-options plans=plans}}`,
|
||||
template: `{{payment-options plans=plans planTypeIsSelected=planTypeIsSelected}}`,
|
||||
|
||||
async test(assert) {
|
||||
this.set("plans", [
|
||||
|
@ -26,21 +26,8 @@ componentTest("Discourse Subscriptions payment options has content", {
|
|||
{ currency: "gdp", interval: "month", amountDollars: "9.99" }
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
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"
|
||||
);
|
||||
this.set("planTypeIsSelected", true);
|
||||
|
||||
assert.equal(
|
||||
find(".btn-discourse-subscriptions-payment-type").length,
|
||||
2,
|
||||
|
@ -73,22 +60,40 @@ componentTest("Discourse Subscriptions payment options has content", {
|
|||
}
|
||||
});
|
||||
|
||||
componentTest("Discourse Subscriptions payment options plan is selected", {
|
||||
template: `{{payment-options plans=plans}}`,
|
||||
|
||||
beforeEach() {},
|
||||
componentTest("Discourse Subscriptions payment type plan", {
|
||||
template: `{{payment-options plans=plans planTypeIsSelected=planTypeIsSelected}}`,
|
||||
|
||||
async test(assert) {
|
||||
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…
Reference in New Issue