diff --git a/assets/javascripts/discourse/components/donation-form.js.es6 b/assets/javascripts/discourse/components/donation-form.js.es6 new file mode 100644 index 0000000..6999995 --- /dev/null +++ b/assets/javascripts/discourse/components/donation-form.js.es6 @@ -0,0 +1,42 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + @computed("confirmation.card.last4") + last4() { + return this.get("confirmation.card.last4"); + }, + + init() { + this._super(...arguments); + + const settings = Discourse.SiteSettings; + + this.setProperties({ + confirmation: false, + currency: settings.discourse_donations_currency, + }); + }, + + actions: { + closeModal() { + this.set('paymentError', false); + this.set('confirmation', false); + }, + + handleConfirmStripeCard(paymentMethod) { + this.set('confirmation', paymentMethod); + }, + + confirmStripeCard() { + const paymentMethodId = this.confirmation.id; + this.stripePaymentHandler(paymentMethodId, this.amount).then((paymentIntent) => { + if (paymentIntent.error) { + this.set('paymentError', paymentIntent.error); + } + else { + console.log('ok done'); + } + }); + }, + }, +}); diff --git a/assets/javascripts/discourse/components/donation-list.js.es6 b/assets/javascripts/discourse/components/donation-list.js.es6 new file mode 100644 index 0000000..b9fcea7 --- /dev/null +++ b/assets/javascripts/discourse/components/donation-list.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Component.extend({ + classNames: "donation-list", + hasSubscriptions: Ember.computed.notEmpty("subscriptions"), + hasCharges: Ember.computed.notEmpty("charges") +}); diff --git a/assets/javascripts/discourse/components/donation-row.js.es6 b/assets/javascripts/discourse/components/donation-row.js.es6 new file mode 100644 index 0000000..fb2ab1a --- /dev/null +++ b/assets/javascripts/discourse/components/donation-row.js.es6 @@ -0,0 +1,99 @@ +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { formatAnchor, formatAmount } from "../lib/donation-utilities"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import showModal from "discourse/lib/show-modal"; + +export default Ember.Component.extend({ + classNameBindings: [":donation-row", "canceled", "updating"], + includePrefix: Ember.computed.or("invoice", "charge"), + canceled: Ember.computed.equal("subscription.status", "canceled"), + + @computed("subscription", "invoice", "charge", "customer") + data(subscription, invoice, charge, customer) { + if (subscription) { + return $.extend({}, subscription.plan, { + anchor: subscription.billing_cycle_anchor + }); + } else if (invoice) { + let receiptSent = false; + + if (invoice.receipt_number && customer.email) { + receiptSent = true; + } + + return $.extend({}, invoice.lines.data[0], { + anchor: invoice.date, + invoiceLink: invoice.invoice_pdf, + receiptSent + }); + } else if (charge) { + let receiptSent = false; + + if (charge.receipt_number && charge.receipt_email) { + receiptSent = true; + } + + return $.extend({}, charge, { + anchor: charge.created, + receiptSent + }); + } + }, + + @computed("data.currency") + currency(currency) { + return currency ? currency.toUpperCase() : null; + }, + + @computed("data.amount", "currency") + amount(amount, currency) { + return formatAmount(amount, currency); + }, + + @computed("data.interval") + interval(interval) { + return interval || "once"; + }, + + @computed("data.anchor", "interval") + period(anchor, interval) { + return I18n.t(`discourse_donations.period.${interval}`, { + anchor: formatAnchor(interval, moment.unix(anchor)) + }); + }, + + cancelSubscription() { + const subscriptionId = this.get("subscription.id"); + this.set("updating", true); + + ajax("/donate/charges/cancel-subscription", { + data: { + subscription_id: subscriptionId + }, + method: "put" + }) + .then(result => { + if (result.success) { + this.set("subscription", result.subscription); + } + }) + .catch(popupAjaxError) + .finally(() => { + this.set("updating", false); + }); + }, + + actions: { + cancelSubscription() { + showModal("cancel-subscription", { + model: { + currency: this.get("currency"), + amount: this.get("amount"), + period: this.get("period"), + confirm: () => this.cancelSubscription() + } + }); + } + } +}); diff --git a/assets/javascripts/discourse/components/stripe-card.js.es6 b/assets/javascripts/discourse/components/stripe-card.js.es6 new file mode 100644 index 0000000..1eab3f1 --- /dev/null +++ b/assets/javascripts/discourse/components/stripe-card.js.es6 @@ -0,0 +1,59 @@ + +export default Ember.Component.extend({ + init() { + this._super(...arguments); + + const settings = Discourse.SiteSettings; + + this.setProperties({ + cardError: false, + color: jQuery("body").css("color"), + backgroundColor: jQuery("body").css("background-color"), + stripe: Stripe(settings.discourse_patrons_public_key), + }); + }, + + didInsertElement() { + this._super(...arguments); + + const color = this.get('color'); + + const style = { + base: { + color, + iconColor: color, + "::placeholder": { color } + } + }; + + const elements = this.stripe.elements(); + const card = elements.create("card", { style, hidePostalCode: true }); + + card.mount('#card-element'); + + this.set("card", card); + + card.on("change", (result) => { + this.set('cardError', false); + + if(result.error) { + this.set('cardError', result.error.message); + } + }); + }, + + actions: { + submitStripeCard() { + this.stripe.createPaymentMethod('card', this.card).then((result) => { + if (result.error) { + this.set('cardError', result.error.message); + } + else { + this.handleConfirmStripeCard(result.paymentMethod); + } + }, () => { + this.set('cardError', 'Unknown error.'); + }); + }, + }, +}); diff --git a/assets/javascripts/discourse/templates/components/donation-form.hbs b/assets/javascripts/discourse/templates/components/donation-form.hbs new file mode 100644 index 0000000..641cfb1 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/donation-form.hbs @@ -0,0 +1,57 @@ + +{{#if confirmation}} + {{#d-modal closeModal=(action "closeModal") modalStyle="inline-modal" title=(i18n "discourse_donations.confirm")}} + {{#d-modal-body}} + + + + + + + + + +
Amount{{amount}}
Card.... .... .... {{last4}}
+ + {{/d-modal-body}} + + + + {{/d-modal}} + + {{#if paymentError}} + + {{/if}} + +{{else}} +
+
+

Your information

+ +
+
+
Payment Amount
+
+ {{input value=amount}}
+ Enter {{currency}} +
+
+
+
+
+
+ {{stripe-card + amount=amount + currency=currency + handleConfirmStripeCard=(action "handleConfirmStripeCard") + }} +
+
+
+{{/if}} diff --git a/assets/javascripts/discourse/templates/components/donation-list.hbs b/assets/javascripts/discourse/templates/components/donation-list.hbs new file mode 100644 index 0000000..4a23964 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/donation-list.hbs @@ -0,0 +1,28 @@ +{{#if hasSubscriptions}} +
+
{{i18n 'discourse_donations.donations.subscriptions'}}
+ +
+{{/if}} + +{{#if hasCharges}} +
+
{{i18n 'discourse_donations.donations.charges'}}
+ +
+{{/if}} diff --git a/assets/javascripts/discourse/templates/components/donation-row.hbs b/assets/javascripts/discourse/templates/components/donation-row.hbs new file mode 100644 index 0000000..0e6d17a --- /dev/null +++ b/assets/javascripts/discourse/templates/components/donation-row.hbs @@ -0,0 +1,41 @@ +{{#if includePrefix}} + {{i18n 'discourse_donations.invoice_prefix'}} +{{/if}} + +{{currency}} + +{{amount}} + +{{period}} + +{{#if invoice}} + ({{i18n 'discourse_donations.invoice'}}) +{{/if}} + +{{#if currentUser}} + {{#if subscription}} + + {{#if updating}} + {{loading-spinner size='small'}} + {{else}} + {{#unless canceled}} + + {{i18n 'cancel'}} + + {{/unless}} + {{/if}} + + {{/if}} +{{/if}} + +{{#if receiptSent}} + + {{i18n 'discourse_donations.receipt' email=customer.email}} +{{/if}} + +{{#if new}} + + {{d-icon 'circle'}} + {{i18n 'new_item'}} + +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/stripe-card.hbs b/assets/javascripts/discourse/templates/components/stripe-card.hbs new file mode 100644 index 0000000..0229be7 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/stripe-card.hbs @@ -0,0 +1,16 @@ + +

Credit card information

+ +
+ +
+ {{#d-button action="submitStripeCard" class="btn btn-primary btn-payment"}} + {{i18n 'discourse_donations.confirm'}}
{{amount}} + {{/d-button}} + + {{#if cardError}} + + {{/if}} +
diff --git a/test/javascripts/components/donation-form-test.es6 b/test/javascripts/components/donation-form-test.es6 new file mode 100644 index 0000000..1bd5933 --- /dev/null +++ b/test/javascripts/components/donation-form-test.es6 @@ -0,0 +1,45 @@ +import componentTest from "helpers/component-test"; + +moduleForComponent("donation-form", { integration: true }); + +componentTest("Discourse Patrons donation form has content", { + template: `{{donation-form}}`, + + beforeEach() { + this.registry.register( + "component:stripe-card", + Ember.Component.extend({ tagName: "dummy-component-tag" }) + ); + }, + + async test(assert) { + assert.ok(find("#payment-form").length, "The form renders"); + assert.ok( + find("dummy-component-tag").length, + "The stripe component renders" + ); + } +}); + +componentTest("donation form has a confirmation", { + template: `{{donation-form confirmation=confirmation}}`, + + beforeEach() { + this.registry.register( + "component:stripe-card", + Ember.Component.extend() + ); + }, + + async test(assert) { + this.set("confirmation", { "card": { "last4": "4242" }}); + + const confirmExists = find(".discourse-donations-confirmation").length; + + assert.ok(confirmExists, "The confirmation form renders"); + + const last4 = find(".discourse-donations-last4").text().trim(); + + assert.equal(last4, ".... .... .... 4242", "The last 4 digits are correct"); + } +}); diff --git a/test/javascripts/components/donation-row-test.js.es6 b/test/javascripts/components/donation-row-test.js.es6 new file mode 100644 index 0000000..14cfd2e --- /dev/null +++ b/test/javascripts/components/donation-row-test.js.es6 @@ -0,0 +1,33 @@ +import componentTest from "helpers/component-test"; + +moduleForComponent("donation-row", { integration: true }); + +componentTest("Discourse Patrons donation-row", { + template: `{{donation-row currency=3 amount=21 period='monthly'}}`, + + test(assert) { + assert.equal(find(".donation-row-currency").text(), "3", "It has currency"); + assert.equal(find(".donation-row-amount").text(), "21", "It has an amount"); + assert.equal( + find(".donation-row-period").text(), + "monthly", + "It has a period" + ); + } +}); + +componentTest("donation-row cancels subscription", { + template: `{{donation-row currentUser=currentUser subscription=subscription}}`, + + beforeEach() { + this.set("currentUser", true); + this.set("subscription", true); + }, + + async test(assert) { + assert.ok( + find(".donation-row-subscription").length, + "It has a subscription" + ); + } +}); diff --git a/test/javascripts/components/stripe-card-test.js.es6 b/test/javascripts/components/stripe-card-test.js.es6 new file mode 100644 index 0000000..5bfee5b --- /dev/null +++ b/test/javascripts/components/stripe-card-test.js.es6 @@ -0,0 +1,40 @@ +import componentTest from "helpers/component-test"; + +moduleForComponent("stripe-card", { integration: true }); + +componentTest("Discourse Patrons stripe card success", { + template: `{{stripe-card handleConfirmStripeCard=onSubmit}}`, + + beforeEach() { + window.Stripe = () => { + return { + createPaymentMethod() { + return new Ember.RSVP.Promise((resolve) => { + resolve('payment-method-response'); + }); + }, + elements() { + return { + create() { + return { + on() {}, + card() {}, + mount() {}, + }; + }, + }; + }, + }; + }; + }, + + async test(assert) { + assert.expect(1); + + this.set("onSubmit", (arg) => { + assert.equal(arg, "payment-method-response", "payment method created"); + }); + + await click(".btn-payment"); + }, +});