mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-25 02:31:59 -05:00
Add suppression modal to Portal
closes TryGhost/Team#2256 - Users can remove themselves from the suppression list from the profile in Portal.
This commit is contained in:
parent
69c2af1ffe
commit
f5e1e6296c
18 changed files with 289 additions and 31 deletions
|
@ -1,21 +1,27 @@
|
||||||
const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list');
|
const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list');
|
||||||
|
|
||||||
class InMemoryEmailSuppressionList extends AbstractEmailSuppressionList {
|
class InMemoryEmailSuppressionList extends AbstractEmailSuppressionList {
|
||||||
|
store = ['spam@member.test', 'fail@member.test'];
|
||||||
|
|
||||||
async removeEmail(email) {
|
async removeEmail(email) {
|
||||||
if (email === 'fail@member.test') {
|
if ((email === 'fail@member.test' || email === 'spam@member.test') && this.store.includes(email)) {
|
||||||
return false;
|
this.store = this.store.filter(el => el !== email);
|
||||||
|
|
||||||
|
setTimeout(() => this.store.push(email), 3000);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSuppressionData(email) {
|
async getSuppressionData(email) {
|
||||||
if (email === 'spam@member.test') {
|
if (email === 'spam@member.test' && this.store.includes(email)) {
|
||||||
return new EmailSuppressionData(true, {
|
return new EmailSuppressionData(true, {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
reason: 'spam'
|
reason: 'spam'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (email === 'fail@member.test') {
|
if (email === 'fail@member.test' && this.store.includes(email)) {
|
||||||
return new EmailSuppressionData(true, {
|
return new EmailSuppressionData(true, {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
reason: 'fail'
|
reason: 'fail'
|
||||||
|
|
|
@ -301,6 +301,29 @@ async function updateNewsletterPreference({data, state, api}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeEmailFromSuppressionList({state, api}) {
|
||||||
|
try {
|
||||||
|
await api.member.deleteSuppression();
|
||||||
|
const action = 'removeEmailFromSuppressionList:success';
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
popupNotification: createPopupNotification({
|
||||||
|
type: 'removeEmailFromSuppressionList:success', autoHide: true, closeable: true, state, status: 'success',
|
||||||
|
message: 'You have been successfully resubscribed'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
action: 'removeEmailFromSuppressionList:failed',
|
||||||
|
popupNotification: createPopupNotification({
|
||||||
|
type: 'removeEmailFromSuppressionList:failed',
|
||||||
|
autoHide: true, closeable: true, state, status: 'error',
|
||||||
|
message: 'Your email has failed to resubscribe, please try again'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateNewsletter({data, state, api}) {
|
async function updateNewsletter({data, state, api}) {
|
||||||
try {
|
try {
|
||||||
const {subscribed} = data;
|
const {subscribed} = data;
|
||||||
|
@ -377,14 +400,16 @@ async function refreshMemberData({state, api}) {
|
||||||
if (member) {
|
if (member) {
|
||||||
return {
|
return {
|
||||||
member,
|
member,
|
||||||
success: true
|
success: true,
|
||||||
|
action: 'refreshMemberData:success'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: err
|
error: err,
|
||||||
|
action: 'refreshMemberData:failed'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -469,7 +494,8 @@ const Actions = {
|
||||||
editBilling,
|
editBilling,
|
||||||
checkoutPlan,
|
checkoutPlan,
|
||||||
updateNewsletterPreference,
|
updateNewsletterPreference,
|
||||||
showPopupNotification
|
showPopupNotification,
|
||||||
|
removeEmailFromSuppressionList
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Handle actions in the App, returns updated state */
|
/** Handle actions in the App, returns updated state */
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {MagicLinkStyles} from './pages/MagicLinkPage';
|
||||||
import {PopupNotificationStyles} from './common/PopupNotification';
|
import {PopupNotificationStyles} from './common/PopupNotification';
|
||||||
import {OfferPageStyles} from './pages/OfferPage';
|
import {OfferPageStyles} from './pages/OfferPage';
|
||||||
import {FeedbackPageStyles} from './pages/FeedbackPage';
|
import {FeedbackPageStyles} from './pages/FeedbackPage';
|
||||||
|
import EmailSuppressedPage from '!!raw-loader!./pages/EmailSuppressedPage.css';
|
||||||
|
|
||||||
// Global styles
|
// Global styles
|
||||||
const FrameStyles = `
|
const FrameStyles = `
|
||||||
|
@ -1173,6 +1174,7 @@ export function getFrameStyles({site}) {
|
||||||
PopupNotificationStyles +
|
PopupNotificationStyles +
|
||||||
MobileStyles +
|
MobileStyles +
|
||||||
MultipleProductsGlobalStyles +
|
MultipleProductsGlobalStyles +
|
||||||
FeedbackPageStyles;
|
FeedbackPageStyles +
|
||||||
|
EmailSuppressedPage;
|
||||||
return FrameStyle;
|
return FrameStyle;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,21 @@ footer.gh-portal-account-footer {
|
||||||
margin: 24px 0 32px;
|
margin: 24px 0 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-portal-list-detail .gh-portal-email-notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--red);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-portal-email-notice-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.gh-portal-billing-button-loader {
|
.gh-portal-billing-button-loader {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {render, fireEvent} from 'utils/test-utils';
|
import {render, fireEvent} from 'utils/test-utils';
|
||||||
import AccountHomePage from './AccountHomePage';
|
import AccountHomePage from './AccountHomePage';
|
||||||
|
import {member} from 'utils/test-fixtures';
|
||||||
|
import {site} from 'utils/fixtures';
|
||||||
|
|
||||||
const setup = (overrides) => {
|
const setup = (overrides) => {
|
||||||
const {mockOnActionFn, ...utils} = render(
|
const {mockOnActionFn, ...utils} = render(
|
||||||
<AccountHomePage />
|
<AccountHomePage />,
|
||||||
|
{
|
||||||
|
overrideContext: {
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const logoutBtn = utils.queryByRole('button', {name: 'logout'});
|
const logoutBtn = utils.queryByRole('button', {name: 'logout'});
|
||||||
return {
|
return {
|
||||||
logoutBtn,
|
logoutBtn,
|
||||||
mockOnActionFn,
|
mockOnActionFn,
|
||||||
...utils
|
utils
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Account Home Page', () => {
|
describe('Account Home Page', () => {
|
||||||
test('renders', () => {
|
test('renders', () => {
|
||||||
const {logoutBtn} = setup();
|
const {logoutBtn, utils} = setup();
|
||||||
expect(logoutBtn).toBeInTheDocument();
|
expect(logoutBtn).toBeInTheDocument();
|
||||||
|
expect(utils.queryByText('You\'re currently not receiving emails')).not.toBeInTheDocument();
|
||||||
|
expect(utils.queryByText('Email newsletter')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can call signout', () => {
|
test('can call signout', () => {
|
||||||
|
@ -26,4 +35,29 @@ describe('Account Home Page', () => {
|
||||||
fireEvent.click(logoutBtn);
|
fireEvent.click(logoutBtn);
|
||||||
expect(mockOnActionFn).toHaveBeenCalledWith('signout');
|
expect(mockOnActionFn).toHaveBeenCalledWith('signout');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can show suppressed info', () => {
|
||||||
|
const {mockOnActionFn, utils} = setup({member: member.suppressed});
|
||||||
|
|
||||||
|
expect(utils.queryByText('You\'re currently not receiving emails')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const manageBtn = utils.queryByRole('button', {name: 'Manage'});
|
||||||
|
expect(manageBtn).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(manageBtn);
|
||||||
|
expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', {lastPage: 'accountHome', page: 'emailSuppressed'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can show Manage button for few newsletters', () => {
|
||||||
|
const {mockOnActionFn, utils} = setup({site: site});
|
||||||
|
|
||||||
|
expect(utils.queryByText('Update your preferences')).toBeInTheDocument();
|
||||||
|
expect(utils.queryByText('You\'re currently not receiving emails')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const manageBtn = utils.queryByRole('button', {name: 'Manage'});
|
||||||
|
expect(manageBtn).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(manageBtn);
|
||||||
|
expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', {lastPage: 'accountHome', page: 'accountEmail'});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import AppContext from 'AppContext';
|
import AppContext from 'AppContext';
|
||||||
import {useContext} from 'react';
|
import {useContext} from 'react';
|
||||||
|
import {hasCommentsEnabled, hasMultipleNewsletters, isEmailSuppressed} from 'utils/helpers';
|
||||||
|
|
||||||
import PaidAccountActions from './PaidAccountActions';
|
import PaidAccountActions from './PaidAccountActions';
|
||||||
import EmailNewsletterAction from './EmailNewsletterAction';
|
import EmailNewsletterAction from './EmailNewsletterAction';
|
||||||
import EmailPreferencesAction from './EmailPreferencesAction';
|
import EmailPreferencesAction from './EmailPreferencesAction';
|
||||||
|
|
||||||
const AccountActions = () => {
|
const AccountActions = () => {
|
||||||
const {member, onAction} = useContext(AppContext);
|
const {member, onAction, site} = useContext(AppContext);
|
||||||
const {name, email} = member;
|
const {name, email} = member;
|
||||||
|
|
||||||
const openEditProfile = () => {
|
const openEditProfile = () => {
|
||||||
|
@ -16,6 +17,8 @@ const AccountActions = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showEmailPreferences = hasMultipleNewsletters({site}) || hasCommentsEnabled({site}) || isEmailSuppressed({member});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='gh-portal-list'>
|
<div className='gh-portal-list'>
|
||||||
|
@ -28,8 +31,12 @@ const AccountActions = () => {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<PaidAccountActions />
|
<PaidAccountActions />
|
||||||
<EmailPreferencesAction />
|
{
|
||||||
<EmailNewsletterAction />
|
showEmailPreferences
|
||||||
|
? <EmailPreferencesAction />
|
||||||
|
: <EmailNewsletterAction />
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/* <ProductList openUpdatePlan={openUpdatePlan}></ProductList> */}
|
{/* <ProductList openUpdatePlan={openUpdatePlan}></ProductList> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import AppContext from 'AppContext';
|
import AppContext from 'AppContext';
|
||||||
import Switch from 'components/common/Switch';
|
import Switch from 'components/common/Switch';
|
||||||
import {getSiteNewsletters, hasCommentsEnabled, hasMultipleNewsletters} from 'utils/helpers';
|
import {getSiteNewsletters} from 'utils/helpers';
|
||||||
import {useContext} from 'react';
|
import {useContext} from 'react';
|
||||||
|
|
||||||
function EmailNewsletterAction() {
|
function EmailNewsletterAction() {
|
||||||
const {member, site, onAction} = useContext(AppContext);
|
const {member, site, onAction} = useContext(AppContext);
|
||||||
let {newsletters} = member;
|
let {newsletters} = member;
|
||||||
|
|
||||||
if (hasMultipleNewsletters({site}) || hasCommentsEnabled({site})) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const subscribed = !!newsletters?.length;
|
const subscribed = !!newsletters?.length;
|
||||||
let label = subscribed ? 'Subscribed' : 'Unsubscribed';
|
let label = subscribed ? 'Subscribed' : 'Unsubscribed';
|
||||||
const onToggleSubscription = (e, sub) => {
|
const onToggleSubscription = (e, sub) => {
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
import AppContext from 'AppContext';
|
import AppContext from 'AppContext';
|
||||||
import {hasCommentsEnabled, hasMultipleNewsletters} from 'utils/helpers';
|
|
||||||
import {useContext} from 'react';
|
import {useContext} from 'react';
|
||||||
|
import {isEmailSuppressed} from 'utils/helpers';
|
||||||
|
import {ReactComponent as EmailDeliveryFailedIcon} from 'images/icons/email-delivery-failed.svg';
|
||||||
|
|
||||||
function EmailPreferencesAction() {
|
function EmailPreferencesAction() {
|
||||||
const {site, onAction} = useContext(AppContext);
|
const {onAction, member} = useContext(AppContext);
|
||||||
if (!hasMultipleNewsletters({site}) && !hasCommentsEnabled({site})) {
|
const emailSuppressed = isEmailSuppressed({member});
|
||||||
return null;
|
const page = emailSuppressed ? 'emailSuppressed' : 'accountEmail';
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className='gh-portal-list-detail'>
|
<div className='gh-portal-list-detail'>
|
||||||
<h3>Emails</h3>
|
<h3>Emails</h3>
|
||||||
<p>Update your preferences</p>
|
{
|
||||||
|
emailSuppressed
|
||||||
|
? (
|
||||||
|
<p className="gh-portal-email-notice">
|
||||||
|
<EmailDeliveryFailedIcon className="gh-portal-email-notice-icon" />
|
||||||
|
You're currently not receiving emails
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
: <p>Update your preferences</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<button className='gh-portal-btn gh-portal-btn-list' onClick={(e) => {
|
<button className='gh-portal-btn gh-portal-btn-list' onClick={(e) => {
|
||||||
onAction('switchPage', {
|
onAction('switchPage', {
|
||||||
page: 'accountEmail',
|
page,
|
||||||
lastPage: 'accountHome'
|
lastPage: 'accountHome'
|
||||||
});
|
});
|
||||||
}}>Manage</button>
|
}}>Manage</button>
|
||||||
|
|
20
ghost/portal/src/components/pages/EmailSuppressedPage.css
Normal file
20
ghost/portal/src/components/pages/EmailSuppressedPage.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.gh-email-suppressed-page-title {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-email-suppressed-page-icon {
|
||||||
|
display: block;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
margin: 0 auto 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-email-suppressed-page-text {
|
||||||
|
padding: 0 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--grey6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-email-suppressed-page-text a {
|
||||||
|
color: var(--grey3);
|
||||||
|
}
|
63
ghost/portal/src/components/pages/EmailSuppressedPage.js
Normal file
63
ghost/portal/src/components/pages/EmailSuppressedPage.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import AppContext from 'AppContext';
|
||||||
|
import {useContext, useEffect} from 'react';
|
||||||
|
import CloseButton from 'components/common/CloseButton';
|
||||||
|
import BackButton from 'components/common/BackButton';
|
||||||
|
import ActionButton from 'components/common/ActionButton';
|
||||||
|
import {ReactComponent as EmailDeliveryFailedIcon} from 'images/icons/email-delivery-failed.svg';
|
||||||
|
|
||||||
|
export default function EmailSuppressedPage() {
|
||||||
|
const {brandColor, lastPage, onAction, action} = useContext(AppContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (['removeEmailFromSuppressionList:success'].includes(action)) {
|
||||||
|
onAction('refreshMemberData');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['removeEmailFromSuppressionList:failed', 'refreshMemberData:success', 'refreshMemberData:failed'].includes(action)) {
|
||||||
|
onAction('back');
|
||||||
|
}
|
||||||
|
}, [action, onAction]);
|
||||||
|
|
||||||
|
const isRunning = ['removeEmailFromSuppressionList:running', 'refreshMemberData:running'].includes(action);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onAction('removeEmailFromSuppressionList');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gh-email-suppressed-page">
|
||||||
|
<header className='gh-portal-detail-header'>
|
||||||
|
<BackButton brandColor={brandColor} hidden={!lastPage} onClick={() => {
|
||||||
|
onAction('back');
|
||||||
|
}} />
|
||||||
|
<CloseButton />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<EmailDeliveryFailedIcon className="gh-email-suppressed-page-icon" />
|
||||||
|
|
||||||
|
<div className="gh-email-suppressed-page-text">
|
||||||
|
<h3 className="gh-portal-main-title gh-email-suppressed-page-title">Email disabled</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
All newsletters have been disabled on your account. <br/> This can happen due to a spam complaint or
|
||||||
|
permanent failure (bounce).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://ghost.org" target="_blank" rel="noopener noreferrer" onClick={() => {
|
||||||
|
window.open('https://ghost.org', '_blank');
|
||||||
|
}}>
|
||||||
|
Learn more about why this happens
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
classes="gh-portal-confirm-button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isRunning}
|
||||||
|
brandColor={brandColor}
|
||||||
|
label="Resubscribe your email"
|
||||||
|
isRunning={isRunning}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {render, fireEvent} from 'utils/test-utils';
|
||||||
|
import EmailSuppressedPage from './EmailSuppressedPage';
|
||||||
|
|
||||||
|
const setup = (overrides) => {
|
||||||
|
const {mockOnActionFn, ...utils} = render(
|
||||||
|
<EmailSuppressedPage />
|
||||||
|
);
|
||||||
|
const resubscribeBtn = utils.queryByRole('button', {name: 'Resubscribe your email'});
|
||||||
|
const title = utils.queryByText('Email disabled');
|
||||||
|
|
||||||
|
return {
|
||||||
|
resubscribeBtn,
|
||||||
|
title,
|
||||||
|
mockOnActionFn,
|
||||||
|
...utils
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Email Suppressed Page', () => {
|
||||||
|
test('renders', () => {
|
||||||
|
const {resubscribeBtn, title} = setup();
|
||||||
|
expect(title).toBeInTheDocument();
|
||||||
|
expect(resubscribeBtn).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can call resubscribe button', () => {
|
||||||
|
const {mockOnActionFn, resubscribeBtn} = setup();
|
||||||
|
|
||||||
|
fireEvent.click(resubscribeBtn);
|
||||||
|
expect(mockOnActionFn).toHaveBeenCalledWith('removeEmailFromSuppressionList');
|
||||||
|
});
|
||||||
|
});
|
4
ghost/portal/src/images/icons/email-delivery-failed.svg
Normal file
4
ghost/portal/src/images/icons/email-delivery-failed.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path d="m12.79 12.376 4.08 4.079m-4.08 0 4.08-4.08" stroke="red" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="m10.439 17.994-2.664-2.652-2.86 1.478.111-4.239m0 0L1.677 9.232a1.344 1.344 0 0 1-.366-1.222 1.369 1.369 0 0 1 .904-1.05l13.111-4.374a1.369 1.369 0 0 1 1.76 1.758L15 10m-9.974 2.581 11.67-9.727" stroke="#A3A3A3" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 513 B |
|
@ -10,6 +10,7 @@ import OfferPage from './components/pages/OfferPage';
|
||||||
import NewsletterSelectionPage from './components/pages/NewsletterSelectionPage';
|
import NewsletterSelectionPage from './components/pages/NewsletterSelectionPage';
|
||||||
import UnsubscribePage from './components/pages/UnsubscribePage';
|
import UnsubscribePage from './components/pages/UnsubscribePage';
|
||||||
import FeedbackPage from './components/pages/FeedbackPage';
|
import FeedbackPage from './components/pages/FeedbackPage';
|
||||||
|
import EmailSuppressedPage from './components/pages/EmailSuppressedPage';
|
||||||
|
|
||||||
/** List of all available pages in Portal, mapped to their UI component
|
/** List of all available pages in Portal, mapped to their UI component
|
||||||
* Any new page added to portal needs to be mapped here
|
* Any new page added to portal needs to be mapped here
|
||||||
|
@ -26,7 +27,8 @@ const Pages = {
|
||||||
magiclink: MagicLinkPage,
|
magiclink: MagicLinkPage,
|
||||||
loading: LoadingPage,
|
loading: LoadingPage,
|
||||||
offer: OfferPage,
|
offer: OfferPage,
|
||||||
feedback: FeedbackPage
|
feedback: FeedbackPage,
|
||||||
|
emailSuppressed: EmailSuppressedPage
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Return page if valid, fallback to signup */
|
/** Return page if valid, fallback to signup */
|
||||||
|
|
|
@ -199,6 +199,20 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteSuppression() {
|
||||||
|
const url = endpointFor({type: 'members', resource: 'member/suppression'});
|
||||||
|
|
||||||
|
return makeRequest({
|
||||||
|
url,
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Your email has failed to resubscribe, please try again');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters}) {
|
async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters}) {
|
||||||
const url = endpointFor({type: 'members', resource: 'send-magic-link'});
|
const url = endpointFor({type: 'members', resource: 'send-magic-link'});
|
||||||
const body = {
|
const body = {
|
||||||
|
|
|
@ -113,7 +113,11 @@ export function getMemberData({
|
||||||
subscriptions = [],
|
subscriptions = [],
|
||||||
paid = false,
|
paid = false,
|
||||||
avatarImage: avatar_image = '',
|
avatarImage: avatar_image = '',
|
||||||
subscribed = true
|
subscribed = true,
|
||||||
|
email_suppression = {
|
||||||
|
suppressed: false,
|
||||||
|
info: null
|
||||||
|
}
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
uuid: `member_${objectId()}`,
|
uuid: `member_${objectId()}`,
|
||||||
|
@ -123,7 +127,8 @@ export function getMemberData({
|
||||||
paid,
|
paid,
|
||||||
subscribed,
|
subscribed,
|
||||||
avatar_image,
|
avatar_image,
|
||||||
subscriptions
|
subscriptions,
|
||||||
|
email_suppression
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -497,6 +497,10 @@ export function hasMultipleNewsletters({site}) {
|
||||||
return newsletters?.length > 1;
|
return newsletters?.length > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmailSuppressed({member}) {
|
||||||
|
return member?.email_suppression?.suppressed;
|
||||||
|
}
|
||||||
|
|
||||||
export function hasOnlyFreeProduct({site}) {
|
export function hasOnlyFreeProduct({site}) {
|
||||||
const products = getSiteProducts({site});
|
const products = getSiteProducts({site});
|
||||||
return (products.length === 1 && hasFreeProductPrice({site}));
|
return (products.length === 1 && hasFreeProductPrice({site}));
|
||||||
|
|
|
@ -189,6 +189,22 @@ export const member = {
|
||||||
avatarImage: '',
|
avatarImage: '',
|
||||||
subscribed: true
|
subscribed: true
|
||||||
}),
|
}),
|
||||||
|
suppressed: getMemberData({
|
||||||
|
name: 'Jamie Larson',
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
firstname: 'Jamie',
|
||||||
|
subscriptions: [],
|
||||||
|
paid: false,
|
||||||
|
avatarImage: '',
|
||||||
|
subscribed: true,
|
||||||
|
email_suppression: {
|
||||||
|
suppressed: true,
|
||||||
|
info: {
|
||||||
|
reason: 'spam',
|
||||||
|
timestamp: '2022-11-23T09:54:06.210Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
paid: getMemberData({
|
paid: getMemberData({
|
||||||
paid: true,
|
paid: true,
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
|
|
|
@ -16765,7 +16765,7 @@ jest-pnp-resolver@^1.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
|
resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
|
||||||
integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
|
integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
|
||||||
|
|
||||||
jest-raw-loader@^1.0.1:
|
jest-raw-loader@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz#ce9f56d54650f157c4a7d16d224ba5d613bcd626"
|
resolved "https://registry.yarnpkg.com/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz#ce9f56d54650f157c4a7d16d224ba5d613bcd626"
|
||||||
integrity sha512-g9oaAjeC4/rIJk1Wd3RxVbOfMizowM7LSjEJqa4R9qDX0OjQNABXOhH+GaznUp+DjTGVPi2vPPbQXyX87DOnYg==
|
integrity sha512-g9oaAjeC4/rIJk1Wd3RxVbOfMizowM7LSjEJqa4R9qDX0OjQNABXOhH+GaznUp+DjTGVPi2vPPbQXyX87DOnYg==
|
||||||
|
@ -22206,7 +22206,7 @@ raw-body@~1.1.0:
|
||||||
bytes "1"
|
bytes "1"
|
||||||
string_decoder "0.10"
|
string_decoder "0.10"
|
||||||
|
|
||||||
raw-loader@^4.0.2:
|
raw-loader@4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
|
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
|
||||||
integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
|
integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
|
||||||
|
|
Loading…
Add table
Reference in a new issue