mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Implemented one-click-subscribe for recommendations (#18067)
fixes https://github.com/TryGhost/Product/issues/3856
This commit is contained in:
parent
8263e34adc
commit
f130fb2e85
4 changed files with 109 additions and 10 deletions
|
@ -625,6 +625,13 @@ export default class App extends React.Component {
|
|||
}, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[Portal] Failed to dispatch action: ${action}`, error);
|
||||
|
||||
if (data && data.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const popupNotification = createPopupNotification({
|
||||
type: `${action}:failed`,
|
||||
autoHide: true, closeable: true, status: 'error', state: this.state,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import setupGhostApi from './utils/api';
|
||||
import {HumanReadableError} from './utils/errors';
|
||||
import {createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl} from './utils/helpers';
|
||||
|
||||
|
@ -474,6 +475,41 @@ async function updateProfile({data, state, api}) {
|
|||
};
|
||||
}
|
||||
|
||||
async function oneClickSubscribe({data: {siteUrl}, state}) {
|
||||
const externalSiteApi = setupGhostApi({siteUrl: siteUrl, apiUrl: 'not-defined', contentApiKey: 'not-defined'});
|
||||
const {t, member} = state;
|
||||
|
||||
const referrerUrl = window.location.href;
|
||||
const referrerSource = window.location.hostname.replace(/^www\./, '');
|
||||
|
||||
await externalSiteApi.member.sendMagicLink({
|
||||
emailType: 'signup',
|
||||
name: member.name,
|
||||
email: member.email,
|
||||
autoRedirect: false,
|
||||
customUrlHistory: [
|
||||
{
|
||||
time: Date.now(),
|
||||
referrerSource,
|
||||
referrerMedium: 'Ghost Recommendations',
|
||||
referrerUrl
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
popupNotification: createPopupNotification({
|
||||
type: 'subscribe:success',
|
||||
autoHide: true,
|
||||
closeable: true,
|
||||
duration: 10000,
|
||||
status: 'success',
|
||||
state,
|
||||
message: t(`To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!`)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const Actions = {
|
||||
togglePopup,
|
||||
openPopup,
|
||||
|
@ -496,7 +532,8 @@ const Actions = {
|
|||
checkoutPlan,
|
||||
updateNewsletterPreference,
|
||||
showPopupNotification,
|
||||
removeEmailFromSuppressionList
|
||||
removeEmailFromSuppressionList,
|
||||
oneClickSubscribe
|
||||
};
|
||||
|
||||
/** Handle actions in the App, returns updated state */
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import AppContext from '../../AppContext';
|
||||
import {useContext, useState, useEffect} from 'react';
|
||||
import {useContext, useState, useEffect, useCallback} from 'react';
|
||||
import CloseButton from '../common/CloseButton';
|
||||
import {clearURLParams} from '../../utils/notifications';
|
||||
import LoadingPage from './LoadingPage';
|
||||
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark-fill.svg';
|
||||
|
||||
export const RecommendationsPageStyles = `
|
||||
.gh-portal-recommendation-item .gh-portal-list-detail {
|
||||
|
@ -13,6 +14,7 @@ export const RecommendationsPageStyles = `
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gh-portal-recommendation-item-favicon {
|
||||
|
@ -64,21 +66,73 @@ const RecommendationIcon = ({title, favicon, featuredImage}) => {
|
|||
return (<img className="gh-portal-recommendation-item-favicon" src={icon} alt={title} onError={hideIcon} />);
|
||||
};
|
||||
|
||||
const prepareTab = () => {
|
||||
return window.open('', '_blank');
|
||||
};
|
||||
|
||||
const openTab = (tab, url) => {
|
||||
if (tab) {
|
||||
tab.location.href = url;
|
||||
tab.focus();
|
||||
} else {
|
||||
// Probably failed to create a tab
|
||||
window.location.href = url;
|
||||
}
|
||||
};
|
||||
|
||||
const RecommendationItem = (recommendation) => {
|
||||
const {t} = useContext(AppContext);
|
||||
const {t, onAction, member} = useContext(AppContext);
|
||||
const {title, url, reason, favicon, one_click_subscribe: oneClickSubscribe, featured_image: featuredImage} = recommendation;
|
||||
const allowOneClickSubscribe = member && oneClickSubscribe;
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
const visitHandler = useCallback(() => {
|
||||
// Open url in a new tab
|
||||
const tab = window.open(url, '_blank');
|
||||
tab?.focus();
|
||||
}, [url]);
|
||||
|
||||
const oneClickSubscribeHandler = useCallback(async () => {
|
||||
// We need to open a tab immediately, otherwise it is not possible to open a tab in case of errors later
|
||||
// after the async operation is done (browser blocks it outside of user interaction)
|
||||
const tab = prepareTab();
|
||||
|
||||
try {
|
||||
await onAction('oneClickSubscribe', {
|
||||
siteUrl: url,
|
||||
throwErrors: true
|
||||
});
|
||||
setSubscribed(true);
|
||||
tab.close();
|
||||
} catch (_) {
|
||||
// Open portal signup page
|
||||
const signupUrl = new URL('#/portal/signup', url);
|
||||
|
||||
// Trigger a visit
|
||||
openTab(tab, signupUrl);
|
||||
}
|
||||
}, [setSubscribed, url]);
|
||||
|
||||
const clickHandler = useCallback((e) => {
|
||||
if (allowOneClickSubscribe) {
|
||||
oneClickSubscribeHandler(e);
|
||||
} else {
|
||||
visitHandler(e);
|
||||
}
|
||||
}, [allowOneClickSubscribe, oneClickSubscribeHandler, visitHandler]);
|
||||
|
||||
return (
|
||||
<section className="gh-portal-recommendation-item">
|
||||
<div className="gh-portal-list-detail gh-portal-list-big">
|
||||
<div className="gh-portal-recommendation-item-header">
|
||||
<div className="gh-portal-recommendation-item-header" onClick={visitHandler}>
|
||||
<RecommendationIcon title={title} favicon={favicon} featuredImage={featuredImage} />
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
{reason && <p>{reason}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" className="gh-portal-btn gh-portal-btn-list">{oneClickSubscribe ? t('Subscribe') : t('Visit')}</a>
|
||||
{subscribed && <CheckmarkIcon className='gh-portal-checkmark-icon' alt='' />}
|
||||
{!subscribed && <button type="button" className="gh-portal-btn gh-portal-btn-list" onClick={clickHandler}>{allowOneClickSubscribe ? t('Subscribe') : t('Visit')}</button>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
@ -93,8 +147,8 @@ const RecommendationsPage = () => {
|
|||
useEffect(() => {
|
||||
api.site.recommendations({limit: 100}).then((data) => {
|
||||
setRecommendations(
|
||||
shuffleRecommendations(data.recommendations
|
||||
));
|
||||
shuffleRecommendations(data.recommendations)
|
||||
);
|
||||
}).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
|
|
@ -231,7 +231,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
|
|||
});
|
||||
},
|
||||
|
||||
async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters, redirect}) {
|
||||
async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters, redirect, customUrlHistory, autoRedirect = true}) {
|
||||
const url = endpointFor({type: 'members', resource: 'send-magic-link'});
|
||||
const body = {
|
||||
name,
|
||||
|
@ -241,9 +241,10 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
|
|||
emailType,
|
||||
labels,
|
||||
requestSrc: 'portal',
|
||||
redirect
|
||||
redirect,
|
||||
autoRedirect
|
||||
};
|
||||
const urlHistory = getUrlHistory();
|
||||
const urlHistory = customUrlHistory ?? getUrlHistory();
|
||||
if (urlHistory) {
|
||||
body.urlHistory = urlHistory;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue