diff --git a/ghost/admin/app/components/gh-membership-products-alpha.hbs b/ghost/admin/app/components/gh-membership-products-alpha.hbs new file mode 100644 index 0000000000..70a6c9e103 --- /dev/null +++ b/ghost/admin/app/components/gh-membership-products-alpha.hbs @@ -0,0 +1,54 @@ +{{#each this.products as |product|}} +
+
+
+

+ {{product.name}} +

+

+ {{product.description}} +

+
+
+
+

+ Pricing +

+

+ {{product.monthlyPrice.currency}} + {{gh-price-amount product.monthlyPrice.amount}}/month +

+

+ {{product.monthlyPrice.currency}} + {{gh-price-amount product.yearlyPrice.amount}}/year +

+
+
+
+
+
+ +
+
+
+{{/each}} +
+
+

+ + New tier +

+
+
+ +{{#if this.showProductModal}} + +{{/if}} diff --git a/ghost/admin/app/components/gh-membership-products-alpha.js b/ghost/admin/app/components/gh-membership-products-alpha.js new file mode 100644 index 0000000000..f95a6d9c54 --- /dev/null +++ b/ghost/admin/app/components/gh-membership-products-alpha.js @@ -0,0 +1,41 @@ +import Component from '@glimmer/component'; +import {action} from '@ember/object'; +import {inject as service} from '@ember/service'; +import {tracked} from '@glimmer/tracking'; + +export default class extends Component { + @service membersUtils; + @service ghostPaths; + @service ajax; + @service store; + @service config; + + @tracked showProductModal = false; + @tracked productModel = null; + + get products() { + return this.args.products; + } + + @action + async openEditProduct(product) { + this.productModel = product; + this.showProductModal = true; + } + + @action + async openNewProduct() { + this.productModel = this.store.createRecord('product'); + this.showProductModal = true; + } + + @action + closeProductModal() { + this.showProductModal = false; + } + + @action + confirmProductSave() { + this.args.confirmProductSave(); + } +} diff --git a/ghost/admin/app/components/modal-product.hbs b/ghost/admin/app/components/modal-product.hbs new file mode 100644 index 0000000000..e7fce91ee6 --- /dev/null +++ b/ghost/admin/app/components/modal-product.hbs @@ -0,0 +1,106 @@ + + + +
+ +
+ + \ No newline at end of file diff --git a/ghost/admin/app/components/modal-product.js b/ghost/admin/app/components/modal-product.js new file mode 100644 index 0000000000..22370712e1 --- /dev/null +++ b/ghost/admin/app/components/modal-product.js @@ -0,0 +1,161 @@ +import EmberObject, {action} from '@ember/object'; +import ModalBase from 'ghost-admin/components/modal-base'; +import classic from 'ember-classic-decorator'; +import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency'; +import {isEmpty} from '@ember/utils'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency-decorators'; +import {tracked} from '@glimmer/tracking'; + +const CURRENCIES = currencies.map((currency) => { + return { + value: currency.isoCode.toLowerCase(), + label: `${currency.isoCode} - ${currency.name}`, + isoCode: currency.isoCode + }; +}); + +// TODO: update modals to work fully with Glimmer components +@classic +export default class ModalProductPrice extends ModalBase { + @service settings; + @tracked model; + @tracked product; + @tracked periodVal; + @tracked stripeMonthlyAmount = 5; + @tracked stripeYearlyAmount = 50; + @tracked currency = 'usd'; + @tracked errors = EmberObject.create(); + @tracked stripePlanError = ''; + + confirm() {} + + get allCurrencies() { + return getCurrencyOptions(); + } + + get selectedCurrency() { + return CURRENCIES.findBy('value', this.currency); + } + + init() { + super.init(...arguments); + this.product = this.model.product; + const monthlyPrice = this.product.get('monthlyPrice'); + const yearlyPrice = this.product.get('yearlyPrice'); + if (monthlyPrice) { + this.stripeMonthlyAmount = (monthlyPrice.amount / 100); + this.currency = monthlyPrice.currency; + } + if (yearlyPrice) { + this.stripeYearlyAmount = (yearlyPrice.amount / 100); + } + } + + get title() { + if (this.isExistingProduct) { + return `Product - ${this.product.name || 'No Name'}`; + } + return 'New Product'; + } + + get isExistingProduct() { + return !!this.model.product; + } + + // TODO: rename to confirm() when modals have full Glimmer support + @action + confirmAction() { + this.saveProduct.perform(); + } + + @action + close(event) { + event?.preventDefault?.(); + this.closeModal(); + } + @action + setCurrency(event) { + const newCurrency = event.value; + this.currency = newCurrency; + } + + @task({drop: true}) + *saveProduct() { + this.validatePrices(); + if (!isEmpty(this.errors) && Object.keys(this.errors).length > 0) { + return; + } + if (this.stripePlanError) { + return; + } + const monthlyAmount = this.stripeMonthlyAmount * 100; + const yearlyAmount = this.stripeYearlyAmount * 100; + this.product.set('monthlyPrice', { + nickname: 'Monthly', + amount: monthlyAmount, + active: true, + currency: this.currency, + interval: 'month', + type: 'recurring' + }); + this.product.set('yearlyPrice', { + nickname: 'Yearly', + amount: yearlyAmount, + active: true, + currency: this.currency, + interval: 'year', + type: 'recurring' + }); + const savedProduct = yield this.product.save(); + yield this.confirm(savedProduct); + this.send('closeModal'); + } + + validatePrices() { + this.stripePlanError = undefined; + + try { + const yearlyAmount = this.stripeYearlyAmount; + const monthlyAmount = this.stripeMonthlyAmount; + const symbol = getSymbol(this.currency); + if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) { + throw new TypeError(`Subscription amount must be at least ${symbol}1.00`); + } + } catch (err) { + this.stripePlanError = err.message; + } + } + + actions = { + confirm() { + this.confirmAction(...arguments); + }, + setAmount(amount) { + this.price.amount = !isNaN(amount) ? parseInt(amount) : 0; + }, + + setCurrency(event) { + const newCurrency = event.value; + this.currency = newCurrency; + }, + validateStripePlans() { + this.stripePlanError = undefined; + + try { + const yearlyAmount = this.stripeYearlyAmount; + const monthlyAmount = this.stripeMonthlyAmount; + const symbol = getSymbol(this.currency); + if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) { + throw new TypeError(`Subscription amount must be at least ${symbol}1.00`); + } + } catch (err) { + this.stripePlanError = err.message; + } + }, + // needed because ModalBase uses .send() for keyboard events + closeModal() { + this.close(); + } + } +} diff --git a/ghost/admin/app/helpers/gh-price-amount.js b/ghost/admin/app/helpers/gh-price-amount.js new file mode 100644 index 0000000000..6bb0beb5db --- /dev/null +++ b/ghost/admin/app/helpers/gh-price-amount.js @@ -0,0 +1,13 @@ +import {helper} from '@ember/component/helper'; + +export function ghPriceAmount(amount) { + if (amount) { + return Math.round(amount / 100); + } + return 0; +} + +// like {{pluralize}} but formats the number according to current locale +export default helper(function ([amount]) { + return ghPriceAmount(amount); +}); diff --git a/ghost/admin/app/styles/layouts/products.css b/ghost/admin/app/styles/layouts/products.css index b9f7c2c882..8e0523bc62 100644 --- a/ghost/admin/app/styles/layouts/products.css +++ b/ghost/admin/app/styles/layouts/products.css @@ -19,7 +19,9 @@ text-align: center; border: 1px solid var(--whitegrey); border-radius: 2px; - padding: 8vmin 48px; + padding: 24px 48px; + background: white; + margin-bottom: 12px; } @media (max-width: 980px) { @@ -251,4 +253,4 @@ display: grid; grid-template-columns: 1fr 2fr; grid-gap: 20px; -} \ No newline at end of file +} diff --git a/ghost/admin/app/templates/settings/membership.hbs b/ghost/admin/app/templates/settings/membership.hbs index ed052ff1e8..a2c08ebe8d 100644 --- a/ghost/admin/app/templates/settings/membership.hbs +++ b/ghost/admin/app/templates/settings/membership.hbs @@ -139,58 +139,65 @@ {{#if this.fetchDefaultProduct.isRunning}} Loading... {{else}} - -
- - -
- -
- {{this.currency}} - {{svg-jar "arrow-down-small"}} -
- + {{else}} + +
+ + +
+ +
+ {{this.currency}} + {{svg-jar "arrow-down-small"}} +
+ +
+
+
+
+ +
+ - + {{this.currency}}/month +
+
+ + {{this.currency}}/year +
-
-
- -
- - {{this.currency}}/month -
-
- - {{this.currency}}/year -
-
- {{#if this.stripePlanError}} -

{{this.stripePlanError}}

- {{/if}} - + {{#if this.stripePlanError}} +

{{this.stripePlanError}}

+ {{/if}} + + {{/if}}