Skip to content

Commit

Permalink
feat: integrate beehiiv newsletter subscription (#1208)
Browse files Browse the repository at this point in the history
  • Loading branch information
andyesp authored and 1emu committed Sep 11, 2023
1 parent 3d1e8aa commit 53678e4
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 76 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ DISCORD_CHANNEL_ID=
DISCORD_SERVICE_ENABLED=false
DISCORD_TOKEN="example-token"

#DCL Data
# DCL Data
GATSBY_DCL_DATA_API=https://data.decentraland.vote/

# Rollbar Token
DAO_ROLLBAR_TOKEN=""

# Newsletter
BEEHIIV_API_KEY=
BEEHIIV_PUBLICATION_ID=
40 changes: 40 additions & 0 deletions src/back/routes/newsletter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import RequestError from 'decentraland-gatsby/dist/entities/Route/error'
import handleAPI from 'decentraland-gatsby/dist/entities/Route/handle'
import routes from 'decentraland-gatsby/dist/entities/Route/routes'
import { Request } from 'express'
import fetch from 'isomorphic-fetch'
import isEmail from 'validator/lib/isEmail'

import { ErrorService } from '../../services/ErrorService'
import { ErrorCategory } from '../../utils/errorCategories'

export default routes((router) => {
router.post('/newsletter-subscribe', handleAPI(handleSubscription))
})

async function handleSubscription(req: Request) {
const email = req.body.email
if (!isEmail(email)) {
throw new RequestError('Invalid email', RequestError.BadRequest)
}

const url = `https://api.beehiiv.com/v2/publications/${process.env.BEEHIIV_PUBLICATION_ID}/subscriptions`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${process.env.BEEHIIV_API_KEY}`,
},
body: `{ "email": "${email}" }`,
})

const data = await response.json()
if (data.errors) {
const errorMessage = 'Error subscribing to newsletter'
ErrorService.report(errorMessage, { email, error: JSON.stringify(data.errors), category: ErrorCategory.Newsletter })
throw new RequestError(errorMessage, RequestError.InternalServerError)
}

return data
}
23 changes: 0 additions & 23 deletions src/clients/Decentraland.ts

This file was deleted.

10 changes: 10 additions & 0 deletions src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,4 +616,14 @@ export class Governance extends API {
)
return response.data
}

async subscribeToNewsletter(email: string) {
const response = await this.fetch<ApiResponse<string>>(
`/newsletter-subscribe`,
this.options().method('POST').json({
email,
})
)
return response.data
}
}
24 changes: 11 additions & 13 deletions src/components/Debug/EnvStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
FORUM_URL,
GOVERNANCE_API,
LOCAL_ENV_VAR,
NEWSLETTER_URL,
OPEN_CALL_FOR_DELEGATES_LINK,
PROD_ENV_VAR,
SEGMENT_KEY,
Expand All @@ -26,18 +25,17 @@ interface Props {
}

const CONSTANTS: Record<string, any> = {
DOCS_URL: DOCS_URL,
FORUM_URL: FORUM_URL,
GOVERNANCE_API: GOVERNANCE_API,
DAO_DISCORD_URL: DAO_DISCORD_URL,
NEWSLETTER_URL: NEWSLETTER_URL,
OPEN_CALL_FOR_DELEGATES_LINK: OPEN_CALL_FOR_DELEGATES_LINK,
CANDIDATE_ADDRESSES: CANDIDATE_ADDRESSES,
DAO_ROLLBAR_TOKEN: DAO_ROLLBAR_TOKEN,
SEGMENT_KEY: SEGMENT_KEY,
LOCAL_ENV_VAR: LOCAL_ENV_VAR,
TEST_ENV_VAR: TEST_ENV_VAR,
PROD_ENV_VAR: PROD_ENV_VAR,
DOCS_URL,
FORUM_URL,
GOVERNANCE_API,
DAO_DISCORD_URL,
OPEN_CALL_FOR_DELEGATES_LINK,
CANDIDATE_ADDRESSES,
DAO_ROLLBAR_TOKEN,
SEGMENT_KEY,
LOCAL_ENV_VAR,
TEST_ENV_VAR,
PROD_ENV_VAR,
}

export default function EnvStatus({ className }: Props) {
Expand Down
11 changes: 7 additions & 4 deletions src/components/Home/BottomBanner/Action.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import React from 'react'

import Link from '../../Common/Typography/Link'

import './Action.css'

export interface ActionProps {
icon: JSX.Element
title: string
description: string
url: string
url?: string
onClick?: () => void
}

function Action({ icon, title, description, url }: ActionProps) {
function Action({ icon, title, description, url, onClick }: ActionProps) {
return (
<a href={url} target="_blank" rel="noreferrer" className="Action">
<Link href={url} onClick={onClick} target="_blank" rel="noreferrer" className="Action">
<div className="Action__Icon">{icon}</div>
<div>
<div className="Action__Title">{title}</div>
<div className="Action__Description">{description}</div>
</div>
</a>
</Link>
)
}

Expand Down
66 changes: 42 additions & 24 deletions src/components/Home/BottomBanner/BottomBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
import React from 'react'
import React, { useMemo } from 'react'

import { DAO_DISCORD_URL, FORUM_URL, NEWSLETTER_URL } from '../../../constants'
import { DAO_DISCORD_URL, FORUM_URL } from '../../../constants'
import { ProposalStatus, ProposalType } from '../../../entities/Proposal/types'
import useAbbreviatedFormatter from '../../../hooks/useAbbreviatedFormatter'
import useFormatMessage from '../../../hooks/useFormatMessage'
import useNewsletterSubscription from '../../../hooks/useNewsletterSubscription'
import useProposals from '../../../hooks/useProposals'
import useTransparency from '../../../hooks/useTransparency'
import Discord from '../../Icon/Discord'
import EmailFilled from '../../Icon/EmailFilled'
import Megaphone from '../../Icon/Megaphone'
import { NewsletterSubscriptionModal } from '../../Modal/NewsletterSubscriptionModal/NewsletterSubscriptionModal'

import Action, { ActionProps } from './Action'
import './BottomBanner.css'
import Stat from './Stat'

const ACTIONS: Record<string, ActionProps> = {
discord: {
icon: <Discord className="Discord__Icon" />,
title: 'page.home.bottom_banner.discord_title',
description: 'page.home.bottom_banner.discord_description',
url: DAO_DISCORD_URL,
},
forum: {
icon: <Megaphone />,
title: 'page.home.bottom_banner.forum_title',
description: 'page.home.bottom_banner.forum_description',
url: FORUM_URL,
},
newsletter: {
icon: <EmailFilled />,
title: 'page.home.bottom_banner.newsletter_title',
description: 'page.home.bottom_banner.newsletter_description',
url: NEWSLETTER_URL,
},
}

function BottomBanner() {
const t = useFormatMessage()
const { data } = useTransparency()
Expand All @@ -47,6 +28,36 @@ function BottomBanner() {
itemsPerPage: 1,
})

const { isSubscriptionModalOpen, setIsSubscriptionModalOpen, onSubscriptionSuccess, subscribed, onClose } =
useNewsletterSubscription()

const actions: Record<string, ActionProps> = useMemo(
() => ({
discord: {
icon: <Discord className="Discord__Icon" />,
title: 'page.home.bottom_banner.discord_title',
description: 'page.home.bottom_banner.discord_description',
url: DAO_DISCORD_URL,
onClick: undefined,
},
forum: {
icon: <Megaphone />,
title: 'page.home.bottom_banner.forum_title',
description: 'page.home.bottom_banner.forum_description',
url: FORUM_URL,
onClick: undefined,
},
newsletter: {
icon: <EmailFilled />,
title: 'page.home.bottom_banner.newsletter_title',
description: 'page.home.bottom_banner.newsletter_description',
url: undefined,
onClick: () => setIsSubscriptionModalOpen(true),
},
}),
[setIsSubscriptionModalOpen]
)

return (
<div className="BottomBanner">
<div className="BottomBanner__Info">
Expand All @@ -62,16 +73,23 @@ function BottomBanner() {
</div>
</div>
<div className="BottomBanner__Actions">
{Object.entries(ACTIONS).map(([key, props]) => (
{Object.entries(actions).map(([key, props]) => (
<Action
key={'BottomBanner__Action' + key}
icon={props.icon}
url={props.url}
title={t(props.title)}
description={t(props.description)}
onClick={props.onClick}
/>
))}
</div>
<NewsletterSubscriptionModal
open={isSubscriptionModalOpen}
onSubscriptionSuccess={onSubscriptionSuccess}
subscribed={subscribed}
onClose={onClose}
/>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Header } from 'decentraland-ui/dist/components/Header/Header'
import { Modal, ModalProps } from 'decentraland-ui/dist/components/Modal/Modal'
import isEmail from 'validator/lib/isEmail'

import { Decentraland } from '../../../clients/Decentraland'
import { Governance } from '../../../clients/Governance'
import useAsyncTask from '../../../hooks/useAsyncTask'
import useFormatMessage from '../../../hooks/useFormatMessage'
import { ANONYMOUS_USR_SUBSCRIPTION, NEWSLETTER_SUBSCRIPTION_KEY } from '../../Banner/Subscription/SubscriptionBanner'
Expand All @@ -26,33 +26,29 @@ type NewsletterSubscriptionResult = {
details: string | null
}

export type NewsletterSubscriptionModalProps = Omit<ModalProps, 'children'> & {
type Props = Omit<ModalProps, 'children'> & {
onSubscriptionSuccess?: () => void
subscribed: boolean
}

async function subscribe(email: string) {
try {
await Decentraland.get().subscribe(email!)
await Governance.get().subscribeToNewsletter(email)
return {
email: email,
email,
error: false,
details: null,
}
} catch (err) {
return {
email: email,
email,
error: true,
details: (err as any).body.detail,
}
}
}

export function NewsletterSubscriptionModal({
onSubscriptionSuccess,
subscribed,
...props
}: NewsletterSubscriptionModalProps) {
export function NewsletterSubscriptionModal({ onSubscriptionSuccess, subscribed, ...props }: Props) {
const [account] = useAuthContext()
const t = useFormatMessage()
const [state, setState] = useState<{ isValid: boolean; message: string; email: string }>({
Expand Down
1 change: 0 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const DOCS_URL = 'https://docs.decentraland.org/decentraland/what-is-the-
export const FORUM_URL = process.env.GATSBY_DISCOURSE_API || env('GATSBY_DISCOURSE_API') || ''
export const GOVERNANCE_API = process.env.GATSBY_GOVERNANCE_API || env('GATSBY_GOVERNANCE_API') || ''
export const DAO_DISCORD_URL = 'https://dcl.gg/dao-discord'
export const NEWSLETTER_URL = 'https://mailchi.mp/decentraland.org/decentraland-dao-weekly-newsletter'
export const OPEN_CALL_FOR_DELEGATES_LINK = 'https://forum.decentraland.org/t/open-call-for-delegates-apply-now/5840'
export const CANDIDATE_ADDRESSES = Candidates.map((delegate) => delegate.address)
export const DAO_ROLLBAR_TOKEN = process.env.DAO_ROLLBAR_TOKEN
Expand Down
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import coauthor from './back/routes/coauthor'
import committee from './back/routes/committee'
import common from './back/routes/common'
import debug from './back/routes/debug'
import newsletter from './back/routes/newsletter'
import project from './back/routes/project'
import proposal from './back/routes/proposal'
import sitemap from './back/routes/sitemap'
Expand Down Expand Up @@ -75,6 +76,7 @@ app.use('/api', [
snapshot,
vestings,
project,
newsletter,
handle(async () => {
throw new RequestError('NotFound', RequestError.NotFound)
}),
Expand Down
1 change: 1 addition & 0 deletions src/utils/errorCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export enum ErrorCategory {
Vesting = 'Vesting',
Voting = 'Voting',
Discord = 'Discord',
Newsletter = 'Newsletter',
}

0 comments on commit 53678e4

Please sign in to comment.