diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js
index 330b5d252a..247eca8451 100644
--- a/ghost/portal/src/App.js
+++ b/ghost/portal/src/App.js
@@ -19,7 +19,7 @@ const DEV_MODE_DATA = {
showPopup: true,
site: Fixtures.site,
member: Fixtures.member.free,
- page: 'signup',
+ page: 'accountEmail',
...Fixtures.paidMemberOnTier(),
pageData: Fixtures.offer
};
@@ -393,6 +393,16 @@ export default class App extends React.Component {
/** Fetch state from Portal Links */
fetchLinkData() {
+ const qParams = new URLSearchParams(window.location.search);
+ if (qParams.get('uuid') && qParams.get('action') === 'unsubscribe') {
+ return {
+ showPopup: true,
+ page: 'unsubscribe',
+ pageData: {
+ uuid: qParams.get('uuid')
+ }
+ };
+ }
const productMonthlyPriceQueryRegex = /^(?:(\w+?))?\/monthly$/;
const productYearlyPriceQueryRegex = /^(?:(\w+?))?\/yearly$/;
const offersRegex = /^offers\/(\w+?)\/?$/;
diff --git a/ghost/portal/src/components/pages/UnsubscribePage.js b/ghost/portal/src/components/pages/UnsubscribePage.js
new file mode 100644
index 0000000000..49c7af4508
--- /dev/null
+++ b/ghost/portal/src/components/pages/UnsubscribePage.js
@@ -0,0 +1,85 @@
+import AppContext from '../../AppContext';
+
+import {useContext, useEffect, useState} from 'react';
+import {getSiteNewsletters} from '../../utils/helpers';
+import setupGhostApi from '../../utils/api';
+import NewsletterManagement from '../common/NewsletterManagement';
+
+const React = require('react');
+
+function AccountHeader() {
+ return (
+
+ );
+}
+
+export default function UnsubscribePage() {
+ const {site, pageData, onAction} = useContext(AppContext);
+ const api = setupGhostApi({siteUrl: site.url});
+ const [member, setMember] = useState();
+ const siteNewsletters = getSiteNewsletters({site});
+ const defaultNewsletters = siteNewsletters.filter((d) => {
+ return d.subscribe_on_signup;
+ });
+ const [subscribedNewsletters, setSubscribedNewsletters] = useState(defaultNewsletters);
+
+ useEffect(() => {
+ const ghostApi = setupGhostApi({siteUrl: site.url});
+ (async () => {
+ const memberData = await ghostApi.member.newsletters({uuid: pageData.uuid});
+
+ setMember(memberData);
+ setSubscribedNewsletters(memberData?.newsletters || []);
+ })();
+ }, [pageData.uuid, site.url]);
+
+ // Case: Email not found
+ if (member === null) {
+ return (
+
+
+
+
Unsubscribe Failed
+
+
+
Email address not found.
+
+
+ );
+ }
+
+ // Case: Single active newsletter
+ if (siteNewsletters?.length === 1) {
+ return (
+
+
+
+
Successfully unsubscribed
+
+
+
{member?.email} will no longer receive this newsletter.
+
+
+ );
+ }
+ return (
+ {
+ setSubscribedNewsletters(newsletters);
+ await api.member.updateNewsletters({uuid: pageData.uuid, newsletters});
+ }}
+ unsubscribeAll={async () => {
+ setSubscribedNewsletters([]);
+ onAction('showPopupNotification', {
+ action: 'updated:success',
+ message: `Newsletter preference updated.`
+ });
+ await api.member.updateNewsletters({uuid: pageData.uuid, newsletters: []});
+ }}
+ isPaidMember={member?.status !== 'free'}
+ />
+ );
+}
diff --git a/ghost/portal/src/pages.js b/ghost/portal/src/pages.js
index 0595ed6d8a..ad109234c9 100644
--- a/ghost/portal/src/pages.js
+++ b/ghost/portal/src/pages.js
@@ -8,6 +8,7 @@ import AccountProfilePage from './components/pages/AccountProfilePage';
import AccountEmailPage from './components/pages/AccountEmailPage';
import OfferPage from './components/pages/OfferPage';
import NewsletterSelectionPage from './components/pages/NewsletterSelectionPage';
+import UnsubscribePage from './components/pages/UnsubscribePage';
/** List of all available pages in Portal, mapped to their UI component
* Any new page added to portal needs to be mapped here
@@ -20,6 +21,7 @@ const Pages = {
accountProfile: AccountProfilePage,
accountEmail: AccountEmailPage,
signupNewsletter: NewsletterSelectionPage,
+ unsubscribe: UnsubscribePage,
magiclink: MagicLinkPage,
loading: LoadingPage,
offer: OfferPage
diff --git a/ghost/portal/src/utils/api.js b/ghost/portal/src/utils/api.js
index aeb147e963..2d40fcf5b5 100644
--- a/ghost/portal/src/utils/api.js
+++ b/ghost/portal/src/utils/api.js
@@ -192,6 +192,43 @@ function setupGhostApi({siteUrl = window.location.origin}) {
});
},
+ async newsletters({uuid}) {
+ let url = endpointFor({type: 'members', resource: `member/newsletters`});
+ url = url + `?uuid=${uuid}`;
+ return makeRequest({
+ url,
+ credentials: 'same-origin'
+ }).then(function (res) {
+ if (!res.ok || res.status === 204) {
+ return null;
+ }
+ return res.json();
+ });
+ },
+
+ async updateNewsletters({uuid, newsletters}) {
+ let url = endpointFor({type: 'members', resource: `member/newsletters`});
+ url = url + `?uuid=${uuid}`;
+ const body = {
+ newsletters
+ };
+
+ return makeRequest({
+ url,
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(body)
+ }).then(function (res) {
+ if (res.ok) {
+ return res.json();
+ } else {
+ throw new Error('Failed to upadte newsletter preferences');
+ }
+ });
+ },
+
async updateEmailAddress({email}) {
const identity = await api.member.identity();
const url = endpointFor({type: 'members', resource: 'member/email'});