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:
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);
|
}, 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,
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue