diff --git a/ghost/admin/app/components/gh-billing-iframe.hbs b/ghost/admin/app/components/gh-billing-iframe.hbs
index 63bae23650..9ba092062c 100644
--- a/ghost/admin/app/components/gh-billing-iframe.hbs
+++ b/ghost/admin/app/components/gh-billing-iframe.hbs
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/gh-billing-iframe.js b/ghost/admin/app/components/gh-billing-iframe.js
index c723772082..96b6b65a2a 100644
--- a/ghost/admin/app/components/gh-billing-iframe.js
+++ b/ghost/admin/app/components/gh-billing-iframe.js
@@ -7,19 +7,35 @@ export default Component.extend({
ghostPaths: service(),
ajax: service(),
- didRender() {
- let iframe = this.element.querySelector('#billing-frame');
+ didInsertElement() {
+ let fetchingSubscription = false;
+ this.billing.getBillingIframe().src = this.billing.getIframeURL();
+
window.addEventListener('message', (event) => {
if (event && event.data && event.data.request === 'token') {
const ghostIdentityUrl = this.get('ghostPaths.url').api('identities');
this.ajax.request(ghostIdentityUrl).then((response) => {
const token = response && response.identities && response.identities[0] && response.identities[0].token;
- iframe.contentWindow.postMessage({
+ this.billing.getBillingIframe().contentWindow.postMessage({
request: 'token',
response: token
}, '*');
});
+
+ // NOTE: the handler is placed here to avoid additional logic to check if iframe has loaded
+ // receiving a 'token' request is an indication that page is ready
+ if (!fetchingSubscription && !this.billing.get('subscription')) {
+ fetchingSubscription = true;
+ this.billing.getBillingIframe().contentWindow.postMessage({
+ query: 'getSubscription',
+ response: 'subscription'
+ }, '*');
+ }
+ }
+
+ if (event && event.data && event.data.subscription) {
+ this.billing.set('subscription', event.data.subscription);
}
});
}
diff --git a/ghost/admin/app/components/gh-billing-modal.hbs b/ghost/admin/app/components/gh-billing-modal.hbs
new file mode 100644
index 0000000000..3535c4214e
--- /dev/null
+++ b/ghost/admin/app/components/gh-billing-modal.hbs
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/gh-billing-modal.js b/ghost/admin/app/components/gh-billing-modal.js
new file mode 100644
index 0000000000..4260689630
--- /dev/null
+++ b/ghost/admin/app/components/gh-billing-modal.js
@@ -0,0 +1,53 @@
+/* global key */
+import Component from '@ember/component';
+import {computed} from '@ember/object';
+import {run} from '@ember/runloop';
+import {inject as service} from '@ember/service';
+
+export default Component.extend({
+ billing: service(),
+
+ visibilityClass: computed('billing.billingWindowOpen', function () {
+ return this.billing.get('billingWindowOpen') ? 'gh-billing' : 'gh-billing closed';
+ }),
+
+ didInsertElement() {
+ this._super(...arguments);
+ this._setupShortcuts();
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+ this._removeShortcuts();
+ },
+
+ actions: {
+ closeModal() {
+ this.billing.closeBillingWindow();
+ }
+ },
+
+ _setupShortcuts() {
+ run(function () {
+ document.activeElement.blur();
+ });
+
+ this._previousKeymasterScope = key.getScope();
+
+ key('enter', 'modal', () => {
+ this.send('confirm');
+ });
+
+ key('escape', 'modal', () => {
+ this.send('closeModal');
+ });
+
+ key.setScope('modal');
+ },
+
+ _removeShortcuts() {
+ key.unbind('enter', 'modal');
+ key.unbind('escape', 'modal');
+ key.setScope(this._previousKeymasterScope);
+ }
+});
diff --git a/ghost/admin/app/components/gh-billing-update-button.hbs b/ghost/admin/app/components/gh-billing-update-button.hbs
index db0a50d57b..12fbba4c5a 100644
--- a/ghost/admin/app/components/gh-billing-update-button.hbs
+++ b/ghost/admin/app/components/gh-billing-update-button.hbs
@@ -1,5 +1,3 @@
-
-
{{#if this.showUpgradeButton}}
{{/if}}
diff --git a/ghost/admin/app/components/gh-billing-update-button.js b/ghost/admin/app/components/gh-billing-update-button.js
index e48d4d5a1c..7000077222 100644
--- a/ghost/admin/app/components/gh-billing-update-button.js
+++ b/ghost/admin/app/components/gh-billing-update-button.js
@@ -3,6 +3,7 @@ import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default Component.extend({
+ router: service(),
config: service(),
ghostPaths: service(),
ajax: service(),
@@ -10,45 +11,11 @@ export default Component.extend({
subscription: null,
- showUpgradeButton: computed.equal('subscription.status', 'trialing'),
-
- didRender() {
- let iframe = this.element.querySelector('#billing-frame-global');
- let fetchingSubscription = false;
-
- window.addEventListener('message', (event) => {
- if (event && event.data && event.data.request === 'token') {
- const ghostIdentityUrl = this.get('ghostPaths.url').api('identities');
-
- this.ajax.request(ghostIdentityUrl).then((response) => {
- const token = response && response.identities && response.identities[0] && response.identities[0].token;
- iframe.contentWindow.postMessage({
- request: 'token',
- response: token
- }, '*');
- });
-
- // NOTE: the handler is placed here to avoid additional logic to check if iframe has loaded
- // receiving a 'token' request is an indication that page is ready
- if (!fetchingSubscription && !this.get('subscription')) {
- fetchingSubscription = true;
- iframe.contentWindow.postMessage({
- query: 'getSubscription',
- response: 'subscription'
- }, '*');
- }
- }
-
- if (event && event.data && event.data.subscription) {
- this.set('subscription', event.data.subscription);
- }
- });
- },
+ showUpgradeButton: computed.equal('billing.subscription.status', 'trialing'),
actions: {
openBilling() {
- this.billing.set('upgrade', true);
- this.billing.toggleProperty('billingWindowOpen');
+ this.billing.openBillingWindow(this.router.currentURL, '/billing/plans');
}
}
});
diff --git a/ghost/admin/app/components/gh-nav-menu.hbs b/ghost/admin/app/components/gh-nav-menu.hbs
index 66e05e67ae..e3fb623868 100644
--- a/ghost/admin/app/components/gh-nav-menu.hbs
+++ b/ghost/admin/app/components/gh-nav-menu.hbs
@@ -14,12 +14,6 @@
@modifier="action wide" />
{{/if}}
-{{#if this.showBillingModal}}
-
-{{/if}}
-
diff --git a/ghost/admin/app/components/gh-nav-menu.js b/ghost/admin/app/components/gh-nav-menu.js
index 7dc3a4b64a..bc7949effe 100644
--- a/ghost/admin/app/components/gh-nav-menu.js
+++ b/ghost/admin/app/components/gh-nav-menu.js
@@ -38,7 +38,6 @@ export default Component.extend(ShortcutsMixin, {
showMenuExtension: and('config.clientExtensions.menu', 'session.user.isOwner'),
showDropdownExtension: and('config.clientExtensions.dropdown', 'session.user.isOwner'),
showScriptExtension: and('config.clientExtensions.script', 'session.user.isOwner'),
- showBillingModal: computed.reads('billing.billingWindowOpen'),
showBilling: computed.reads('config.billingUrl'),
init() {
@@ -81,8 +80,7 @@ export default Component.extend(ShortcutsMixin, {
this.toggleProperty('showSearchModal');
},
toggleBillingModal() {
- this.billing.set('upgrade', false);
- this.billing.toggleProperty('billingWindowOpen');
+ this.billing.openBillingWindow(this.router.currentURL);
}
},
diff --git a/ghost/admin/app/components/modal-billing.js b/ghost/admin/app/components/modal-billing.js
deleted file mode 100644
index 62de5c250d..0000000000
--- a/ghost/admin/app/components/modal-billing.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import ModalComponent from 'ghost-admin/components/modal-base';
-import {inject as service} from '@ember/service';
-
-export default ModalComponent.extend({
- billing: service(),
-
- actions: {
- closeModal() {
- this.billing.closeBillingWindow();
- }
- }
-});
diff --git a/ghost/admin/app/controllers/application.js b/ghost/admin/app/controllers/application.js
index accd03a30e..109d7b8faf 100644
--- a/ghost/admin/app/controllers/application.js
+++ b/ghost/admin/app/controllers/application.js
@@ -5,12 +5,14 @@ import {inject as service} from '@ember/service';
export default Controller.extend({
customViews: service(),
+ config: service(),
dropdown: service(),
router: service(),
session: service(),
settings: service(),
ui: service(),
+ showBilling: computed.reads('config.billingUrl'),
showNavMenu: computed('router.currentRouteName', 'session.{isAuthenticated,user.isFulfilled}', 'ui.isFullScreen', function () {
let {router, session, ui} = this;
diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js
index 12bdbaa640..67eaa40cf6 100644
--- a/ghost/admin/app/router.js
+++ b/ghost/admin/app/router.js
@@ -22,7 +22,10 @@ Router.map(function () {
this.route('reset', {path: '/reset/:token'});
this.route('about');
this.route('site');
- this.route('billing');
+
+ this.route('billing', function () {
+ this.route('billing-sub', {path: '/*sub'});
+ });
this.route('posts');
this.route('pages');
diff --git a/ghost/admin/app/routes/billing.js b/ghost/admin/app/routes/billing.js
index 5ff4df978a..c63ec15c62 100644
--- a/ghost/admin/app/routes/billing.js
+++ b/ghost/admin/app/routes/billing.js
@@ -8,15 +8,41 @@ export default Route.extend({
action: {refreshModel: true}
},
+ beforeModel(transition) {
+ this.billing.set('previousTransition', transition);
+ },
+
model(params) {
if (params.action) {
this.billing.set('action', params.action);
}
- this.billing.set('billingWindowOpen', true);
+ this.billing.setBillingWindowOpen(true);
+ },
- // NOTE: if this route is ever triggered it was opened through external link because
- // the route has no underlying templates to render we redirect to root route
- this.transitionTo('/');
+ actions: {
+ willTransition(transition) {
+ let isBillingTransition = false;
+
+ if (transition) {
+ let destinationUrl = (typeof transition.to === 'string')
+ ? transition.to
+ : (transition.intent
+ ? transition.intent.url
+ : '');
+
+ if (destinationUrl.includes('/billing')) {
+ isBillingTransition = true;
+ }
+ }
+
+ this.billing.setBillingWindowOpen(isBillingTransition);
+ }
+ },
+
+ buildRouteInfoMetadata() {
+ return {
+ titleToken: 'Billing'
+ };
}
});
diff --git a/ghost/admin/app/services/billing.js b/ghost/admin/app/services/billing.js
index 94fb98ab48..a2d285a0a8 100644
--- a/ghost/admin/app/services/billing.js
+++ b/ghost/admin/app/services/billing.js
@@ -1,31 +1,91 @@
import Service from '@ember/service';
-import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default Service.extend({
+ router: service(),
config: service(),
ghostPaths: service(),
+ billingRouteRoot: '#/billing',
billingWindowOpen: false,
- upgrade: false,
- action: null,
+ subscription: null,
+ previousRoute: null,
- closeBillingWindow() {
- this.set('billingWindowOpen', false);
- this.set('action', null);
+ init() {
+ this._super(...arguments);
+
+ if (this.config.get('billingUrl')) {
+ window.addEventListener('message', (event) => {
+ if (event && event.data && event.data.route) {
+ this.handleRouteChangeInIframe(event.data.route);
+ }
+ });
+ }
},
- endpoint: computed('config.billingUrl', 'billingWindowOpen', 'action', function () {
+ handleRouteChangeInIframe(destinationRoute) {
+ if (this.get('billingWindowOpen')) {
+ let billingRoute = this.get('billingRouteRoot');
+
+ if (destinationRoute !== '/') {
+ billingRoute += destinationRoute;
+ }
+
+ if (window.location.hash !== billingRoute) {
+ window.history.replaceState(window.history.state, '', billingRoute);
+ }
+ }
+ },
+
+ getIframeURL() {
let url = this.config.get('billingUrl');
- if (this.get('upgrade')) {
- url = this.ghostPaths.url.join(url, 'plans');
- }
+ if (window.location.hash && window.location.hash.includes(this.get('billingRouteRoot'))) {
+ let destinationRoute = window.location.hash.replace(this.get('billingRouteRoot'), '');
- if (this.get('action')) {
- url += `?action=${this.get('action')}`;
+ if (destinationRoute) {
+ url += destinationRoute;
+ }
}
return url;
- })
+ },
+
+ // Controls billing window modal visibility and sync of the URL visible in browser
+ // and the URL opened on the iframe. It is responsible to non user triggered iframe opening,
+ // for example: by entering "/billing" route in the URL or using history navigation (back and forward)
+ setBillingWindowOpen(value) {
+ let billingIframe = this.getBillingIframe();
+
+ if (billingIframe && value) {
+ billingIframe.contentWindow.location.replace(this.getIframeURL());
+ }
+
+ this.set('billingWindowOpen', value);
+ },
+
+ // Controls navigation to billing window modal which is triggered from the application UI.
+ // For example: pressing "View Billing" link in navigation menu. It's main side effect is
+ // remembering the route from which the action has been triggered - "previousRoute" so it
+ // could be reused when closing billing window
+ openBillingWindow(currentRoute, childRoute) {
+ this.set('previousRoute', currentRoute);
+
+ // Ensures correct "getIframeURL" calculation when syncing iframe location
+ // in setBillingWindowOpen
+ window.location.hash = childRoute || '/billing';
+
+ this.router.transitionTo(childRoute || '/billing');
+ },
+
+ closeBillingWindow() {
+ this.set('billingWindowOpen', false);
+
+ let transitionRoute = this.get('previousRoute') || '/';
+ this.router.transitionTo(transitionRoute);
+ },
+
+ getBillingIframe() {
+ return document.getElementById('billing-frame');
+ }
});
diff --git a/ghost/admin/app/styles/layouts/billing.css b/ghost/admin/app/styles/layouts/billing.css
index e9588beb54..f6928ee86f 100644
--- a/ghost/admin/app/styles/layouts/billing.css
+++ b/ghost/admin/app/styles/layouts/billing.css
@@ -1,19 +1,36 @@
-.fullscreen-modal-billing {
- margin: 0;
- max-width: 100%;
+.gh-billing {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ z-index: 9999;
+ background: var(--main-bg-color);
}
-.fullscreen-modal-billing .modal-content {
+.gh-billing-container {
position: relative;
height: 100%;
- padding: 0;
+ width: 100%;
}
-.fullscreen-modal-billing .modal-body {
+.gh-billing.closed {
+ display: none;
+}
+
+.gh-billing .close {
+ position: absolute;
+ top: 19px;
+ right: 19px;
+ z-index: 9999;
margin: 0;
+ padding: 0;
+ width: 16px;
+ height: 16px;
+ border: none;
}
-.fullscreen-modal-billing .billing-frame {
+.gh-billing .billing-frame {
position: absolute;
top: 0;
right: 0;
@@ -35,10 +52,3 @@
transition: all 0.2s ease-in-out;
top: 25px;
}
-
-#billing-frame-global {
- visibility: hidden;
- height:0;
- width:0;
- border:none;
-}
diff --git a/ghost/admin/app/templates/application.hbs b/ghost/admin/app/templates/application.hbs
index 0d52cfcdb9..a88c98d7d1 100644
--- a/ghost/admin/app/templates/application.hbs
+++ b/ghost/admin/app/templates/application.hbs
@@ -28,6 +28,10 @@
@modifier="action narrow"
/>
{{/if}}
+
+ {{#if this.showBilling}}
+
+ {{/if}}
\ No newline at end of file