Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate beehiiv newsletter subscription #1208

Merged
merged 1 commit into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,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=
andyesp marked this conversation as resolved.
Show resolved Hide resolved
BEEHIIV_PUBLICATION_ID=
andyesp marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -593,4 +593,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',
}
Loading