Ghost/ghost/admin/app/controllers/settings/membership.js
Kevin Ansfield 9bdb25d184
Fixed hosting management screen not loading after sign-in process (#15763)
refs https://github.com/TryGhost/Team/issues/2110

- dynamically defined properties on the config service did not have
autotracking set up properly if they were accessed in any way before the
property was defined, this caused problems in a number of areas because
we have both "unauthed" and "authed" sets of config and when not logged
in we had parts of the app checking for authed config properties that
don't exist until after sign-in and subsequent config re-fetch
- renamed `config` service to `configManager` and updated to only
contain methods for fetching config data
- added a `config` instance initializer that sets up a `TrackedObject`
instance with some custom properties/methods and registers it on
`config:main`
- uses application instance initializer rather than a standard
initializer because standard initializers are only called once when
setting up the test suite so we'd end up with config leaking across
tests
- added an `@inject` decorator that when used takes the property name
and injects whatever is registered at `${propertyName}:main`, this
allows us to use dependency injection for any object rather than just
services or controllers
- using `application.inject()` in the initializer was initially used but
that only works for objects that extend from `EmberObject`, the
injections weren't available in native-class glimmer components so this
decorator keeps the injection syntax consistent
  - swapped all `@service config` uses to `@inject config`
2022-11-03 11:14:36 +00:00

412 lines
12 KiB
JavaScript

import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
import Controller from '@ember/controller';
import envConfig from 'ghost-admin/config/environment';
import {action} from '@ember/object';
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const CURRENCIES = currencies.map((currency) => {
return {
value: currency.isoCode.toLowerCase(),
label: `${currency.isoCode} - ${currency.name}`,
isoCode: currency.isoCode
};
});
export default class MembersAccessController extends Controller {
@service feature;
@service membersUtils;
@service modals;
@service settings;
@service store;
@service session;
@inject config;
@tracked showPortalSettings = false;
@tracked showStripeConnect = false;
@tracked showTierModal = false;
@tracked tier = null;
@tracked tiers = null;
@tracked tierModel = null;
@tracked paidSignupRedirect;
@tracked freeSignupRedirect;
@tracked welcomePageURL;
@tracked stripeMonthlyAmount = 5;
@tracked stripeYearlyAmount = 50;
@tracked currency = 'usd';
@tracked stripePlanError = '';
@tracked portalPreviewUrl = '';
portalPreviewGuid = Date.now().valueOf();
queryParams = ['showPortalSettings', 'verifyEmail'];
@tracked verifyEmail = null;
get freeTier() {
return this.tiers?.find(tier => tier.type === 'free');
}
get paidTiers() {
return this.tiers?.filter(tier => tier.type === 'paid');
}
get allCurrencies() {
return getCurrencyOptions();
}
get siteUrl() {
return this.config.blogUrl;
}
get selectedCurrency() {
return CURRENCIES.findBy('value', this.currency);
}
get isConnectDisallowed() {
const siteUrl = this.config.blogUrl;
return envConfig.environment !== 'development' && !/^https:/.test(siteUrl);
}
get hasChangedPrices() {
if (this.tier) {
const monthlyPrice = this.tier.get('monthlyPrice');
const yearlyPrice = this.tier.get('yearlyPrice');
if (monthlyPrice?.amount && parseFloat(this.stripeMonthlyAmount) !== (monthlyPrice.amount / 100)) {
return true;
}
if (yearlyPrice?.amount && parseFloat(this.stripeYearlyAmount) !== (yearlyPrice.amount / 100)) {
return true;
}
}
return false;
}
@action
setup() {
this.fetchTiers.perform();
this.updatePortalPreview();
}
get isDirty() {
return this.settings.hasDirtyAttributes || this.hasChangedPrices;
}
@action
async membersSubscriptionAccessChanged() {
const oldValue = this.settings.changedAttributes().membersSignupAccess?.[0];
if (oldValue === 'none') {
// when saved value is 'none' the server won't inject the portal script
// to work around that and show the expected portal preview we save and
// force a refresh
await this.switchFromNoneTask.perform();
} else {
this.updatePortalPreview();
}
}
@action
setStripePlansCurrency(event) {
const newCurrency = event.value;
this.currency = newCurrency;
}
@action
setPaidSignupRedirect(url) {
this.paidSignupRedirect = url;
}
@action
setFreeSignupRedirect(url) {
this.freeSignupRedirect = url;
}
@action
setWelcomePageURL(url) {
this.welcomePageURL = url;
}
@action
validatePaidSignupRedirect() {
return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect');
}
@action
validateFreeSignupRedirect() {
return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect');
}
@action
validateWelcomePageURL() {
const siteUrl = this.siteUrl;
if (this.welcomePageURL === undefined) {
// Not initialised
return;
}
if (this.welcomePageURL.href.startsWith(siteUrl)) {
const path = this.welcomePageURL.href.replace(siteUrl, '');
this.freeTier.welcomePageURL = path;
} else {
this.freeTier.welcomePageURL = this.welcomePageURL.href;
}
}
@action
validateStripePlans({updatePortalPreview = true} = {}) {
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`);
}
if (updatePortalPreview) {
this.updatePortalPreview();
}
} catch (err) {
this.stripePlanError = err.message;
}
}
@action
openStripeConnect() {
this.stripeEnabledOnOpen = this.membersUtils.isStripeEnabled;
this.showStripeConnect = true;
}
@action
async closeStripeConnect() {
if (this.stripeEnabledOnOpen !== this.membersUtils.isStripeEnabled) {
await this.saveSettingsTask.perform({forceRefresh: true});
}
this.showStripeConnect = false;
}
@action
async openEditTier(tier) {
this.tierModel = tier;
this.showTierModal = true;
}
@action
async openNewTier() {
this.tierModel = this.store.createRecord('tier');
this.showTierModal = true;
}
@action
closeTierModal() {
this.showTierModal = false;
}
@action
openPortalSettings() {
this.saveSettingsTask.perform();
this.showPortalSettings = true;
}
@action
async closePortalSettings() {
const changedAttributes = this.settings.changedAttributes();
if (changedAttributes && Object.keys(changedAttributes).length > 0) {
const shouldClose = await this.modals.open(ConfirmUnsavedChangesModal);
if (shouldClose) {
this.settings.rollbackAttributes();
this.showPortalSettings = false;
this.updatePortalPreview();
}
} else {
this.showPortalSettings = false;
this.updatePortalPreview();
}
}
@action
updatePortalPreview({forceRefresh} = {forceRefresh: false}) {
// TODO: can these be worked out from settings in membersUtils?
const monthlyPrice = Math.round(this.stripeMonthlyAmount * 100);
const yearlyPrice = Math.round(this.stripeYearlyAmount * 100);
let portalPlans = this.settings.portalPlans || [];
let isMonthlyChecked = portalPlans.includes('monthly');
let isYearlyChecked = portalPlans.includes('yearly');
const tiers = this.store.peekAll('tier');
const portalTiers = tiers?.filter((tier) => {
return tier.get('visibility') === 'public'
&& tier.get('active') === true
&& tier.get('type') === 'paid';
}).map((tier) => {
return tier.id;
});
const newUrl = new URL(this.membersUtils.getPortalPreviewUrl({
button: false,
monthlyPrice,
yearlyPrice,
portalTiers,
currency: this.currency,
isMonthlyChecked,
isYearlyChecked,
portalPlans: null
}));
if (forceRefresh) {
this.portalPreviewGuid = Date.now().valueOf();
}
newUrl.searchParams.set('v', this.portalPreviewGuid);
this.portalPreviewUrl = newUrl;
}
@action
portalPreviewInserted(iframe) {
this.portalPreviewIframe = iframe;
if (!this.portalMessageListener) {
this.portalMessageListener = (event) => {
// don't resize membership portal preview when events fire in customize portal modal
if (this.showPortalSettings) {
return;
}
const resizeEvents = ['portal-ready', 'portal-preview-updated'];
if (resizeEvents.includes(event.data.type) && event.data.payload?.height && this.portalPreviewIframe?.parentNode) {
this.portalPreviewIframe.parentNode.style.height = `${event.data.payload.height}px`;
}
};
window.addEventListener('message', this.portalMessageListener, true);
}
}
@action
portalPreviewDestroyed() {
this.portalPreviewIframe = null;
if (this.portalMessageListener) {
window.removeEventListener('message', this.portalMessageListener);
}
}
@action
confirmTierSave() {
this.updatePortalPreview({forceRefresh: true});
return this.fetchTiers.perform();
}
@task
*switchFromNoneTask() {
return yield this.saveSettingsTask.perform({forceRefresh: true});
}
setupPortalTier(tier) {
if (tier) {
const monthlyPrice = tier.get('monthlyPrice');
const yearlyPrice = tier.get('yearlyPrice');
this.currency = tier.get('currency');
if (monthlyPrice) {
this.stripeMonthlyAmount = (monthlyPrice / 100);
}
if (yearlyPrice) {
this.stripeYearlyAmount = (yearlyPrice / 100);
}
this.updatePortalPreview();
}
}
@task({drop: true})
*fetchTiers() {
this.tiers = yield this.store.query('tier', {
include: 'monthly_price,yearly_price,benefits'
});
this.tier = this.paidTiers.firstObject;
this.setupPortalTier(this.tier);
}
@task({drop: true})
*saveSettingsTask(options) {
if (this.settings.errors.length !== 0) {
return;
}
// When no filer is selected in `Specific tier(s)` option
if (!this.settings.defaultContentVisibility) {
return;
}
const result = yield this.settings.save();
yield this.freeTier.save();
this.updatePortalPreview(options);
return result;
}
async saveTier() {
const paidMembersEnabled = this.settings.paidMembersEnabled;
if (this.tier && paidMembersEnabled) {
const monthlyAmount = Math.round(this.stripeMonthlyAmount * 100);
const yearlyAmount = Math.round(this.stripeYearlyAmount * 100);
this.tier.set('monthlyPrice', monthlyAmount);
this.tier.set('yearlyPrice', yearlyAmount);
const savedTier = await this.tier.save();
return savedTier;
}
}
@action
reset() {
this.settings.rollbackAttributes();
this.resetPrices();
this.showLeavePortalModal = false;
this.showPortalSettings = false;
}
resetPrices() {
const monthlyPrice = this.tier.get('monthlyPrice');
const yearlyPrice = this.tier.get('yearlyPrice');
this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5;
this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50;
}
_validateSignupRedirect(url, type) {
const siteUrl = this.config.blogUrl;
let errMessage = `Please enter a valid URL`;
this.settings.errors.remove(type);
this.settings.hasValidated.removeObject(type);
if (url === null) {
this.settings.errors.add(type, errMessage);
this.settings.hasValidated.pushObject(type);
return false;
}
if (url === undefined) {
// Not initialised
return;
}
if (url.href.startsWith(siteUrl)) {
const path = url.href.replace(siteUrl, '');
this.settings[type] = path;
} else {
this.settings[type] = url.href;
}
}
}