From 51085ef5dbed6be99b3dbcb042b96fdad9c11637 Mon Sep 17 00:00:00 2001 From: Tair Asim Date: Fri, 8 Sep 2023 12:53:32 +0200 Subject: [PATCH] feat: notification preferences (#1628) feat: notification preferences + onboarding opt-in to email updates fixes FTD-2101 --- @3rdweb-sdk/react/hooks/useApi.ts | 44 +++++++++ components/onboarding/index.tsx | 2 +- components/settings/Account/AccountForm.tsx | 88 +++++++++++------ components/settings/Account/Notifications.tsx | 99 +++++++++++++++++++ core-ui/sidebar/settings.tsx | 7 +- page-id.ts | 3 + pages/api/email-signup.ts | 26 ++--- pages/dashboard/settings/billing.tsx | 21 ++-- pages/dashboard/settings/notifications.tsx | 52 ++++++++++ pages/dashboard/settings/usage.tsx | 13 +-- 10 files changed, 289 insertions(+), 66 deletions(-) create mode 100644 components/settings/Account/Notifications.tsx create mode 100644 pages/dashboard/settings/notifications.tsx diff --git a/@3rdweb-sdk/react/hooks/useApi.ts b/@3rdweb-sdk/react/hooks/useApi.ts index d6fdc0ef1ea..4325cc89af5 100644 --- a/@3rdweb-sdk/react/hooks/useApi.ts +++ b/@3rdweb-sdk/react/hooks/useApi.ts @@ -27,11 +27,21 @@ export type Account = { currentBillingPeriodStartsAt: string; currentBillingPeriodEndsAt: string; onboardedAt?: string; + notificationPreferences?: { + billing: "email" | "none"; + updates: "email" | "none"; + }; }; export interface UpdateAccountInput { name?: string; email?: string; + subscribeToUpdates?: boolean; +} + +export interface UpdateAccountNotificationsInput { + billing: "email" | "none"; + updates: "email" | "none"; } export type ApiKeyService = { @@ -201,6 +211,40 @@ export function useUpdateAccount() { ); } +export function useUpdateNotifications() { + const { user } = useUser(); + const queryClient = useQueryClient(); + + return useMutationWithInvalidate( + async (input: UpdateAccountNotificationsInput) => { + invariant(user, "No user is logged in"); + + const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, { + method: "PUT", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ preferences: input }), + }); + const json = await res.json(); + + if (json.error) { + throw new Error(json.error?.message || json.error); + } + + return json.data; + }, + { + onSuccess: () => { + return queryClient.invalidateQueries( + accountKeys.me(user?.address as string), + ); + }, + }, + ); +} + export function useCreateAccountPlan() { const { user } = useUser(); const queryClient = useQueryClient(); diff --git a/components/onboarding/index.tsx b/components/onboarding/index.tsx index be40021784a..2f7fe71a2af 100644 --- a/components/onboarding/index.tsx +++ b/components/onboarding/index.tsx @@ -38,7 +38,7 @@ export const Onboarding: React.FC = () => { = ({ buttonText = "Save", horizontal = false, previewEnabled = false, - hideBillingButton = false, + showBillingButton = true, + showSubscription = false, disableUnchanged = false, padded = true, optional = false, }) => { + const [isSubscribing, setIsSubscribing] = useState( + showSubscription && !!account.email?.length, + ); const trackEvent = useTrack(); const bg = useColorModeValue("backgroundCardHighlight", "transparent"); @@ -80,14 +87,23 @@ export const AccountForm: React.FC = ({ onSave(); } + const formData = { + ...values, + ...(showSubscription + ? { + subscribeToUpdates: isSubscribing, + } + : {}), + }; + trackEvent({ category: "account", action: "update", label: "attempt", - data: values, + data: formData, }); - updateMutation.mutate(values, { + updateMutation.mutate(formData, { onSuccess: (data) => { if (!optional) { onSuccess(); @@ -105,17 +121,17 @@ export const AccountForm: React.FC = ({ data, }); }, - onError: (err) => { + onError: (error) => { // don't show errors when form is optional if (!optional) { - onError(err); + onError(error); } trackEvent({ category: "account", action: "update", label: "error", - error: err, + error, }); }, }); @@ -206,32 +222,44 @@ export const AccountForm: React.FC = ({ )} - - - - {!hideBillingButton && } - - {!previewEnabled && ( - - )} - - + isChecked={isSubscribing} + onChange={(e) => setIsSubscribing(e.target.checked)} + /> + + )} + + + + {!showBillingButton && } + + {!previewEnabled && ( + + )} + ); diff --git a/components/settings/Account/Notifications.tsx b/components/settings/Account/Notifications.tsx new file mode 100644 index 00000000000..32676455a27 --- /dev/null +++ b/components/settings/Account/Notifications.tsx @@ -0,0 +1,99 @@ +import { + Account, + useUpdateNotifications, +} from "@3rdweb-sdk/react/hooks/useApi"; +import { Divider, Flex, HStack, Switch, VStack } from "@chakra-ui/react"; +import { useTrack } from "hooks/analytics/useTrack"; +import { useTxNotifications } from "hooks/useTxNotifications"; +import { useState } from "react"; +import { Heading, Text } from "tw-components"; + +interface NotificationsProps { + account: Account; +} + +export const Notifications: React.FC = ({ account }) => { + const [preferences, setPreferences] = useState({ + billing: account.notificationPreferences?.billing || "email", + updates: account.notificationPreferences?.updates || "none", + }); + + const trackEvent = useTrack(); + const updateMutation = useUpdateNotifications(); + + const { onSuccess, onError } = useTxNotifications( + "Notification settings saved.", + "Failed to save your notification settings.", + ); + + const handleChange = (name: "billing" | "updates", checked: boolean) => { + const newPreferences = { + ...preferences, + [name]: checked ? "email" : "none", + }; + setPreferences(newPreferences); + + updateMutation.mutate(newPreferences, { + onSuccess: (data) => { + onSuccess(); + + trackEvent({ + category: "notifications", + action: "update", + label: "success", + data, + }); + }, + onError: (error) => { + onError(error); + + trackEvent({ + category: "notifications", + action: "update", + label: "error", + error, + }); + }, + }); + }; + + return ( + + + + + + Reminders + Approaching and exceeding usage limits. + + handleChange("billing", e.target.checked)} + /> + + + + + + + Product Updates + New features and key product updates. + + handleChange("updates", e.target.checked)} + /> + + + ); +}; diff --git a/core-ui/sidebar/settings.tsx b/core-ui/sidebar/settings.tsx index f765c4753ac..76dffdac943 100644 --- a/core-ui/sidebar/settings.tsx +++ b/core-ui/sidebar/settings.tsx @@ -2,7 +2,7 @@ import { SidebarNav } from "./nav"; import { Route } from "./types"; type SettingsSidebarProps = { - activePage: "apiKeys" | "devices" | "usage" | "billing"; + activePage: "apiKeys" | "devices" | "usage" | "billing" | "notifications"; }; const links: Route[] = [ @@ -14,6 +14,11 @@ const links: Route[] = [ name: "billing", }, { path: "/dashboard/settings/usage", title: "Usage", name: "usage" }, + { + path: "/dashboard/settings/notifications", + title: "Notifications", + name: "notifications", + }, ]; export const SettingsSidebar: React.FC = ({ diff --git a/page-id.ts b/page-id.ts index bf036aaea46..56c5b31c9f9 100644 --- a/page-id.ts +++ b/page-id.ts @@ -131,6 +131,9 @@ export enum PageId { // thirdweb.com/dashboard/settings/usage SettingsUsage = "settings-usage", + // thirdweb.com/dashboard/settings/notifications + SettingsNotifications = "settings-notifications", + // --------------------------------------------------------------------------- // solutions pages // --------------------------------------------------------------------------- diff --git a/pages/api/email-signup.ts b/pages/api/email-signup.ts index 97baedc9f07..33a63e24585 100644 --- a/pages/api/email-signup.ts +++ b/pages/api/email-signup.ts @@ -20,19 +20,21 @@ const handler = async (req: NextRequest) => { const { email, send_welcome_email = false } = requestBody; invariant(process.env.BEEHIIV_API_KEY, "missing BEEHIIV_API_KEY"); - const response = await fetch("https://api.beehiiv.com/v1/subscribers", { - headers: { - "Content-Type": "application/json", - "X-ApiKey": process.env.BEEHIIV_API_KEY, + const response = await fetch( + "https://api.beehiiv.com/v2/publications/9f54090a-6d14-406b-adfd-dbb30574f664/subscriptions", + { + headers: { + "Content-Type": "application/json", + "X-ApiKey": process.env.BEEHIIV_API_KEY, + }, + method: "POST", + body: JSON.stringify({ + email, + send_welcome_email, + utm_source: "thirdweb.com", + }), }, - method: "POST", - body: JSON.stringify({ - email, - publication_id: "9f54090a-6d14-406b-adfd-dbb30574f664", - send_welcome_email, - utm_source: "thirdweb.com", - }), - }); + ); return NextResponse.json( { status: response.statusText }, diff --git a/pages/dashboard/settings/billing.tsx b/pages/dashboard/settings/billing.tsx index 69190877bb1..fa6747c9721 100644 --- a/pages/dashboard/settings/billing.tsx +++ b/pages/dashboard/settings/billing.tsx @@ -13,6 +13,7 @@ import { StepsCard } from "components/dashboard/StepsCard"; import { useEffect, useMemo, useState } from "react"; import { FiCheckCircle, FiAlertCircle } from "react-icons/fi"; import { BillingPlan } from "components/settings/Account/BillingPlan"; +import { Notifications } from "components/settings/Account/Notifications"; const SettingsBillingPage: ThirdwebNextPage = () => { const address = useAddress(); @@ -66,7 +67,6 @@ const SettingsBillingPage: ThirdwebNextPage = () => { account={account} previewEnabled={stepsCompleted.account} horizontal - hideBillingButton onSave={() => setStepsCompleted({ account: true, payment: false })} /> ), @@ -137,16 +137,9 @@ const SettingsBillingPage: ThirdwebNextPage = () => { ) : ( <> - - - Account & Billing - - + + Account & Billing + @@ -171,7 +164,11 @@ const SettingsBillingPage: ThirdwebNextPage = () => { - + )} diff --git a/pages/dashboard/settings/notifications.tsx b/pages/dashboard/settings/notifications.tsx new file mode 100644 index 00000000000..76497049cfc --- /dev/null +++ b/pages/dashboard/settings/notifications.tsx @@ -0,0 +1,52 @@ +import { AppLayout } from "components/app-layouts/app"; +import { Flex } from "@chakra-ui/react"; +import { useAddress } from "@thirdweb-dev/react"; +import { SettingsSidebar } from "core-ui/sidebar/settings"; +import { PageId } from "page-id"; +import { ThirdwebNextPage } from "utils/types"; +import { ConnectWalletPrompt } from "components/settings/ConnectWalletPrompt"; +import { Heading, Text } from "tw-components"; +import { useAccount } from "@3rdweb-sdk/react/hooks/useApi"; +import { Notifications } from "components/settings/Account/Notifications"; + +const SettingsNotificationsPage: ThirdwebNextPage = () => { + const address = useAddress(); + const meQuery = useAccount(); + + if (!address) { + return ; + } + + if (meQuery.isLoading || !meQuery.data) { + return null; + } + + const account = meQuery.data; + + return ( + + + + Notification Settings + + + Configure your email notification preferences. + + + + + + ); +}; + +SettingsNotificationsPage.getLayout = (page, props) => ( + + + + {page} + +); + +SettingsNotificationsPage.pageId = PageId.SettingsNotifications; + +export default SettingsNotificationsPage; diff --git a/pages/dashboard/settings/usage.tsx b/pages/dashboard/settings/usage.tsx index c69731ced50..804b99e122c 100644 --- a/pages/dashboard/settings/usage.tsx +++ b/pages/dashboard/settings/usage.tsx @@ -29,16 +29,9 @@ const SettingsUsagePage: ThirdwebNextPage = () => { return ( - - - Usage - - + + Usage +