mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Wired membership tiers UI in new members setting
no refs - Wired Premium membership UI to existing monthly/yearly prices in the default product - Wired free membership UI to redirect URI setting - Updated save to validate settings/errors
This commit is contained in:
parent
686dadbeda
commit
35612db851
@ -1,18 +1,38 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {action} from '@ember/object';
|
||||
import {getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class MembersAccessController extends Controller {
|
||||
@service settings;
|
||||
@service store;
|
||||
@service config;
|
||||
|
||||
@tracked showLeavePortalModal = false;
|
||||
@tracked showLeaveRouteModal = false;
|
||||
@tracked showPortalSettings = false;
|
||||
|
||||
@tracked product = null;
|
||||
@tracked stripePrices = [];
|
||||
@tracked paidSignupRedirect;
|
||||
@tracked freeSignupRedirect;
|
||||
@tracked stripeMonthlyAmount = 5;
|
||||
@tracked stripeYearlyAmount = 50;
|
||||
@tracked currency = 'usd';
|
||||
@tracked stripePlanError = '';
|
||||
|
||||
queryParams = ['showPortalSettings'];
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.siteUrl = this.config.get('blogUrl');
|
||||
this.fetchDefaultProduct.perform();
|
||||
|
||||
this.allCurrencies = getCurrencyOptions();
|
||||
}
|
||||
|
||||
leaveRoute(transition) {
|
||||
if (this.settings.get('hasDirtyAttributes')) {
|
||||
transition.abort();
|
||||
@ -21,11 +41,77 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
_validateSignupRedirect(url, type) {
|
||||
let errMessage = `Please enter a valid URL`;
|
||||
this.settings.get('errors').remove(type);
|
||||
this.settings.get('hasValidated').removeObject(type);
|
||||
|
||||
if (url === null) {
|
||||
this.settings.get('errors').add(type, errMessage);
|
||||
this.settings.get('hasValidated').pushObject(type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
// Not initialised
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.href.startsWith(this.siteUrl)) {
|
||||
const path = url.href.replace(this.siteUrl, '');
|
||||
this.settings.set(type, path);
|
||||
} else {
|
||||
this.settings.set(type, url.href);
|
||||
}
|
||||
}
|
||||
|
||||
@action openPortalSettings() {
|
||||
this.saveSettingsTask.perform();
|
||||
this.showPortalSettings = true;
|
||||
}
|
||||
|
||||
@action
|
||||
setStripePlansCurrency(event) {
|
||||
const newCurrency = event.value;
|
||||
this.currency = newCurrency;
|
||||
}
|
||||
|
||||
@action
|
||||
setPaidSignupRedirect(url) {
|
||||
this.paidSignupRedirect = url;
|
||||
}
|
||||
|
||||
@action
|
||||
setFreeSignupRedirect(url) {
|
||||
this.freeSignupRedirect = url;
|
||||
}
|
||||
|
||||
@action
|
||||
validatePaidSignupRedirect() {
|
||||
return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect');
|
||||
}
|
||||
|
||||
@action
|
||||
validateFreeSignupRedirect() {
|
||||
return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect');
|
||||
}
|
||||
|
||||
@action
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
closePortalSettings() {
|
||||
const changedAttributes = this.settings.changedAttributes();
|
||||
@ -65,8 +151,66 @@ export default class MembersAccessController extends Controller {
|
||||
this.leaveSettingsTransition = null;
|
||||
}
|
||||
|
||||
saveProduct() {
|
||||
if (this.product) {
|
||||
const stripePrices = this.product.stripePrices || [];
|
||||
if (this.stripeMonthlyAmount && this.stripeYearlyAmount) {
|
||||
stripePrices.push(
|
||||
{
|
||||
nickname: 'Monthly',
|
||||
amount: this.stripeMonthlyAmount * 100,
|
||||
active: 1,
|
||||
currency: this.currency,
|
||||
interval: 'month',
|
||||
type: 'recurring'
|
||||
},
|
||||
{
|
||||
nickname: 'Yearly',
|
||||
amount: this.stripeYearlyAmount * 100,
|
||||
active: 1,
|
||||
currency: this.currency,
|
||||
interval: 'year',
|
||||
type: 'recurring'
|
||||
}
|
||||
);
|
||||
this.product.set('stripePrices', stripePrices);
|
||||
return this.product;
|
||||
} else {
|
||||
return this.product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*fetchDefaultProduct() {
|
||||
const products = yield this.store.query('product', {include: 'stripe_prices'});
|
||||
this.product = products.firstObject;
|
||||
this.stripePrices = [];
|
||||
if (this.product) {
|
||||
this.stripePrices = this.product.get('stripePrices');
|
||||
const monthlyPrice = this.stripePrices.find(d => d.nickname === 'Monthly');
|
||||
const yearlyPrice = this.stripePrices.find(d => d.nickname === 'Yearly');
|
||||
if (monthlyPrice && monthlyPrice.amount) {
|
||||
this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
|
||||
this.currency = monthlyPrice.currency;
|
||||
}
|
||||
if (yearlyPrice && yearlyPrice.amount) {
|
||||
this.stripeYearlyAmount = (yearlyPrice.amount / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*saveSettingsTask() {
|
||||
yield this.validateStripePlans();
|
||||
|
||||
if (this.stripePlanError) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.get('errors').length !== 0) {
|
||||
return;
|
||||
}
|
||||
return yield this.settings.save();
|
||||
}
|
||||
|
||||
|
@ -225,7 +225,7 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.gh-setting-action .for-checkbox label,
|
||||
.gh-setting-action .for-checkbox label,
|
||||
.gh-setting-action .for-radio label {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
@ -1465,6 +1465,26 @@ p.theme-validation-details {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.gh-settings-members-pricelabelcont .gh-select {
|
||||
padding: 0;
|
||||
width: 320px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.gh-settings-members-pricelabelcont .gh-select select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-settings-members-pricelabelcont .gh-select svg {
|
||||
position: absolute;
|
||||
bottom: unset;
|
||||
width: 12px;
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
.gh-setting-members-prices {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@ -1488,4 +1508,4 @@ p.theme-validation-details {
|
||||
height: 582px;
|
||||
margin-bottom: 60px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
@ -74,12 +74,18 @@
|
||||
<div class="gh-setting-content-extended">
|
||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="free-welcome-page">
|
||||
<label for="freeWelcomePage">Welcome page</label>
|
||||
<GhTextInput
|
||||
<GhUrlInput
|
||||
@id="freeWelcomePage"
|
||||
@placeholder='https://'
|
||||
data-test-title-input={{true}}
|
||||
@value={{readonly this.settings.membersFreeSignupRedirect}}
|
||||
@baseUrl={{readonly this.siteUrl}}
|
||||
@setResult={{action "setFreeSignupRedirect"}}
|
||||
@validateUrl={{action "validateFreeSignupRedirect"}}
|
||||
@placeholder={{readonly this.siteUrl}}
|
||||
/>
|
||||
<GhErrorMessage
|
||||
@errors={{settings.errors}}
|
||||
@property="membersFreeSignupRedirect"
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.settings.errors}} @property="free-welcome-page" />
|
||||
<p>Redirect to this URL after signup for a free membership</p>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
@ -99,43 +105,74 @@
|
||||
<div class="gh-expandable-content">
|
||||
{{#liquid-if this.paidOpen}}
|
||||
<div class="gh-setting-content-extended">
|
||||
<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>USD</div>
|
||||
</div>
|
||||
<div class="gh-setting-members-prices">
|
||||
<div class="gh-input-group">
|
||||
<GhTextInput
|
||||
@id="monthlyPrice"
|
||||
@placeholder=''
|
||||
data-test-title-input={{true}}
|
||||
/>
|
||||
<div class="gh-input-append">USD/month</div>
|
||||
{{#if this.fetchDefaultProduct.isRunning}}
|
||||
Loading...
|
||||
{{else}}
|
||||
<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-select">
|
||||
<OneWaySelect
|
||||
@value={{this.selectedCurrency}}
|
||||
id="currency"
|
||||
name="currency"
|
||||
@options={{readonly this.allCurrencies}}
|
||||
@optionValuePath="value"
|
||||
@optionLabelPath="label"
|
||||
@update={{this.setStripePlansCurrency}}
|
||||
/>
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-input-group">
|
||||
<GhTextInput
|
||||
@id="yearlyPrice"
|
||||
@placeholder=''
|
||||
data-test-title-input={{true}}
|
||||
/>
|
||||
<div class="gh-input-append">USD/year</div>
|
||||
</div>
|
||||
</div>
|
||||
<GhErrorMessage @errors={{this.settings.errors}} @property="prices" />
|
||||
</GhFormGroup>
|
||||
<div class="gh-setting-members-prices">
|
||||
|
||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="paid-welcome-page">
|
||||
<label for="paidWelcomePage">Welcome page</label>
|
||||
<GhTextInput
|
||||
@id="paidWelcomePage"
|
||||
@placeholder='https://'
|
||||
data-test-title-input={{true}}
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.settings.errors}} @property="paid-welcome-page" />
|
||||
<p>Redirect to this URL after signup for premium membership</p>
|
||||
</GhFormGroup>
|
||||
<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={{action "validateStripePlans"}}
|
||||
@placeholder=''
|
||||
data-test-title-input={{true}}
|
||||
/>
|
||||
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/month</span>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.stripePlanError}}
|
||||
<p class="response w-100 red"> {{this.stripePlanError}} </p>
|
||||
{{/if}}
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="paid-welcome-page">
|
||||
<label for="paidWelcomePage">Welcome page</label>
|
||||
<GhUrlInput
|
||||
@value={{readonly this.settings.membersPaidSignupRedirect}}
|
||||
@baseUrl={{readonly this.siteUrl}}
|
||||
@setResult={{action "setPaidSignupRedirect"}}
|
||||
@validateUrl={{action "validatePaidSignupRedirect"}}
|
||||
@placeholder={{readonly this.siteUrl}}
|
||||
/>
|
||||
<GhErrorMessage
|
||||
@errors={{settings.errors}}
|
||||
@property="membersPaidSignupRedirect"
|
||||
/>
|
||||
<p>Redirect to this URL after signup for premium membership</p>
|
||||
</GhFormGroup>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
|
@ -128,3 +128,34 @@ export function getSymbol(currency) {
|
||||
export function getNonDecimal(amount/*, currency*/) {
|
||||
return amount / 100;
|
||||
}
|
||||
|
||||
export function getCurrencyOptions() {
|
||||
const noOfTopCurrencies = 5;
|
||||
|
||||
const topCurrencies = currencies.slice(0, noOfTopCurrencies).map((currency) => {
|
||||
return {
|
||||
value: currency.isoCode.toLowerCase(),
|
||||
label: `${currency.isoCode} - ${currency.name}`,
|
||||
isoCode: currency.isoCode
|
||||
};
|
||||
});
|
||||
|
||||
const otherCurrencies = currencies.slice(noOfTopCurrencies, currencies.length).map((currency) => {
|
||||
return {
|
||||
value: currency.isoCode.toLowerCase(),
|
||||
label: `${currency.isoCode} - ${currency.name}`,
|
||||
isoCode: currency.isoCode
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
groupName: '—',
|
||||
options: topCurrencies
|
||||
},
|
||||
{
|
||||
groupName: '—',
|
||||
options: otherCurrencies
|
||||
}
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user