mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-19 00:11:49 +03:00
9bdb25d184
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`
412 lines
12 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|