Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/portal",
"version": "2.67.11",
"version": "2.67.12",
"license": "MIT",
"repository": "https://github.com/TryGhost/Ghost",
"author": "Ghost Foundation",
Expand Down
29 changes: 24 additions & 5 deletions apps/portal/src/actions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import setupGhostApi from './utils/api';
import {chooseBestErrorMessage} from './utils/errors';
import {createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl, getRefDomain} from './utils/helpers';
import {createNotification, createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl, getRefDomain} from './utils/helpers';
import {t} from './utils/i18n';

function switchPage({data, state}) {
Expand Down Expand Up @@ -49,16 +49,35 @@ function closePopup({state}) {
};
}

function openNotification({data}) {
function openNotification({data, state}) {
const {
action = 'openNotification',
status = 'success',
autoHide = true,
closeable = true,
duration = 2600,
message = ''
} = data || {};

const notification = createNotification({
type: action,
status,
autoHide,
closeable,
duration,
state,
message
});

return {
showNotification: true,
...data
notification,
notificationSequence: notification.count
};
}

function closeNotification() {
return {
showNotification: false
notification: null
};
}

Expand Down
162 changes: 153 additions & 9 deletions apps/portal/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,24 @@
import {transformPortalAnchorToRelative} from './utils/transform-portal-anchor-to-relative';
import {getActivePage, isAccountPage, isOfferPage} from './pages';
import ActionHandler from './actions';
import {getGiftRedemptionErrorMessage} from './utils/gift-redemption-notification';
import './app.css';
import {hasRecommendations, hasGiftSubscriptions, createPopupNotification, hasAvailablePrices, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isRetentionOffer, isComplimentaryMember, isInviteOnly, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
import {hasRecommendations, hasGiftSubscriptions, createNotification, createPopupNotification, hasAvailablePrices, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isRetentionOffer, isComplimentaryMember, isInviteOnly, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
import {validateHexColor} from './utils/sanitize-html';
import {handleDataAttributes} from './data-attributes';

const safeDecodeURIComponent = (value) => {
try {
return decodeURIComponent(value);
} catch (error) {
return null;
}

Check warning on line 27 in apps/portal/src/app.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1owX7XTjUozvLfgFFF&open=AZ1owX7XTjUozvLfgFFF&pullRequest=27188
};

const staleGiftRedemptionRequestResult = {
staleGiftRedemptionRequest: true
};

const DEV_MODE_DATA = {
showPopup: true,
site: Fixtures.site,
Expand Down Expand Up @@ -59,10 +72,15 @@
actionErrorMessage: null,
initStatus: 'running',
lastPage: null,
notification: null,
notificationSequence: -1,
customSiteUrl: props.customSiteUrl,
locale: props.locale,
scrollbarWidth: 0
};

this._redemptionRequestId = 0;
this.currentRedemptionToken = null;
}

componentDidMount() {
Expand Down Expand Up @@ -161,17 +179,39 @@
/** Setup custom trigger buttons handling on page */
setupCustomTriggerButton() {
// Handler for custom buttons
this.clickHandler = (event) => {
this.clickHandler = async (event) => {

Check failure on line 182 in apps/portal/src/app.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1owX7XTjUozvLfgFFG&open=AZ1owX7XTjUozvLfgFFG&pullRequest=27188
event.preventDefault();
const target = event.currentTarget;
const pagePath = (target && target.dataset.portal);
const {page, pageQuery, pageData} = this.getPageFromLinkPath(pagePath) || {};
const linkData = this.getPageFromLinkPath(pagePath);
if (!linkData) {
return;
}

const {page, pageQuery, pageData} = linkData;
if (this.state.initStatus === 'success') {
if (page === 'gift' && !hasGiftSubscriptions({site: this.state.site})) {
this.invalidateGiftRedemptionRequest();
removePortalLinkFromUrl();

return;
}
if (page === 'giftRedemption' && pageData?.token) {
const redemptionRequest = this.startGiftRedemptionRequest(pageData.token);
const giftLinkData = await this.fetchGiftRedemptionData({
site: this.state.site,
token: pageData.token
});

if (!this.isCurrentGiftRedemptionRequest(redemptionRequest)) {
return;
}

this.setState(giftLinkData);
return;
}

this.invalidateGiftRedemptionRequest();
if (pageQuery && pageQuery !== 'free') {
this.handleSignupQuery({site: this.state.site, pageQuery});
} else {
Expand Down Expand Up @@ -202,11 +242,30 @@
});
}

startGiftRedemptionRequest(token) {
this._redemptionRequestId += 1;
this.currentRedemptionToken = token;

return {
requestId: this._redemptionRequestId,
token
};
}

invalidateGiftRedemptionRequest() {
this._redemptionRequestId += 1;
this.currentRedemptionToken = null;
}

isCurrentGiftRedemptionRequest({requestId, token}) {
return this._redemptionRequestId === requestId && this.currentRedemptionToken === token;
}

/** Initialize portal setup on load, fetch data and setup state*/
async initSetup() {
try {
// Fetch data from API, links, preview, dev sources
const {site, member, offers, page, showPopup, popupNotification, lastPage, pageQuery, pageData} = await this.fetchData();
const {site, member, offers, page, showPopup, popupNotification, notification, notificationSequence, lastPage, pageQuery, pageData} = await this.fetchData();
const i18nLanguage = this.props.siteI18nEnabled ? this.props.locale || site.locale || 'en' : 'en';
i18n.changeLanguage(i18nLanguage);

Expand All @@ -220,6 +279,8 @@
showPopup,
pageData,
popupNotification,
notification,
notificationSequence,
dir: i18n.dir() || 'ltr',
action: 'init:success',
initStatus: 'success',
Expand Down Expand Up @@ -266,7 +327,8 @@
async fetchData() {
const {site: apiSiteData, member, offers} = await this.fetchApiData();
const {site: devSiteData, ...restDevData} = this.fetchDevData();
const {site: linkSiteData, ...restLinkData} = this.fetchLinkData(apiSiteData, member);
const linkData = await this.fetchLinkData(apiSiteData, member);
const {site: linkSiteData, ...restLinkData} = linkData?.staleGiftRedemptionRequest ? {} : linkData;
const {site: previewSiteData, ...restPreviewData} = this.fetchPreviewData();
const {site: notificationSiteData, ...restNotificationData} = this.fetchNotificationData();
let page = '';
Expand Down Expand Up @@ -492,7 +554,49 @@
}

/** Fetch state from Portal Links */
fetchLinkData(site, member) {
async fetchGiftRedemptionData({site, token}) {
if (!hasGiftSubscriptions({site})) {
removePortalLinkFromUrl();

return {};
}

try {
const response = await this.GhostApi.gift.fetchRedemptionData({token});

return {
showPopup: true,
notification: null,
page: 'giftRedemption',
pageData: {
token,
gift: response?.gift || null
}
};
} catch (error) {
removePortalLinkFromUrl();

const notification = createNotification({
type: 'giftRedemption:failed',
status: 'error',
autoHide: false,
closeable: true,
state: this.state,
message: getGiftRedemptionErrorMessage(error)
});

return {
showPopup: false,
pageData: null,
notification,
notificationSequence: notification.count
};
}
}

async fetchLinkData(site, member) {

Check failure on line 597 in apps/portal/src/app.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 53 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1kLTAfkZOia24LK229&open=AZ1kLTAfkZOia24LK229&pullRequest=27188
this.invalidateGiftRedemptionRequest();

const qParams = new URLSearchParams(window.location.search);

if (qParams.get('stripe') === 'gift-purchase-success') {
Expand Down Expand Up @@ -551,6 +655,7 @@
const productMonthlyPriceQueryRegex = /^(?:(\w+?))?\/monthly$/;
const productYearlyPriceQueryRegex = /^(?:(\w+?))?\/yearly$/;
const offersRegex = /^offers\/(\w+?)\/?$/;
const giftRedemptionRegex = /^\/portal\/gift\/redeem\/([^/?#]+)\/?$/;
const linkRegex = /^\/portal\/?(?:\/(\w+(?:\/\w+)*))?\/?$/;
const feedbackRegex = /^\/feedback\/(\w+?)\/(\w+?)\/?$/;

Expand Down Expand Up @@ -581,6 +686,25 @@
}
}
}
if (path && giftRedemptionRegex.test(path)) {
const [, token] = path.match(giftRedemptionRegex);

Check warning on line 690 in apps/portal/src/app.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "RegExp.exec()" method instead.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1kLTAfkZOia24LK22-&open=AZ1kLTAfkZOia24LK22-&pullRequest=27188
const decodedToken = safeDecodeURIComponent(token);
if (!decodedToken) {
return {};
}

const redemptionRequest = this.startGiftRedemptionRequest(decodedToken);
const giftLinkData = await this.fetchGiftRedemptionData({
site,
token: decodedToken
});

if (!this.isCurrentGiftRedemptionRequest(redemptionRequest)) {
return staleGiftRedemptionRequestResult;
}

return giftLinkData;
}
if (path && linkRegex.test(path)) {
const [,pagePath] = path.match(linkRegex);
const {page, pageQuery, pageData} = this.getPageFromLinkPath(pagePath, site) || {};
Expand Down Expand Up @@ -787,9 +911,14 @@
}

/**Handle state update for preview url and Portal Link changes */
updateStateForPreviewLinks() {
async updateStateForPreviewLinks() {
const {site: previewSite, ...restPreviewData} = this.fetchPreviewData();
const {site: linkSite, ...restLinkData} = this.fetchLinkData(this.state.site, this.state.member);
const linkData = await this.fetchLinkData(this.state.site, this.state.member);
if (linkData?.staleGiftRedemptionRequest) {
return;
}

const {site: linkSite, ...restLinkData} = linkData;

const updatedState = {
site: {
Expand Down Expand Up @@ -891,11 +1020,25 @@
const customMonthlyProductSignup = /^signup\/?(?:\/(\w+?))\/monthly\/?$/;
const customYearlyProductSignup = /^signup\/?(?:\/(\w+?))\/yearly\/?$/;
const customOfferRegex = /^offers\/(\w+?)\/?$/;
const giftRedemptionRegex = /^gift\/redeem\/([^/?#]+)\/?$/;

if (path === undefined || path === '') {
return {
page: 'default'
};
} else if (giftRedemptionRegex.test(path)) {
const [, token] = path.match(giftRedemptionRegex);
const decodedToken = safeDecodeURIComponent(token);
if (!decodedToken) {
return null;
}

return {
page: 'giftRedemption',
pageData: {
token: decodedToken
}
};
} else if (customOfferRegex.test(path)) {
return {
pageQuery: path
Expand Down Expand Up @@ -1082,7 +1225,7 @@

/**Get final App level context from App state*/
getContextFromState() {
const {site, member, offers, action, actionErrorMessage, page, lastPage, showPopup, pageQuery, pageData, popupNotification, customSiteUrl, dir, scrollbarWidth, otcRef, inboxLinks} = this.state;
const {site, member, offers, action, actionErrorMessage, page, lastPage, showPopup, pageQuery, pageData, popupNotification, notification, customSiteUrl, dir, scrollbarWidth, otcRef, inboxLinks} = this.state;
const contextPage = this.getContextPage({site, page, member});
const contextMember = this.getContextMember({site, page: contextPage, member, offers, pageData, customSiteUrl});
return {
Expand All @@ -1099,6 +1242,7 @@
lastPage,
showPopup,
popupNotification,
notification,
customSiteUrl,
dir,
scrollbarWidth,
Expand Down
2 changes: 2 additions & 0 deletions apps/portal/src/components/frame.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import EmailSuppressedPage from './pages/email-suppressed-page.css?inline';
import EmailSuppressionFAQ from './pages/email-suppression-faq.css?inline';
import EmailReceivingFAQ from './pages/email-receiving-faq.css?inline';
import {TipsAndDonationsSuccessStyle} from './pages/support-success';
import {GiftRedemptionStyles} from './pages/gift-redemption-page';
import {GiftSuccessStyle} from './pages/gift-success-page';
import {TipsAndDonationsErrorStyle} from './pages/support-error';
import {RecommendationsPageStyles} from './pages/recommendations-page';
Expand Down Expand Up @@ -1313,6 +1314,7 @@ export function getFrameStyles({site}) {
EmailSuppressionFAQ +
EmailReceivingFAQ +
TipsAndDonationsSuccessStyle +
GiftRedemptionStyles +
TipsAndDonationsErrorStyle +
GiftSuccessStyle +
RecommendationsPageStyles +
Expand Down
Loading
Loading