mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
1196688b0e
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.
582 lines
18 KiB
JavaScript
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 {};
|
|
}
|