mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +03:00
Fixed browser URL syncronization with embeded iframe state
no issue - Opted in to use explicit `hisotry.replaceState` and setting iframe's `src` using assignment instead of tracking it through computed property. This allows for tighter control over when iframe's history is updated which was causing problems when `src` was bound to computed property - Added billing page metadata. This way browser history records appear with nicer signature - Removed "update button" iframe and rewrote "global iframe" to not use modals. This allows to have single iframe on a page, which simplifies `postMessage` communication and preserve history inside iframe to be able to navigate it after closure - Added route change handler responding to BMA app route changes. Allows to sync browser URL visible to the user with active route in BMA iframe. The sync is based on `hisory.replaceState` method that makes sure singular history records are kept in the browser history - Added nested wildcard billing route. This is meant to catch all the nested routes inside of BMA iframe
This commit is contained in:
parent
5a1b3d90fb
commit
5d59670ac3
@ -1 +1 @@
|
||||
<iframe id="billing-frame" class="billing-frame" src={{this.billing.endpoint}} frameborder="0" allowtransparency="true"></iframe>
|
||||
<iframe id="billing-frame" class="billing-frame" frameborder="0"></iframe>
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
11
ghost/admin/app/components/gh-billing-modal.hbs
Normal file
11
ghost/admin/app/components/gh-billing-modal.hbs
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="{{this.visibilityClass}}">
|
||||
<div class="gh-billing-container">
|
||||
<div class="gh-billing-close">
|
||||
<button class="close" href title="Close" {{on "click" (action "closeModal")}}>
|
||||
{{svg-jar "close"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<GhBillingIframe></GhBillingIframe>
|
||||
</div>
|
||||
</div>
|
53
ghost/admin/app/components/gh-billing-modal.js
Normal file
53
ghost/admin/app/components/gh-billing-modal.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
@ -1,5 +1,3 @@
|
||||
<iframe id="billing-frame-global" src={{this.billing.endpoint}}></iframe>
|
||||
|
||||
{{#if this.showUpgradeButton}}
|
||||
<button class="gh-btn gh-btn-green" {{action "openBilling"}}><span>Upgrade</span></button>
|
||||
{{/if}}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -14,12 +14,6 @@
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showBillingModal}}
|
||||
<GhFullscreenModal @modal="billing"
|
||||
@close={{action "toggleBillingModal"}}
|
||||
@modifier="full-overlay billing" />
|
||||
{{/if}}
|
||||
|
||||
<section class="gh-nav-body">
|
||||
<div class="gh-nav-top">
|
||||
<ul class="gh-nav-list gh-nav-main">
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
@ -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;
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -28,6 +28,10 @@
|
||||
@modifier="action narrow"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showBilling}}
|
||||
<GhBillingModal />
|
||||
{{/if}}
|
||||
</GhApp>
|
||||
|
||||
<EmberLoadRemover />
|
Loading…
Reference in New Issue
Block a user