fix: Handle newsletter unsubscribe
There were a few issues with the newsletter unsubscribe links that we sent out in the newsletter. They were pointing to the backend for one thing. Also updated the frontend pages to handle unsubscribe from both users and subscribers.
This commit is contained in:
parent
f8feb5cf8b
commit
9208fb9a3c
10 changed files with 71 additions and 36 deletions
|
@ -115,7 +115,9 @@ const send = async (test = true) => {
|
||||||
if (l > 0) {
|
if (l > 0) {
|
||||||
const body = mustache.render(template, {
|
const body = mustache.render(template, {
|
||||||
...i18n[lang],
|
...i18n[lang],
|
||||||
unsubscribe: `${backend}newsletter/unsubscribe/${sub.ehash}`,
|
unsubscribe: `https://freesewing.org${
|
||||||
|
lang === 'en' ? '/' : '/' + lang + '/'
|
||||||
|
}newsletter/unsubscribe?x=${sub.ehash}`,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
console.log(`[${lang}] ${l}/${subs} (${i}) Sending to ${sub.email}`)
|
console.log(`[${lang}] ${l}/${subs} (${i}) Sending to ${sub.email}`)
|
||||||
|
|
|
@ -25,12 +25,12 @@ SubscribersController.prototype.subscribeConfirm = async (req, res, tools) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Unsubscribe confirmation
|
* Unsubscribe
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
SubscribersController.prototype.unsubscribeConfirm = async (req, res, tools) => {
|
SubscribersController.prototype.unsubscribe = async (req, res, tools) => {
|
||||||
const Subscriber = new SubscriberModel(tools)
|
const Subscriber = new SubscriberModel(tools)
|
||||||
await Subscriber.unsubscribeConfirm(req)
|
await Subscriber.unsubscribe(req)
|
||||||
|
|
||||||
return Subscriber.sendResponse(res)
|
return Subscriber.sendResponse(res)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export function SubscriberModel(tools) {
|
||||||
return decorateModel(this, tools, {
|
return decorateModel(this, tools, {
|
||||||
name: 'subscriber',
|
name: 'subscriber',
|
||||||
encryptedFields: ['email'],
|
encryptedFields: ['email'],
|
||||||
|
models: ['user'],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,32 +129,48 @@ SubscriberModel.prototype.subscribeConfirm = async function ({ body }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Confirms a pending unsubscription
|
* Unsubscribe a user
|
||||||
* This is an unauthenticated route
|
* This is an unauthenticated route (has to for newsletter subscribers might not be users)
|
||||||
*
|
*
|
||||||
* @param {body} object - The request body
|
* @param {body} object - The request body
|
||||||
* @returns {SubscriberModal} object - The SubscriberModel
|
* @returns {SubscriberModal} object - The SubscriberModel
|
||||||
*/
|
*/
|
||||||
SubscriberModel.prototype.unsubscribeConfirm = async function ({ params }) {
|
SubscriberModel.prototype.unsubscribe = async function ({ params }) {
|
||||||
/*
|
/*
|
||||||
* Validate input and load subscription record
|
* Is ehash set?
|
||||||
*/
|
*/
|
||||||
await this.verifySubscription(params)
|
if (!params.ehash) return this.setResponse(400, 'ehashMissing')
|
||||||
|
|
||||||
|
const { ehash } = params
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If a status code is already set, do not continue
|
* Find the subscription record
|
||||||
*/
|
*/
|
||||||
if (this.response?.status) return this
|
await this.read({ ehash })
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Remove the record
|
* If found, remove the record
|
||||||
*/
|
*/
|
||||||
|
if (this.record) {
|
||||||
await this.delete({ id: this.record.id })
|
await this.delete({ id: this.record.id })
|
||||||
|
|
||||||
/*
|
|
||||||
* Return 204
|
|
||||||
*/
|
|
||||||
return this.setResponse(204)
|
return this.setResponse(204)
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* If not, perhaps it's an account ehash rather than subscriber ehash
|
||||||
|
*/
|
||||||
|
await this.User.read({ ehash })
|
||||||
|
if (this.User.record) {
|
||||||
|
await this.User.update({ newsletter: false })
|
||||||
|
|
||||||
|
return this.setResponse(204)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return 404
|
||||||
|
*/
|
||||||
|
return this.setResponse(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1675,6 +1675,7 @@ UserModel.prototype.asAccount = function () {
|
||||||
consent: this.record.consent,
|
consent: this.record.consent,
|
||||||
control: this.record.control,
|
control: this.record.control,
|
||||||
createdAt: this.record.createdAt,
|
createdAt: this.record.createdAt,
|
||||||
|
ehash: this.record.ehash,
|
||||||
email: this.clear.email,
|
email: this.clear.email,
|
||||||
data,
|
data,
|
||||||
ihash: this.record.ihash,
|
ihash: this.record.ihash,
|
||||||
|
|
|
@ -16,5 +16,5 @@ export function subscribersRoutes(tools) {
|
||||||
app.put('/subscriber', (req, res) => Subscriber.subscribeConfirm(req, res, tools))
|
app.put('/subscriber', (req, res) => Subscriber.subscribeConfirm(req, res, tools))
|
||||||
|
|
||||||
// Unsubscribe from newsletter
|
// Unsubscribe from newsletter
|
||||||
app.delete('/subscriber/:id/:ehash', (req, res) => Subscriber.unsubscribeConfirm(req, res, tools))
|
app.delete('/subscriber/:ehash', (req, res) => Subscriber.unsubscribe(req, res, tools))
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ const NewsletterPage = ({ page }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newId = getSearchParam('id')
|
const newId = getSearchParam('id')
|
||||||
const newEhash = getSearchParam('ehash')
|
const newEhash = getSearchParam('check')
|
||||||
if (newId !== id) setId(newId)
|
if (newId !== id) setId(newId)
|
||||||
if (newEhash !== ehash) setEhash(newEhash)
|
if (newEhash !== ehash) setEhash(newEhash)
|
||||||
}, [id, ehash])
|
}, [id, ehash])
|
||||||
|
@ -49,6 +49,7 @@ const NewsletterPage = ({ page }) => {
|
||||||
return (
|
return (
|
||||||
<PageWrapper {...page} title={false}>
|
<PageWrapper {...page} title={false}>
|
||||||
<Hodl />
|
<Hodl />
|
||||||
|
<pre>{JSON.stringify({ id, ehash })}</pre>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ const NewsletterPage = ({ page }) => {
|
||||||
<p>{t('newsletter:subscribePs')}</p>
|
<p>{t('newsletter:subscribePs')}</p>
|
||||||
<p>
|
<p>
|
||||||
<PageLink
|
<PageLink
|
||||||
href={`/newsletter/unsubscribe/${id}/${ehash}`}
|
href={`/newsletter/unsubscribe?x=${ehash}`}
|
||||||
txt={t('newsletter:unsubscribeLink')}
|
txt={t('newsletter:unsubscribeLink')}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -85,7 +86,7 @@ const NewsletterPage = ({ page }) => {
|
||||||
<p>
|
<p>
|
||||||
{t('newsletter:faqLead')}:{' '}
|
{t('newsletter:faqLead')}:{' '}
|
||||||
<PageLink
|
<PageLink
|
||||||
href="/docs/faq/newsletter/why-subscribe-multiple-clicks"
|
href="/docs/about/faq/newsletter/why-subscribe-multiple-clicks"
|
||||||
txt={t('newsletter:subscribeWhy')}
|
txt={t('newsletter:subscribeWhy')}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -27,25 +27,22 @@ const NewsletterPage = ({ page }) => {
|
||||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||||
const backend = useBackend()
|
const backend = useBackend()
|
||||||
|
|
||||||
const [confirmed, setConfirmed] = useState(false)
|
|
||||||
const [id, setId] = useState()
|
|
||||||
const [ehash, setEhash] = useState()
|
const [ehash, setEhash] = useState()
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newId = getSearchParam('id')
|
const newEhash = getSearchParam('x')
|
||||||
const newEhash = getSearchParam('ehash')
|
|
||||||
if (newId !== id) setId(newId)
|
|
||||||
if (newEhash !== ehash) setEhash(newEhash)
|
if (newEhash !== ehash) setEhash(newEhash)
|
||||||
}, [id, ehash])
|
}, [ehash])
|
||||||
|
|
||||||
const handler = async () => {
|
const handler = async () => {
|
||||||
setLoadingStatus([true, 'status:contactingBackend'])
|
setLoadingStatus([true, 'status:contactingBackend'])
|
||||||
await backend.confirmNewsletterUnsubscribe({ id, ehash })
|
await backend.newsletterUnsubscribe(ehash)
|
||||||
setLoadingStatus([true, 'status:settingsSaved', true, true])
|
setLoadingStatus([true, 'status:settingsSaved', true, true])
|
||||||
setConfirmed(true)
|
setDone(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id || !ehash)
|
if (!ehash)
|
||||||
return (
|
return (
|
||||||
<PageWrapper {...page} title={false}>
|
<PageWrapper {...page} title={false}>
|
||||||
<Hodl />
|
<Hodl />
|
||||||
|
@ -55,7 +52,7 @@ const NewsletterPage = ({ page }) => {
|
||||||
return (
|
return (
|
||||||
<PageWrapper {...page} title={false}>
|
<PageWrapper {...page} title={false}>
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
{confirmed ? (
|
{done ? (
|
||||||
<>
|
<>
|
||||||
<h1>{t('newsletter:newsletter')}</h1>
|
<h1>{t('newsletter:newsletter')}</h1>
|
||||||
<p>{t('newsletter:thanksDone')}</p>
|
<p>{t('newsletter:thanksDone')}</p>
|
||||||
|
@ -76,7 +73,7 @@ const NewsletterPage = ({ page }) => {
|
||||||
<p>
|
<p>
|
||||||
{t('newsletter:faqLead')}:{' '}
|
{t('newsletter:faqLead')}:{' '}
|
||||||
<PageLink
|
<PageLink
|
||||||
href="/docs/faq/newsletter/why-unsubscribe-multiple-clicks"
|
href="/docs/about/faq/newsletter/why-unsubscribe-multiple-clicks"
|
||||||
txt={t('newsletter:unsubscribeWhy')}
|
txt={t('newsletter:unsubscribeWhy')}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -11,8 +11,10 @@ import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||||
import { ListInput } from 'shared/components/inputs.mjs'
|
import { ListInput } from 'shared/components/inputs.mjs'
|
||||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||||
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
||||||
|
import { Popout } from 'shared/components/popout/index.mjs'
|
||||||
|
import { PageLink } from 'shared/components/link.mjs'
|
||||||
|
|
||||||
export const ns = ['account', 'status']
|
export const ns = ['account', 'status', 'newsletter']
|
||||||
|
|
||||||
export const NewsletterSettings = ({ welcome = false, bare = false }) => {
|
export const NewsletterSettings = ({ welcome = false, bare = false }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
|
@ -89,6 +91,15 @@ export const NewsletterSettings = ({ welcome = false, bare = false }) => {
|
||||||
) : bare ? null : (
|
) : bare ? null : (
|
||||||
<BackToAccountButton />
|
<BackToAccountButton />
|
||||||
)}
|
)}
|
||||||
|
<Popout tip>
|
||||||
|
<p>{t('newsletter:subscribePs')}</p>
|
||||||
|
<p>
|
||||||
|
<PageLink
|
||||||
|
href={`/newsletter/unsubscribe?x=${account.ehash}`}
|
||||||
|
txt={t('newsletter:unsubscribeLink')}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Popout>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ const ConsentLacking = ({ banner, refresh }) => {
|
||||||
|
|
||||||
export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
||||||
const { t } = useTranslation(ns)
|
const { t } = useTranslation(ns)
|
||||||
const { account, token, admin, stopImpersonating, signOut } = useAccount()
|
const { account, setAccount, token, admin, stopImpersonating, signOut } = useAccount()
|
||||||
const backend = useBackend()
|
const backend = useBackend()
|
||||||
|
|
||||||
const [ready, setReady] = useState(false)
|
const [ready, setReady] = useState(false)
|
||||||
|
@ -151,7 +151,13 @@ export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
||||||
}
|
}
|
||||||
const verifyUser = async () => {
|
const verifyUser = async () => {
|
||||||
const result = await backend.ping()
|
const result = await backend.ping()
|
||||||
if (!result.success) {
|
if (result.success) {
|
||||||
|
// Refresh account in local storage
|
||||||
|
setAccount({
|
||||||
|
...account,
|
||||||
|
...result.data.account,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
if (result.data?.error?.error) setError(result.data.error.error)
|
if (result.data?.error?.error) setError(result.data.error.error)
|
||||||
else signOut()
|
else signOut()
|
||||||
}
|
}
|
||||||
|
|
|
@ -500,10 +500,10 @@ Backend.prototype.confirmNewsletterSubscribe = async function ({ id, ehash }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Confirm newsletter unsubscribe
|
* Newsletter unsubscribe
|
||||||
*/
|
*/
|
||||||
Backend.prototype.confirmNewsletterUnsubscribe = async function ({ id, ehash }) {
|
Backend.prototype.newsletterUnsubscribe = async function (ehash) {
|
||||||
return responseHandler(await api.delete(`/subscriber/${id}/${ehash}`))
|
return responseHandler(await api.delete(`/subscriber/${ehash}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue