0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00
ghost/apps/portal/src/actions.js
Cathy Sarisky 1196688b0e
🌐 Improved i18n support for Portal error messages (#21190)
no ref

Expose (some) Portal error strings for translations

💩This is a somewhat hacky (but test-passing and individual inspection
passing) solution to the way Portal handles errors. Or rather, the
half-dozen ways Portal handles errors.

Passing 't' around with context and state, and occasionally recreating
it from the site object. Yes, I am also somewhat horrified, but a better
implementation will need a major rewrite of Portal.

Addresses errors in both the popover React app and in the
data-attributes.

There are probably more. Since Portal exposes raw API responses in some
places, it's hard to enumerate everything that /might/ end up being
client-facing, but at least I've gotten the ones that I've commonly
seen.

Improvements very welcome.
2024-10-03 13:35:23 +00:00

582 lines
18 KiB
JavaScript

import setupGhostApi from './utils/api';
import {chooseBestErrorMessage} from './utils/errors';
import {createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl, getRefDomain} from './utils/helpers';
function switchPage({data, state}) {
return {
page: data.page,
popupNotification: null,
lastPage: data.lastPage || null,
pageData: data.pageData || state.pageData
};
}
function togglePopup({state}) {
return {
showPopup: !state.showPopup
};
}
function openPopup({data}) {
return {
showPopup: true,
page: data.page,
...(data.pageQuery ? {pageQuery: data.pageQuery} : {}),
...(data.pageData ? {pageData: data.pageData} : {})
};
}
function back({state}) {
if (state.lastPage) {
return {
page: state.lastPage
};
} else {
return closePopup({state});
}
}
function closePopup({state}) {
removePortalLinkFromUrl();
return {
showPopup: false,
lastPage: null,
pageQuery: '',
popupNotification: null,
page: state.page === 'magiclink' ? '' : state.page
};
}
function openNotification({data}) {
return {
showNotification: true,
...data
};
}
function closeNotification() {
return {
showNotification: false
};
}
async function signout({api, state}) {
try {
await api.member.signout();
return {
action: 'signout:success'
};
} catch (e) {
const {t} = state;
return {
action: 'signout:failed',
popupNotification: createPopupNotification({
type: 'signout:failed', autoHide: false, closeable: true, state, status: 'error',
message: t('Failed to log out, please try again')
})
};
}
}
async function signin({data, api, state}) {
const {t} = state;
try {
const integrityToken = await api.member.getIntegrityToken();
await api.member.sendMagicLink({...data, emailType: 'signin', integrityToken});
return {
page: 'magiclink',
lastPage: 'signin'
};
} catch (e) {
return {
action: 'signin:failed',
popupNotification: createPopupNotification({
type: 'signin:failed', autoHide: false, closeable: true, state, status: 'error',
message: chooseBestErrorMessage(e, t('Failed to log in, please try again'), t)
})
};
}
}
async function signup({data, state, api}) {
try {
let {plan, tierId, cadence, email, name, newsletters, offerId} = data;
if (plan.toLowerCase() === 'free') {
const integrityToken = await api.member.getIntegrityToken();
await api.member.sendMagicLink({emailType: 'signup', integrityToken, ...data});
} else {
if (tierId && cadence) {
await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId});
} else {
({tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan}));
await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId});
}
return {
page: 'loading'
};
}
return {
page: 'magiclink',
lastPage: 'signup'
};
} catch (e) {
const {t} = state;
const message = chooseBestErrorMessage(e, t('Failed to sign up, please try again'), t);
return {
action: 'signup:failed',
popupNotification: createPopupNotification({
type: 'signup:failed', autoHide: false, closeable: true, state, status: 'error',
message: message
})
};
}
}
async function checkoutPlan({data, state, api}) {
try {
let {plan, offerId, tierId, cadence} = data;
if (!tierId || !cadence) {
({tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan}));
}
await api.member.checkoutPlan({
plan,
tierId,
cadence,
offerId,
metadata: {
checkoutType: 'upgrade'
}
});
} catch (e) {
const {t} = state;
return {
action: 'checkoutPlan:failed',
popupNotification: createPopupNotification({
type: 'checkoutPlan:failed', autoHide: false, closeable: true, state, status: 'error',
message: t('Failed to process checkout, please try again')
})
};
}
}
async function updateSubscription({data, state, api}) {
const {t} = state;
try {
const {plan, planId, subscriptionId, cancelAtPeriodEnd} = data;
const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: planId});
await api.member.updateSubscription({
planName: plan,
tierId,
cadence,
subscriptionId,
cancelAtPeriodEnd,
planId: planId
});
const member = await api.member.sessionData();
const action = 'updateSubscription:success';
return {
action,
popupNotification: createPopupNotification({
type: action, autoHide: true, closeable: true, state, status: 'success',
message: t('Subscription plan updated successfully')
}),
page: 'accountHome',
member: member
};
} catch (e) {
return {
action: 'updateSubscription:failed',
popupNotification: createPopupNotification({
type: 'updateSubscription:failed', autoHide: false, closeable: true, state, status: 'error',
message: t('Failed to update subscription, please try again')
})
};
}
}
async function cancelSubscription({data, state, api}) {
try {
const {subscriptionId, cancellationReason} = data;
await api.member.updateSubscription({
subscriptionId, smartCancel: true, cancellationReason
});
const member = await api.member.sessionData();
const action = 'cancelSubscription:success';
return {
action,
page: 'accountHome',
member: member
};
} catch (e) {
const {t} = state;
return {
action: 'cancelSubscription:failed',
popupNotification: createPopupNotification({
type: 'cancelSubscription:failed', autoHide: false, closeable: true, state, status: 'error',
message: t('Failed to cancel subscription, please try again')
})
};
}
}
async function continueSubscription({data, state, api}) {
try {
const {subscriptionId} = data;
await api.member.updateSubscription({
subscriptionId, cancelAtPeriodEnd: false
});
const member = await api.member.sessionData();
const action = 'continueSubscription:success';
return {
action,
page: 'accountHome',
member: member
};
} catch (e) {
const {t} = state;
return {
action: 'continueSubscription:failed',
popupNotification: createPopupNotification({
type: 'continueSubscription:failed', autoHide: false, closeable: true, state, status: 'error',
message: t('Failed to cancel subscription, please try again')
})
};
}
}
async function editBilling({data, state, api}) {
try {
await api.member.editBilling(data);
} catch (e) {
const {t} = state;
return {
action: 'editBilling:failed',
popupNotification: createPopupNotification({
type: 'editBilling:failed', autoHide: false, closeable: true, state, status: 'error',
message: t('Failed to update billing information, please try again')
})
};
}
}
async function clearPopupNotification() {
return {
popupNotification: null
};
}
async function showPopupNotification({data, state}) {
let {action, message = ''} = data;
action = action || 'showPopupNotification:success';
return {
popupNotification: createPopupNotification({
type: action,
autoHide: true,
closeable: true,
state,
status: 'success',
message
})
};
}
async function updateNewsletterPreference({data, state, api}) {
try {
const {newsletters, enableCommentNotifications} = data;
if (!newsletters && enableCommentNotifications === undefined) {
return {};
}
const updateData = {};
if (newsletters) {
updateData.newsletters = newsletters;
}
if (enableCommentNotifications !== undefined) {
updateData.enableCommentNotifications = enableCommentNotifications;
}
const member = await api.member.update(updateData);
const action = 'updateNewsletterPref:success';
return {
action,
member
};
} catch (e) {
const {t} = state;
return {
action: 'updateNewsletterPref:failed',
popupNotification: createPopupNotification({
type: 'updateNewsletter:failed',
autoHide: true, closeable: true, state, status: 'error',
message: t('Failed to update newsletter settings')
})
};
}
}
async function removeEmailFromSuppressionList({state, api}) {
const {t} = state;
try {
await api.member.deleteSuppression();
const action = 'removeEmailFromSuppressionList:success';
return {
action,
popupNotification: createPopupNotification({
type: 'removeEmailFromSuppressionList:success', autoHide: true, closeable: true, state, status: 'success',
message: t('You have been successfully resubscribed')
})
};
} catch (e) {
return {
action: 'removeEmailFromSuppressionList:failed',
popupNotification: createPopupNotification({
type: 'removeEmailFromSuppressionList:failed',
autoHide: true, closeable: true, state, status: 'error',
message: t('Your email has failed to resubscribe, please try again')
})
};
}
}
async function updateNewsletter({data, state, api}) {
const {t} = state;
try {
const {subscribed} = data;
const member = await api.member.update({subscribed});
if (!member) {
throw new Error('Failed to update newsletter');
}
const action = 'updateNewsletter:success';
return {
action,
member: member,
popupNotification: createPopupNotification({
type: action, autoHide: true, closeable: true, state, status: 'success',
message: t('Email newsletter settings updated')
})
};
} catch (e) {
return {
action: 'updateNewsletter:failed',
popupNotification: createPopupNotification({
type: 'updateNewsletter:failed', autoHide: true, closeable: true, state, status: 'error',
message: t('Failed to update newsletter settings')
})
};
}
}
async function updateMemberEmail({data, state, api}) {
const {email} = data;
const originalEmail = getMemberEmail({member: state.member});
if (email !== originalEmail) {
try {
await api.member.updateEmailAddress({email});
return {
success: true
};
} catch (err) {
return {
success: false,
error: err
};
}
}
return null;
}
async function updateMemberData({data, state, api}) {
const {name} = data;
const originalName = getMemberName({member: state.member});
if (originalName !== name) {
try {
const member = await api.member.update({name});
if (!member) {
throw new Error('Failed to update member');
}
return {
member,
success: true
};
} catch (err) {
return {
success: false,
error: err
};
}
}
return null;
}
async function refreshMemberData({state, api}) {
if (state.member) {
try {
const member = await api.member.sessionData();
if (member) {
return {
member,
success: true,
action: 'refreshMemberData:success'
};
}
return null;
} catch (err) {
return {
success: false,
error: err,
action: 'refreshMemberData:failed'
};
}
}
return null;
}
async function updateProfile({data, state, api}) {
const {t} = state;
const [dataUpdate, emailUpdate] = await Promise.all([updateMemberData({data, state, api}), updateMemberEmail({data, state, api})]);
if (dataUpdate && emailUpdate) {
if (emailUpdate.success) {
return {
action: 'updateProfile:success',
...(dataUpdate.success ? {member: dataUpdate.member} : {}),
page: 'accountHome',
popupNotification: createPopupNotification({
type: 'updateProfile:success', autoHide: true, closeable: true, status: 'success', state,
message: t('Check your inbox to verify email update')
})
};
}
const message = !dataUpdate.success ? t('Failed to update account data') : t('Failed to send verification email');
return {
action: 'updateProfile:failed',
...(dataUpdate.success ? {member: dataUpdate.member} : {}),
popupNotification: createPopupNotification({
type: 'updateProfile:failed', autoHide: true, closeable: true, status: 'error', message, state
})
};
} else if (dataUpdate) {
const action = dataUpdate.success ? 'updateProfile:success' : 'updateProfile:failed';
const status = dataUpdate.success ? 'success' : 'error';
const message = !dataUpdate.success ? t('Failed to update account details') : t('Account details updated successfully');
return {
action,
...(dataUpdate.success ? {member: dataUpdate.member} : {}),
...(dataUpdate.success ? {page: 'accountHome'} : {}),
popupNotification: createPopupNotification({
type: action, autoHide: dataUpdate.success, closeable: true, status, state, message
})
};
} else if (emailUpdate) {
const action = emailUpdate.success ? 'updateProfile:success' : 'updateProfile:failed';
const status = emailUpdate.success ? 'success' : 'error';
const message = !emailUpdate.success ? t('Failed to send verification email') : t('Check your inbox to verify email update');
return {
action,
...(emailUpdate.success ? {page: 'accountHome'} : {}),
popupNotification: createPopupNotification({
type: action, autoHide: emailUpdate.success, closeable: true, status, state, message
})
};
}
return {
action: 'updateProfile:success',
page: 'accountHome',
popupNotification: createPopupNotification({
type: 'updateProfile:success', autoHide: true, closeable: true, status: 'success', state,
message: t('Account details updated successfully')
})
};
}
async function oneClickSubscribe({data: {siteUrl}, state}) {
const externalSiteApi = setupGhostApi({siteUrl: siteUrl, apiUrl: 'not-defined', contentApiKey: 'not-defined'});
const {member} = state;
const referrerUrl = window.location.href;
const referrerSource = getRefDomain();
const integrityToken = await externalSiteApi.member.getIntegrityToken();
await externalSiteApi.member.sendMagicLink({
emailType: 'signup',
name: member.name,
email: member.email,
autoRedirect: false,
integrityToken,
customUrlHistory: state.site.outbound_link_tagging ? [
{
time: Date.now(),
referrerSource,
referrerMedium: 'Ghost Recommendations',
referrerUrl
}
] : []
});
return {};
}
function trackRecommendationClicked({data: {recommendationId}, api}) {
try {
const existing = localStorage.getItem('ghost-recommendations-clicked');
const clicked = existing ? JSON.parse(existing) : [];
if (clicked.includes(recommendationId)) {
// Already tracked
return;
}
clicked.push(recommendationId);
localStorage.setItem('ghost-recommendations-clicked', JSON.stringify(clicked));
} catch (e) {
// Ignore localstorage errors (browser not supported or in private mode)
}
api.recommendations.trackClicked({
recommendationId
});
return {};
}
async function trackRecommendationSubscribed({data: {recommendationId}, api}) {
api.recommendations.trackSubscribed({
recommendationId
});
return {};
}
const Actions = {
togglePopup,
openPopup,
closePopup,
switchPage,
openNotification,
closeNotification,
back,
signout,
signin,
signup,
updateSubscription,
cancelSubscription,
continueSubscription,
updateNewsletter,
updateProfile,
refreshMemberData,
clearPopupNotification,
editBilling,
checkoutPlan,
updateNewsletterPreference,
showPopupNotification,
removeEmailFromSuppressionList,
oneClickSubscribe,
trackRecommendationClicked,
trackRecommendationSubscribed
};
/** Handle actions in the App, returns updated state */
export default async function ActionHandler({action, data, state, api}) {
const handler = Actions[action];
if (handler) {
return await handler({data, state, api}) || {};
}
return {};
}