0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Implemented one-click-subscribe for recommendations (#18067)

fixes https://github.com/TryGhost/Product/issues/3856
This commit is contained in:
Simon Backx 2023-09-11 17:06:15 +02:00 committed by GitHub
parent 8263e34adc
commit f130fb2e85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 10 deletions

View file

@ -625,6 +625,13 @@ export default class App extends React.Component {
}, 2000); }, 2000);
} }
} catch (error) { } 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({ const popupNotification = createPopupNotification({
type: `${action}:failed`, type: `${action}:failed`,
autoHide: true, closeable: true, status: 'error', state: this.state, autoHide: true, closeable: true, status: 'error', state: this.state,

View file

@ -1,3 +1,4 @@
import setupGhostApi from './utils/api';
import {HumanReadableError} from './utils/errors'; import {HumanReadableError} from './utils/errors';
import {createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl} from './utils/helpers'; 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 = { const Actions = {
togglePopup, togglePopup,
openPopup, openPopup,
@ -496,7 +532,8 @@ const Actions = {
checkoutPlan, checkoutPlan,
updateNewsletterPreference, updateNewsletterPreference,
showPopupNotification, showPopupNotification,
removeEmailFromSuppressionList removeEmailFromSuppressionList,
oneClickSubscribe
}; };
/** Handle actions in the App, returns updated state */ /** Handle actions in the App, returns updated state */

View file

@ -1,8 +1,9 @@
import AppContext from '../../AppContext'; import AppContext from '../../AppContext';
import {useContext, useState, useEffect} from 'react'; import {useContext, useState, useEffect, useCallback} from 'react';
import CloseButton from '../common/CloseButton'; import CloseButton from '../common/CloseButton';
import {clearURLParams} from '../../utils/notifications'; import {clearURLParams} from '../../utils/notifications';
import LoadingPage from './LoadingPage'; import LoadingPage from './LoadingPage';
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark-fill.svg';
export const RecommendationsPageStyles = ` export const RecommendationsPageStyles = `
.gh-portal-recommendation-item .gh-portal-list-detail { .gh-portal-recommendation-item .gh-portal-list-detail {
@ -13,6 +14,7 @@ export const RecommendationsPageStyles = `
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
cursor: pointer;
} }
.gh-portal-recommendation-item-favicon { .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} />); 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 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 {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 ( return (
<section className="gh-portal-recommendation-item"> <section className="gh-portal-recommendation-item">
<div className="gh-portal-list-detail gh-portal-list-big"> <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} /> <RecommendationIcon title={title} favicon={favicon} featuredImage={featuredImage} />
<h3>{title}</h3> <h3>{title}</h3>
</div> </div>
{reason && <p>{reason}</p>} {reason && <p>{reason}</p>}
</div> </div>
<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> </div>
</section> </section>
); );
@ -93,8 +147,8 @@ const RecommendationsPage = () => {
useEffect(() => { useEffect(() => {
api.site.recommendations({limit: 100}).then((data) => { api.site.recommendations({limit: 100}).then((data) => {
setRecommendations( setRecommendations(
shuffleRecommendations(data.recommendations shuffleRecommendations(data.recommendations)
)); );
}).catch((err) => { }).catch((err) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err); console.error(err);

View file

@ -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 url = endpointFor({type: 'members', resource: 'send-magic-link'});
const body = { const body = {
name, name,
@ -241,9 +241,10 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
emailType, emailType,
labels, labels,
requestSrc: 'portal', requestSrc: 'portal',
redirect redirect,
autoRedirect
}; };
const urlHistory = getUrlHistory(); const urlHistory = customUrlHistory ?? getUrlHistory();
if (urlHistory) { if (urlHistory) {
body.urlHistory = urlHistory; body.urlHistory = urlHistory;
} }