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 {}; }