mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Added UI for handling multiple tiers in membership settings
refs https://github.com/TryGhost/Team/issues/715 Adds new modal and component to handle managing a list of products(tiers) in Admin behind the developer experiments flag. Also adds a new helper for stripe prices to convert amount from decimal value.
This commit is contained in:
parent
c10f8d014a
commit
6165441c30
7 changed files with 435 additions and 51 deletions
54
ghost/admin/app/components/gh-membership-products-alpha.hbs
Normal file
54
ghost/admin/app/components/gh-membership-products-alpha.hbs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{{#each this.products as |product|}}
|
||||||
|
<div class="gh-product-card" style="flex-direction: row;align-items: stretch">
|
||||||
|
<div style="display: flex;flex-grow: 1;flex-shrink: 0;flex-basis: 50%">
|
||||||
|
<div style="display: flex;flex-direction:column;align-items: flex-start;flex-grow: 1">
|
||||||
|
<h3 class="gh-product-card-name">
|
||||||
|
{{product.name}}
|
||||||
|
</h3>
|
||||||
|
<p class="gh-product-card-description">
|
||||||
|
{{product.description}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;flex-direction:column;align-items: flex-start;flex-grow: 1; flex-shrink: 0; border-left: 1px solid #c4cad2;padding-left: 12px;flex-basis: 50%">
|
||||||
|
<h3 class="gh-product-card-name" style="margin-bottom: 12px">
|
||||||
|
Pricing
|
||||||
|
</h3>
|
||||||
|
<p class="gh-product-card-description">
|
||||||
|
<span class="ttu" style="font-weight: bold">{{product.monthlyPrice.currency}}</span>
|
||||||
|
<span style="font-weight: bold">{{gh-price-amount product.monthlyPrice.amount}}</span>/month
|
||||||
|
</p>
|
||||||
|
<p class="gh-product-card-description" style="margin-bottom: 0">
|
||||||
|
<span class="ttu" style="font-weight: bold">{{product.monthlyPrice.currency}}</span>
|
||||||
|
<span style="font-weight: bold">{{gh-price-amount product.yearlyPrice.amount}}</span>/year
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;align-items: center">
|
||||||
|
<div>
|
||||||
|
<button class="gh-btn gh-btn-link" {{action "openEditProduct" product}}>
|
||||||
|
<span>Edit</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
<div class="gh-product-card" {{action "openNewProduct" product}} style="cursor: pointer">
|
||||||
|
<div style="display: flex;flex-direction:column;align-items: flex-start;flex-grow: 1">
|
||||||
|
<h3 class="gh-product-card-name">
|
||||||
|
+ New tier
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.showProductModal}}
|
||||||
|
<GhFullscreenModal
|
||||||
|
@modal="product"
|
||||||
|
@model={{hash
|
||||||
|
product=this.productModel
|
||||||
|
}}
|
||||||
|
@confirm={{this.confirmProductSave}}
|
||||||
|
@close={{this.closeProductModal}}
|
||||||
|
@modifier="action wide" />
|
||||||
|
{{/if}}
|
41
ghost/admin/app/components/gh-membership-products-alpha.js
Normal file
41
ghost/admin/app/components/gh-membership-products-alpha.js
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
106
ghost/admin/app/components/modal-product.hbs
Normal file
106
ghost/admin/app/components/modal-product.hbs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<header class="modal-header" data-test-modal="webhook-form">
|
||||||
|
<h1 data-test-text="title">{{this.title}}</h1>
|
||||||
|
</header>
|
||||||
|
<button class="close" href title="Close" {{action "closeModal"}} {{action (optional this.noop) on="mouseDown"}}>
|
||||||
|
{{svg-jar "close"}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="gh-main-section-block">
|
||||||
|
<div class="gh-main-section-content grey gh-product-priceform-block">
|
||||||
|
<GhFormGroup @errors={{this.errors}} @property="name">
|
||||||
|
<label for="name" class="fw6">Name</label>
|
||||||
|
<GhTextInput
|
||||||
|
@value={{readonly this.product.name}}
|
||||||
|
@input={{action (mut this.product.name) value="target.value"}}
|
||||||
|
@name="name"
|
||||||
|
@id="name"
|
||||||
|
@class="gh-input" />
|
||||||
|
<GhErrorMessage @errors={{this.errors}} @property="name" />
|
||||||
|
</GhFormGroup>
|
||||||
|
<GhFormGroup @errors={{this.errors}} @property="description">
|
||||||
|
<label for="description" class="fw6">Description</label>
|
||||||
|
<GhTextInput
|
||||||
|
@value={{readonly this.product.description}}
|
||||||
|
@input={{action (mut this.product.description) value="target.value"}}
|
||||||
|
@name="description"
|
||||||
|
@id="description"
|
||||||
|
@class="gh-input" />
|
||||||
|
<GhErrorMessage @errors={{this.errors}} @property="description" />
|
||||||
|
</GhFormGroup>
|
||||||
|
<div class="">
|
||||||
|
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices">
|
||||||
|
<div class="gh-settings-members-pricelabelcont">
|
||||||
|
<label for="monthlyPrice">Prices</label>
|
||||||
|
<span>–</span>
|
||||||
|
<div>
|
||||||
|
<span class="gh-setting-members-currency gh-select">
|
||||||
|
<div class="gh-setting-members-currencylabel">
|
||||||
|
<span>{{this.currency}}</span>
|
||||||
|
{{svg-jar "arrow-down-small"}}
|
||||||
|
</div>
|
||||||
|
<OneWaySelect
|
||||||
|
@value={{this.selectedCurrency}}
|
||||||
|
id="currency"
|
||||||
|
name="currency"
|
||||||
|
@options={{readonly this.allCurrencies}}
|
||||||
|
@optionValuePath="value"
|
||||||
|
@optionLabelPath="label"
|
||||||
|
@update={{action "setCurrency"}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gh-setting-members-prices">
|
||||||
|
|
||||||
|
<div class="gh-input-group">
|
||||||
|
<GhTextInput
|
||||||
|
@id="monthlyPrice"
|
||||||
|
@value={{readonly this.stripeMonthlyAmount}}
|
||||||
|
@type="number"
|
||||||
|
@input={{action (mut this.stripeMonthlyAmount) value="target.value"}}
|
||||||
|
@focus-out={{action "validateStripePlans"}}
|
||||||
|
/>
|
||||||
|
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/month</span>
|
||||||
|
</div>
|
||||||
|
<div class="gh-input-group">
|
||||||
|
<GhTextInput
|
||||||
|
@id="yearlyPrice"
|
||||||
|
@value={{readonly this.stripeYearlyAmount}}
|
||||||
|
@type="number"
|
||||||
|
@input={{action (mut this.stripeYearlyAmount) value="target.value"}}
|
||||||
|
@focus-out={{this.validateStripePlans}}
|
||||||
|
@placeholder=''
|
||||||
|
data-test-title-input={{true}}
|
||||||
|
/>
|
||||||
|
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/year</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{#if this.stripePlanError}}
|
||||||
|
<p class="response w-100"><span class="red">{{this.stripePlanError}}</span></p>
|
||||||
|
{{/if}}
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
class="gh-btn"
|
||||||
|
{{action "closeModal"}}
|
||||||
|
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
||||||
|
{{action (optional this.noop) on="mouseDown"}}
|
||||||
|
data-test-button="cancel-webhook"
|
||||||
|
>
|
||||||
|
<span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
<GhTaskButton @buttonText="Save"
|
||||||
|
@successText={{this.successText}}
|
||||||
|
@task={{this.saveProduct}}
|
||||||
|
@idleClass="gh-btn-primary"
|
||||||
|
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||||
|
data-test-button="save-product" />
|
||||||
|
</div>
|
161
ghost/admin/app/components/modal-product.js
Normal file
161
ghost/admin/app/components/modal-product.js
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
ghost/admin/app/helpers/gh-price-amount.js
Normal file
13
ghost/admin/app/helpers/gh-price-amount.js
Normal file
|
@ -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);
|
||||||
|
});
|
|
@ -19,7 +19,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid var(--whitegrey);
|
border: 1px solid var(--whitegrey);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 8vmin 48px;
|
padding: 24px 48px;
|
||||||
|
background: white;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
|
@ -251,4 +253,4 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr;
|
grid-template-columns: 1fr 2fr;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,58 +139,65 @@
|
||||||
{{#if this.fetchDefaultProduct.isRunning}}
|
{{#if this.fetchDefaultProduct.isRunning}}
|
||||||
Loading...
|
Loading...
|
||||||
{{else}}
|
{{else}}
|
||||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices">
|
{{#if (enable-developer-experiments)}}
|
||||||
<div class="gh-settings-members-pricelabelcont">
|
<GhMembershipProductsAlpha
|
||||||
<label for="monthlyPrice">Prices</label>
|
@products={{this.products}}
|
||||||
<span>–</span>
|
@confirmProductSave={{this.confirmProductSave}}
|
||||||
<div>
|
/>
|
||||||
<span class="gh-setting-members-currency gh-select">
|
{{else}}
|
||||||
<div class="gh-setting-members-currencylabel">
|
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices">
|
||||||
<span>{{this.currency}}</span>
|
<div class="gh-settings-members-pricelabelcont">
|
||||||
{{svg-jar "arrow-down-small"}}
|
<label for="monthlyPrice">Prices</label>
|
||||||
</div>
|
<span>–</span>
|
||||||
<OneWaySelect
|
<div>
|
||||||
@value={{this.selectedCurrency}}
|
<span class="gh-setting-members-currency gh-select">
|
||||||
id="currency"
|
<div class="gh-setting-members-currencylabel">
|
||||||
name="currency"
|
<span>{{this.currency}}</span>
|
||||||
@options={{readonly this.allCurrencies}}
|
{{svg-jar "arrow-down-small"}}
|
||||||
@optionValuePath="value"
|
</div>
|
||||||
@optionLabelPath="label"
|
<OneWaySelect
|
||||||
@update={{this.setStripePlansCurrency}}
|
@value={{this.selectedCurrency}}
|
||||||
|
id="currency"
|
||||||
|
name="currency"
|
||||||
|
@options={{readonly this.allCurrencies}}
|
||||||
|
@optionValuePath="value"
|
||||||
|
@optionLabelPath="label"
|
||||||
|
@update={{this.setStripePlansCurrency}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gh-setting-members-prices">
|
||||||
|
|
||||||
|
<div class="gh-input-group">
|
||||||
|
<GhTextInput
|
||||||
|
@id="monthlyPrice"
|
||||||
|
@value={{readonly this.stripeMonthlyAmount}}
|
||||||
|
@type="number"
|
||||||
|
@input={{action (mut this.stripeMonthlyAmount) value="target.value"}}
|
||||||
|
@focus-out={{action "validateStripePlans"}}
|
||||||
/>
|
/>
|
||||||
</span>
|
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/month</span>
|
||||||
|
</div>
|
||||||
|
<div class="gh-input-group">
|
||||||
|
<GhTextInput
|
||||||
|
@id="yearlyPrice"
|
||||||
|
@value={{readonly this.stripeYearlyAmount}}
|
||||||
|
@type="number"
|
||||||
|
@input={{action (mut this.stripeYearlyAmount) value="target.value"}}
|
||||||
|
@focus-out={{this.validateStripePlans}}
|
||||||
|
@placeholder=''
|
||||||
|
data-test-title-input={{true}}
|
||||||
|
/>
|
||||||
|
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/year</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{#if this.stripePlanError}}
|
||||||
<div class="gh-setting-members-prices">
|
<p class="response w-100"><span class="red">{{this.stripePlanError}}</span></p>
|
||||||
|
{{/if}}
|
||||||
<div class="gh-input-group">
|
</GhFormGroup>
|
||||||
<GhTextInput
|
|
||||||
@id="monthlyPrice"
|
|
||||||
@value={{readonly this.stripeMonthlyAmount}}
|
|
||||||
@type="number"
|
|
||||||
@input={{action (mut this.stripeMonthlyAmount) value="target.value"}}
|
|
||||||
@focus-out={{action "validateStripePlans"}}
|
|
||||||
/>
|
|
||||||
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/month</span>
|
|
||||||
</div>
|
|
||||||
<div class="gh-input-group">
|
|
||||||
<GhTextInput
|
|
||||||
@id="yearlyPrice"
|
|
||||||
@value={{readonly this.stripeYearlyAmount}}
|
|
||||||
@type="number"
|
|
||||||
@input={{action (mut this.stripeYearlyAmount) value="target.value"}}
|
|
||||||
@focus-out={{this.validateStripePlans}}
|
|
||||||
@placeholder=''
|
|
||||||
data-test-title-input={{true}}
|
|
||||||
/>
|
|
||||||
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/year</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{#if this.stripePlanError}}
|
|
||||||
<p class="response w-100"><span class="red">{{this.stripePlanError}}</span></p>
|
|
||||||
{{/if}}
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
|
{{/if}}
|
||||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="paid-welcome-page">
|
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="paid-welcome-page">
|
||||||
<label for="paidWelcomePage">Welcome page</label>
|
<label for="paidWelcomePage">Welcome page</label>
|
||||||
<GhUrlInput
|
<GhUrlInput
|
||||||
|
|
Loading…
Add table
Reference in a new issue