Added handling for forceUpgrade state (#2116)

no issue

- Lapsed trials and subscriptions will set the site's hosting config to `forceUpgrade` in which case a Ghost(Pro) site does not have a valid subscription or trial
- In this state we need to redirect all routes for all staff users to `/#/pro` to ensure the subscription can be put back into an active state
- This commit tackles
    - Route update on startup on the application route level
    - Catching and redirecting all transition (utils routes)
    - Fetching the owner user to pass this information to the Ghost(Pro) app for better communication to non-owner staff users
    - Allowing non-owner users in the force upgrade state to transition to the `/#/pro` route
This commit is contained in:
Aileen Nowak 2021-10-22 12:29:55 +02:00 committed by GitHub
parent 5b434d5ae6
commit b63a396423
5 changed files with 74 additions and 5 deletions

View File

@ -9,6 +9,8 @@ export default Component.extend({
ajax: service(), ajax: service(),
notifications: service(), notifications: service(),
isOwner: null,
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
@ -27,13 +29,21 @@ export default Component.extend({
request: 'token', request: 'token',
response: token response: token
}, '*'); }, '*');
this.set('isOwner', true);
}).catch((error) => { }).catch((error) => {
if (error.payload?.errors && error.payload.errors[0]?.type === 'NoPermissionError') { if (error.payload?.errors && error.payload.errors[0]?.type === 'NoPermissionError') {
// noop - user doesn't have permission to access billing // no permission means the current user requesting the token is not the owner of the site.
return; this.set('isOwner', false);
}
throw error; // Avoid letting the BMA waiting for a message and send an empty token response instead
this.billing.getBillingIframe().contentWindow.postMessage({
request: 'token',
response: null
}, '*');
} else {
throw error;
}
}); });
// NOTE: the handler is placed here to avoid additional logic to check if iframe has loaded // NOTE: the handler is placed here to avoid additional logic to check if iframe has loaded
@ -47,6 +57,27 @@ export default Component.extend({
} }
} }
if (event && event.data && event.data.request === 'forceUpgradeInfo') {
// Send BMA requested information about forceUpgrade and owner name/email
let ownerUser = null;
const owner = this.billing.ownerUser;
if (owner) {
ownerUser = {
name: owner?.name,
email: owner?.email
};
}
this.billing.getBillingIframe().contentWindow.postMessage({
request: 'forceUpgradeInfo',
response: {
forceUpgrade: this.config.get('hostSettings.forceUpgrade'),
isOwner: this.isOwner,
ownerUser
}
}, '*');
}
if (event && event.data && event.data.subscription) { if (event && event.data && event.data.subscription) {
this.billing.set('subscription', event.data.subscription); this.billing.set('subscription', event.data.subscription);
this.billing.set('checkoutRoute', event.data?.checkoutRoute || '/plans'); this.billing.set('checkoutRoute', event.data?.checkoutRoute || '/plans');

View File

@ -36,6 +36,7 @@ export default Route.extend(ShortcutsRoute, {
settings: service(), settings: service(),
ui: service(), ui: service(),
whatsNew: service(), whatsNew: service(),
billing: service(),
shortcuts, shortcuts,
@ -177,6 +178,11 @@ export default Route.extend(ShortcutsRoute, {
await this.session.postAuthPreparation(); await this.session.postAuthPreparation();
} }
if (this.config.get('hostSettings.forceUpgrade')) {
// enforce opening the BMA in a force upgrade state
this.billing.openBillingWindow(this.router.currentURL, '/pro');
}
} }
}); });

View File

@ -5,6 +5,7 @@ import {inject as service} from '@ember/service';
export default AuthenticatedRoute.extend({ export default AuthenticatedRoute.extend({
billing: service(), billing: service(),
session: service(), session: service(),
config: service(),
queryParams: { queryParams: {
action: {refreshModel: true} action: {refreshModel: true}
@ -13,7 +14,8 @@ export default AuthenticatedRoute.extend({
beforeModel(transition) { beforeModel(transition) {
this._super(...arguments); this._super(...arguments);
if (!this.session.user.isOwnerOnly) { // allow non-owner users to access the BMA when we're in a force upgrade state
if (!this.session.user.isOwnerOnly && !this.config.get('hostSettings.forceUpgrade')) {
return this.transitionTo('home'); return this.transitionTo('home');
} }

View File

@ -5,12 +5,14 @@ export default Service.extend({
router: service(), router: service(),
config: service(), config: service(),
ghostPaths: service(), ghostPaths: service(),
store: service(),
billingRouteRoot: '#/pro', billingRouteRoot: '#/pro',
billingWindowOpen: false, billingWindowOpen: false,
subscription: null, subscription: null,
previousRoute: null, previousRoute: null,
action: null, action: null,
ownerUser: null,
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -52,6 +54,21 @@ export default Service.extend({
return url; return url;
}, },
async getOwnerUser() {
if (!this.get('ownerUser')) {
// Try to receive the owner user from the store
let user = this.store.peekAll('user').findBy('isOwnerOnly', true);
if (!user) {
// load it when it's not there yet
await this.store.findAll('user');
user = this.store.peekAll('user').findBy('isOwnerOnly', true);
}
this.set('ownerUser', user);
}
return this.get('ownerUser');
},
// Sends a route update to a child route in the BMA, because we can't control // Sends a route update to a child route in the BMA, because we can't control
// navigating to it otherwise // navigating to it otherwise
sendRouteUpdate() { sendRouteUpdate() {
@ -88,6 +105,9 @@ export default Service.extend({
// remembering the route from which the action has been triggered - "previousRoute" so it // remembering the route from which the action has been triggered - "previousRoute" so it
// could be reused when closing billing window // could be reused when closing billing window
openBillingWindow(currentRoute, childRoute) { openBillingWindow(currentRoute, childRoute) {
// initiate getting owner user in the background
this.getOwnerUser();
if (this.get('billingWindowOpen')) { if (this.get('billingWindowOpen')) {
// don't attempt to open again // don't attempt to open again
return; return;

View File

@ -1,12 +1,22 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import {inject as service} from '@ember/service';
Route.reopen({ Route.reopen({
config: service(),
billing: service(),
router: service(),
actions: { actions: {
willTransition(transition) { willTransition(transition) {
if (this.get('upgradeStatus.isRequired')) { if (this.get('upgradeStatus.isRequired')) {
transition.abort(); transition.abort();
this.upgradeStatus.requireUpgrade(); this.upgradeStatus.requireUpgrade();
return false; return false;
} else if (this.config.get('hostSettings.forceUpgrade')) {
transition.abort();
// Catch and redirect every route in a force upgrade state
this.billing.openBillingWindow(this.router.currentURL, '/pro');
return false;
} else { } else {
return true; return true;
} }