diff --git a/app/controllers/discourse_subscriptions/user/subscriptions_controller.rb b/app/controllers/discourse_subscriptions/user/subscriptions_controller.rb index 6eb661f..0e86e6a 100644 --- a/app/controllers/discourse_subscriptions/user/subscriptions_controller.rb +++ b/app/controllers/discourse_subscriptions/user/subscriptions_controller.rb @@ -63,6 +63,31 @@ module DiscourseSubscriptions render_json_error e.message end end + + def update + params.require(:payment_method) + + subscription = Subscription.where(external_id: params[:id]).first + begin + attach_method_to_customer(subscription.customer_id, params[:payment_method]) + subscription = ::Stripe::Subscription.update(params[:id], { default_payment_method: params[:payment_method] }) + render json: success_json + rescue ::Stripe::InvalidRequestError + render_json_error I18n.t("discourse_subscriptions.card.invalid") + end + end + + private + + def attach_method_to_customer(customer_id, method) + customer = Customer.find(customer_id) + ::Stripe::PaymentMethod.attach( + method, + { + customer: customer.customer_id + } + ) + end end end end diff --git a/assets/javascripts/discourse/controllers/user-billing-subscriptions-card.js b/assets/javascripts/discourse/controllers/user-billing-subscriptions-card.js new file mode 100644 index 0000000..3f0b3eb --- /dev/null +++ b/assets/javascripts/discourse/controllers/user-billing-subscriptions-card.js @@ -0,0 +1,56 @@ +import Controller from "@ember/controller"; +import { action } from "@ember/object"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import I18n from "I18n"; +import bootbox from "bootbox"; + +export default Controller.extend({ + loading: false, + saved: false, + init() { + this._super(...arguments); + this.set( + "stripe", + Stripe(this.siteSettings.discourse_subscriptions_public_key) + ); + const elements = this.get("stripe").elements(); + this.set("cardElement", elements.create("card", { hidePostalCode: true })); + }, + + @action + async updatePaymentMethod() { + this.set("loading", true); + this.set("saved", false); + + const paymentMethodObject = await this.stripe.createPaymentMethod({ + type: "card", + card: this.cardElement, + }); + + if (paymentMethodObject.error) { + bootbox.alert( + paymentMethodObject.error?.message || I18n.t("generic_error") + ); + this.set("loading", false); + return; + } + + const paymentMethod = paymentMethodObject.paymentMethod.id; + + try { + await ajax(`/s/user/subscriptions/${this.model}`, { + method: "PUT", + data: { + payment_method: paymentMethod, + }, + }); + this.set("saved", true); + } catch (err) { + popupAjaxError(err); + } finally { + this.set("loading", false); + this.cardElement?.clear(); + } + }, +}); diff --git a/assets/javascripts/discourse/discourse-subscriptions-user-route-map.js b/assets/javascripts/discourse/discourse-subscriptions-user-route-map.js index d80c8ce..5f382db 100644 --- a/assets/javascripts/discourse/discourse-subscriptions-user-route-map.js +++ b/assets/javascripts/discourse/discourse-subscriptions-user-route-map.js @@ -4,7 +4,9 @@ export default { map() { this.route("billing", function () { this.route("payments"); - this.route("subscriptions"); + this.route("subscriptions", function () { + this.route("card", { path: "/card/:stripe-subscription-id" }); + }); }); }, }; diff --git a/assets/javascripts/discourse/routes/user-billing-index.js b/assets/javascripts/discourse/routes/user-billing-index.js index c728e7e..66bca47 100644 --- a/assets/javascripts/discourse/routes/user-billing-index.js +++ b/assets/javascripts/discourse/routes/user-billing-index.js @@ -4,6 +4,6 @@ export default Route.extend({ templateName: "user/billing/index", redirect() { - this.transitionTo("user.billing.subscriptions"); + this.transitionTo("user.billing.subscriptions.index"); }, }); diff --git a/assets/javascripts/discourse/routes/user-billing-subscriptions-card.js b/assets/javascripts/discourse/routes/user-billing-subscriptions-card.js new file mode 100644 index 0000000..ea5cc73 --- /dev/null +++ b/assets/javascripts/discourse/routes/user-billing-subscriptions-card.js @@ -0,0 +1,7 @@ +import Route from "@ember/routing/route"; + +export default Route.extend({ + model(params) { + return params["stripe-subscription-id"]; + }, +}); diff --git a/assets/javascripts/discourse/routes/user-billing-subscriptions-index.js b/assets/javascripts/discourse/routes/user-billing-subscriptions-index.js new file mode 100644 index 0000000..84a2899 --- /dev/null +++ b/assets/javascripts/discourse/routes/user-billing-subscriptions-index.js @@ -0,0 +1,42 @@ +import Route from "@ember/routing/route"; +import UserSubscription from "discourse/plugins/discourse-subscriptions/discourse/models/user-subscription"; +import I18n from "I18n"; +import { action } from "@ember/object"; +import bootbox from "bootbox"; + +export default Route.extend({ + model() { + return UserSubscription.findAll(); + }, + + @action + updateCard(subscriptionId) { + this.transitionTo("user.billing.subscriptions.card", subscriptionId); + }, + @action + cancelSubscription(subscription) { + bootbox.confirm( + I18n.t( + "discourse_subscriptions.user.subscriptions.operations.destroy.confirm" + ), + I18n.t("no_value"), + I18n.t("yes_value"), + (confirmed) => { + if (confirmed) { + subscription.set("loading", true); + + subscription + .destroy() + .then((result) => subscription.set("status", result.status)) + .catch((data) => + bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")) + ) + .finally(() => { + subscription.set("loading", false); + this.refresh(); + }); + } + } + ); + }, +}); diff --git a/assets/javascripts/discourse/routes/user-billing-subscriptions.js b/assets/javascripts/discourse/routes/user-billing-subscriptions.js index 61e970d..0051f5c 100644 --- a/assets/javascripts/discourse/routes/user-billing-subscriptions.js +++ b/assets/javascripts/discourse/routes/user-billing-subscriptions.js @@ -1,40 +1,3 @@ import Route from "@ember/routing/route"; -import UserSubscription from "discourse/plugins/discourse-subscriptions/discourse/models/user-subscription"; -import I18n from "I18n"; -import { action } from "@ember/object"; -import bootbox from "bootbox"; -export default Route.extend({ - templateName: "user/billing/subscriptions", - - model() { - return UserSubscription.findAll(); - }, - - @action - cancelSubscription(subscription) { - bootbox.confirm( - I18n.t( - "discourse_subscriptions.user.subscriptions.operations.destroy.confirm" - ), - I18n.t("no_value"), - I18n.t("yes_value"), - (confirmed) => { - if (confirmed) { - subscription.set("loading", true); - - subscription - .destroy() - .then((result) => subscription.set("status", result.status)) - .catch((data) => - bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")) - ) - .finally(() => { - subscription.set("loading", false); - this.refresh(); - }); - } - } - ); - }, -}); +export default Route.extend(); diff --git a/assets/javascripts/discourse/templates/user/billing/subscriptions/card.hbs b/assets/javascripts/discourse/templates/user/billing/subscriptions/card.hbs new file mode 100644 index 0000000..8b351b2 --- /dev/null +++ b/assets/javascripts/discourse/templates/user/billing/subscriptions/card.hbs @@ -0,0 +1,17 @@ +