From 93a7331ddf7cfabf38dd9fbf3358a96634610258 Mon Sep 17 00:00:00 2001 From: brennerthomas Date: Tue, 3 Dec 2024 21:48:55 +0100 Subject: [PATCH] Web: Solving newsletter subscription issues (#939) --- functions/src/webhooks/stripe/index.ts | 12 ++---- shared/emails/transactional/welcome-2-fr.html | 10 ++--- shared/emails/transactional/welcome-3-fr.html | 6 +-- shared/emails/transactional/welcome-4-fr.html | 5 ++- .../sendgrid/SendgridSubscriptionClient.ts | 5 +-- website/.env.development | 7 +++- .../newsletter/subscription-info-form.tsx | 34 +++++++++++------ .../newsletter/subscription/public/route.ts | 38 +++++++++++++++---- .../app/api/newsletter/subscription/route.ts | 14 +++---- 9 files changed, 80 insertions(+), 51 deletions(-) diff --git a/functions/src/webhooks/stripe/index.ts b/functions/src/webhooks/stripe/index.ts index 0ee7e1ce4..3ec3f21c3 100644 --- a/functions/src/webhooks/stripe/index.ts +++ b/functions/src/webhooks/stripe/index.ts @@ -3,11 +3,7 @@ import { logger } from 'firebase-functions'; import { onRequest } from 'firebase-functions/v2/https'; import Stripe from 'stripe'; import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { - NEWSLETTER_LIST_ID, - NEWSLETTER_SUPPRESSION_LIST_ID, - SendgridSubscriptionClient, -} from '../../../../shared/src/sendgrid/SendgridSubscriptionClient'; +import { SendgridSubscriptionClient } from '../../../../shared/src/sendgrid/SendgridSubscriptionClient'; import { StripeEventHandler } from '../../../../shared/src/stripe/StripeEventHandler'; import { Contribution } from '../../../../shared/src/types/contribution'; import { CountryCode } from '../../../../shared/src/types/country'; @@ -17,12 +13,12 @@ import { STRIPE_API_READ_KEY, STRIPE_WEBHOOK_SECRET } from '../../config'; const addContributorToNewsletter = async (contributionRef: DocumentReference) => { const newsletterClient = new SendgridSubscriptionClient({ apiKey: process.env.SENDGRID_API_KEY!, - listId: NEWSLETTER_LIST_ID, - suppressionListId: NEWSLETTER_SUPPRESSION_LIST_ID, + listId: process.env.SENDGRID_LIST_ID!, + suppressionListId: parseInt(process.env.SENDGRID_SUPPRESSION_LIST_ID!), }); const user = (await contributionRef.parent.parent?.get()) as DocumentSnapshot; logger.info( - `Adding contributor ${user.id} (${user.get('email')}) to Sendgrid newsletter list (${NEWSLETTER_LIST_ID}).`, + `Adding contributor ${user.id} (${user.get('email')}) to Sendgrid newsletter list (${process.env.SENDGRID_LIST_ID}).`, ); await newsletterClient.upsertSubscription({ firstname: user.get('personal.name'), diff --git a/shared/emails/transactional/welcome-2-fr.html b/shared/emails/transactional/welcome-2-fr.html index 45b036c66..14d652002 100644 --- a/shared/emails/transactional/welcome-2-fr.html +++ b/shared/emails/transactional/welcome-2-fr.html @@ -242,7 +242,7 @@ class="feature" style="margin: 0 0 12px; font-size: 50px; font-weight: 500; line-height: 60px; color: #191a19" > - Agir contre la pauvreté. Qui le fera, sinon nous? + Agir contre la pauvreté. À nous de jouer! @@ -286,7 +286,7 @@ " > Faut-il aider les personnes qui vivent dans la pauvreté? - Voici quelques raisons personnelles de le faire: + Personnellement j'ai quelques bonnes raisons de le faire:

@@ -320,7 +320,7 @@ class="feature" style="color: #191a19; font-family: 'Unica77', Helvetica, Arial, sans-serif; font-weight: 400" > - Avoir conscience de la pauvreté globale n’aide personne à y échapper.
  • @@ -369,8 +369,8 @@ Ne laissons pas les gens sous la pluie + >Ne laissons personne sous la pluie + si nous pouvons faire autrement.

    diff --git a/shared/emails/transactional/welcome-3-fr.html b/shared/emails/transactional/welcome-3-fr.html index 967243b61..6ce040476 100644 --- a/shared/emails/transactional/welcome-3-fr.html +++ b/shared/emails/transactional/welcome-3-fr.html @@ -420,13 +420,13 @@ bénévoles internationaux - entier qui communiquent, codent et lèvent des fonds d’un peu + qui communiquent, codent et lèvent des fonds. partout dans le monde.Nous sommes tous convaincus - Nous sommes tous convaincus qu’aider des personnes dans le besoin est une responsabilité sociale. + qu’aider des personnes dans le besoin est une responsabilité sociale.

    diff --git a/shared/emails/transactional/welcome-4-fr.html b/shared/emails/transactional/welcome-4-fr.html index c5951f109..3322b4270 100644 --- a/shared/emails/transactional/welcome-4-fr.html +++ b/shared/emails/transactional/welcome-4-fr.html @@ -242,7 +242,8 @@ class="feature" style="margin: 0 0 12px; font-size: 50px; font-weight: 500; line-height: 60px; color: #191a19" > - Si ce n’est pas maintenant, alors quand? + Le moment d'agir, + c'est maintenant! @@ -349,7 +350,7 @@ color: #191a19; " > - Nous garantissons que votre don parviendra + Nous garantissons que ton don parviendra aux personnes qui en ont le plus besoin. Afin de pouvoir nous consacrer entièrement à notre mission en minimisant les coûts liés à la collecte de fonds, nous t’incitons à donner 1% de ton salaire de manière récurrente. Tu peux interrompre tes versements à tout diff --git a/shared/src/sendgrid/SendgridSubscriptionClient.ts b/shared/src/sendgrid/SendgridSubscriptionClient.ts index 565d7f4a8..7d09cde39 100644 --- a/shared/src/sendgrid/SendgridSubscriptionClient.ts +++ b/shared/src/sendgrid/SendgridSubscriptionClient.ts @@ -3,14 +3,11 @@ import { SendgridContactType } from '@socialincome/shared/src/sendgrid/types'; import { CountryCode } from '../types/country'; import { Suppression } from './types'; -export const NEWSLETTER_LIST_ID = '2896ee4d-d1e0-4a4a-8565-7e592c377e36'; -export const NEWSLETTER_SUPPRESSION_LIST_ID = 45634; - export type NewsletterSubscriptionData = { firstname?: string; lastname?: string; email: string; - language: 'de' | 'en'; + language: 'de' | 'en' | 'fr' | 'it'; country?: CountryCode; status?: 'subscribed' | 'unsubscribed'; isContributor?: boolean; diff --git a/website/.env.development b/website/.env.development index b26b9b13d..6833321ca 100644 --- a/website/.env.development +++ b/website/.env.development @@ -13,4 +13,9 @@ NEXT_PUBLIC_FIREBASE_FIRESTORE_EMULATOR_PORT="8080" NEXT_PUBLIC_FIREBASE_FUNCTIONS_EMULATOR_HOST="localhost" NEXT_PUBLIC_FIREBASE_FUNCTIONS_EMULATOR_PORT="5001" -BASE_URL="http://localhost:3001" \ No newline at end of file +BASE_URL="http://localhost:3001" + +SENDGRID_LIST_ID="2896ee4d-d1e0-4a4a-8565-7e592c377e36" +SENDGRID_SUPPRESSION_LIST_ID=45634 +SENDGRID_API_KEY="SG.Q****" + diff --git a/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx b/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx index f8c8ff37a..f611521da 100644 --- a/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx +++ b/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx @@ -1,10 +1,12 @@ 'use client'; import { DefaultParams } from '@/app/[lang]/[region]'; +import { SpinnerIcon } from '@/components/logos/spinner-icon'; import { useApi } from '@/hooks/useApi'; import { zodResolver } from '@hookform/resolvers/zod'; import { NewsletterSubscriptionData } from '@socialincome/shared/src/sendgrid/SendgridSubscriptionClient'; import { Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from '@socialincome/ui'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; import * as z from 'zod'; @@ -21,6 +23,7 @@ type PersonalInfoFormProps = { export function SubscriptionInfoForm({ lang, translations }: PersonalInfoFormProps) { const api = useApi(); + const [isSubmitting, setIsSubmitting] = useState(false); const formSchema = z.object({ firstname: z.string(), @@ -37,20 +40,27 @@ export function SubscriptionInfoForm({ lang, translations }: PersonalInfoFormPro }); const onSubmit = async (values: FormSchema) => { + setIsSubmitting(true); const data: NewsletterSubscriptionData = { firstname: values.firstname, email: values.email, - language: lang === 'de' ? 'de' : 'en', + language: lang === 'de' ? 'de' : lang === 'fr' ? 'fr' : lang === 'it' ? 'it' : 'en', status: 'subscribed', }; - api.post('/api/newsletter/subscription/public', data).then((response) => { + try { + const response = await api.post('/api/newsletter/subscription/public', data); if (response.status === 200) { toast.success(translations.toastMessage); + form.reset(); } else { toast.error(translations.toastErrorMessage + '(' + response.statusText + ')'); } - }); + } catch (error) { + toast.error(translations.toastErrorMessage); + } finally { + setIsSubmitting(false); + } }; return ( @@ -82,15 +92,15 @@ export function SubscriptionInfoForm({ lang, translations }: PersonalInfoFormPro )} /> - + {isSubmitting ? ( +
    + +
    + ) : ( + + )} ); diff --git a/website/src/app/api/newsletter/subscription/public/route.ts b/website/src/app/api/newsletter/subscription/public/route.ts index 752239e44..c136752b4 100644 --- a/website/src/app/api/newsletter/subscription/public/route.ts +++ b/website/src/app/api/newsletter/subscription/public/route.ts @@ -1,20 +1,44 @@ import { - NEWSLETTER_LIST_ID, - NEWSLETTER_SUPPRESSION_LIST_ID, NewsletterSubscriptionData, SendgridSubscriptionClient, } from '@socialincome/shared/src/sendgrid/SendgridSubscriptionClient'; export type CreateNewsletterSubscription = Omit; -type CreateNewsletterSubscriptionReqeust = { json(): Promise } & Request; +type CreateNewsletterSubscriptionRequest = { json(): Promise } & Request; -export async function POST(request: CreateNewsletterSubscriptionReqeust) { +type SendgridClientProps = { + SENDGRID_API_KEY: string; + SENDGRID_LIST_ID: string; + SENDGRID_SUPPRESSION_LIST_ID: number; +}; + +const validateSendgridClientProps = (): SendgridClientProps => { + const suppressionListId = parseInt(process.env.SENDGRID_SUPPRESSION_LIST_ID!); + if (isNaN(suppressionListId)) { + throw new Error('SENDGRID_SUPPRESSION_LIST_ID must be a valid number'); + } + + const sendgridClientProps: SendgridClientProps = { + SENDGRID_API_KEY: process.env.SENDGRID_API_KEY!, + SENDGRID_LIST_ID: process.env.SENDGRID_LIST_ID!, + SENDGRID_SUPPRESSION_LIST_ID: suppressionListId, + }; + + Object.entries(sendgridClientProps).forEach(([key, value]) => { + if (!value) throw new Error(`Missing required environment variable: ${key}`); + }); + return sendgridClientProps; +}; + +export async function POST(request: CreateNewsletterSubscriptionRequest) { const data = await request.json(); + const sendgridClientProps: SendgridClientProps = validateSendgridClientProps(); const sendgrid = new SendgridSubscriptionClient({ - apiKey: process.env.SENDGRID_API_KEY!, - listId: NEWSLETTER_LIST_ID, - suppressionListId: NEWSLETTER_SUPPRESSION_LIST_ID, + apiKey: sendgridClientProps.SENDGRID_API_KEY!, + listId: sendgridClientProps.SENDGRID_LIST_ID!, + suppressionListId: sendgridClientProps.SENDGRID_SUPPRESSION_LIST_ID, }); + try { await sendgrid.upsertSubscription({ ...data, status: 'subscribed' }); return new Response(null, { status: 200 }); diff --git a/website/src/app/api/newsletter/subscription/route.ts b/website/src/app/api/newsletter/subscription/route.ts index 5bcc709c1..59a70ba96 100644 --- a/website/src/app/api/newsletter/subscription/route.ts +++ b/website/src/app/api/newsletter/subscription/route.ts @@ -1,9 +1,5 @@ import { authorizeRequest, handleApiError } from '@/app/api/auth'; -import { - NEWSLETTER_LIST_ID, - NEWSLETTER_SUPPRESSION_LIST_ID, - SendgridSubscriptionClient, -} from '@socialincome/shared/src/sendgrid/SendgridSubscriptionClient'; +import { SendgridSubscriptionClient } from '@socialincome/shared/src/sendgrid/SendgridSubscriptionClient'; import { NextResponse } from 'next/server'; /** @@ -14,8 +10,8 @@ export async function GET(request: Request) { const userDoc = await authorizeRequest(request); const sendgrid = new SendgridSubscriptionClient({ apiKey: process.env.SENDGRID_API_KEY!, - listId: NEWSLETTER_LIST_ID, - suppressionListId: NEWSLETTER_SUPPRESSION_LIST_ID, + listId: process.env.SENDGRID_LIST_ID!, + suppressionListId: parseInt(process.env.SENDGRID_SUPPRESSION_LIST_ID!), }); const subscriber = await sendgrid.getContact(userDoc.get('email')); return NextResponse.json(subscriber); @@ -37,8 +33,8 @@ export async function POST(request: NewsletterSubscriptionUpdateRequest) { const data = await request.json(); const sendgrid = new SendgridSubscriptionClient({ apiKey: process.env.SENDGRID_API_KEY!, - listId: NEWSLETTER_LIST_ID, - suppressionListId: NEWSLETTER_SUPPRESSION_LIST_ID, + listId: process.env.SENDGRID_LIST_ID!, + suppressionListId: parseInt(process.env.SENDGRID_SUPPRESSION_LIST_ID!), }); await sendgrid.upsertSubscription({ firstname: userDoc.get('personal.name'),