Skip to content

Commit

Permalink
feat: notification preferences (#1628)
Browse files Browse the repository at this point in the history
feat: notification preferences + onboarding opt-in to email updates

fixes FTD-2101
  • Loading branch information
assimovt authored Sep 8, 2023
1 parent a2393b5 commit 51085ef
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 66 deletions.
44 changes: 44 additions & 0 deletions @3rdweb-sdk/react/hooks/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion components/onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const Onboarding: React.FC = () => {

<AccountForm
account={meQuery.data as Account}
hideBillingButton
showSubscription
buttonText="Continue to Dashboard"
trackingCategory="onboarding"
padded={false}
Expand Down
88 changes: 58 additions & 30 deletions components/settings/Account/AccountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
VStack,
HStack,
useColorModeValue,
Switch,
} from "@chakra-ui/react";
import {
Button,
Expand All @@ -26,12 +27,14 @@ import {
import { Account, useUpdateAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { useTrack } from "hooks/analytics/useTrack";
import { ManageBillingButton } from "components/settings/Account/ManageBillingButton";
import { useState } from "react";

interface AccountFormProps {
account: Account;
horizontal?: boolean;
previewEnabled?: boolean;
hideBillingButton?: boolean;
showBillingButton?: boolean;
showSubscription?: boolean;
buttonProps?: ButtonProps;
buttonText?: string;
padded?: boolean;
Expand All @@ -48,11 +51,15 @@ export const AccountForm: React.FC<AccountFormProps> = ({
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");

Expand Down Expand Up @@ -80,14 +87,23 @@ export const AccountForm: React.FC<AccountFormProps> = ({
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();
Expand All @@ -105,17 +121,17 @@ export const AccountForm: React.FC<AccountFormProps> = ({
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,
});
},
});
Expand Down Expand Up @@ -206,32 +222,44 @@ export const AccountForm: React.FC<AccountFormProps> = ({
</FormErrorMessage>
)}
</FormControl>
</Flex>

<VStack w="full" gap={3}>
<HStack
justifyContent={!hideBillingButton ? "space-between" : "flex-end"}
w="full"
>
{!hideBillingButton && <ManageBillingButton account={account} />}

{!previewEnabled && (
<Button
{...buttonProps}
type="button"
onClick={handleSubmit}
colorScheme="blue"
{showSubscription && (
<HStack gap={2} justifyContent="center">
<Text>Subscribe to new features and key product updates</Text>
<Switch
isDisabled={
updateMutation.isLoading ||
(disableUnchanged && !form.formState.isDirty)
!form.getValues("email").length ||
!!form.getFieldState("email", form.formState).error
}
isLoading={updateMutation.isLoading}
>
{buttonText}
</Button>
)}
</HStack>
</VStack>
isChecked={isSubscribing}
onChange={(e) => setIsSubscribing(e.target.checked)}
/>
</HStack>
)}
</Flex>

<HStack
justifyContent={!showBillingButton ? "space-between" : "flex-end"}
w="full"
>
{!showBillingButton && <ManageBillingButton account={account} />}

{!previewEnabled && (
<Button
{...buttonProps}
type="button"
onClick={handleSubmit}
colorScheme="blue"
isDisabled={
updateMutation.isLoading ||
(disableUnchanged && !form.formState.isDirty)
}
isLoading={updateMutation.isLoading}
>
{buttonText}
</Button>
)}
</HStack>
</VStack>
</form>
);
Expand Down
99 changes: 99 additions & 0 deletions components/settings/Account/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -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<NotificationsProps> = ({ 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 (
<VStack alignItems="flex-start" maxW="xl" gap={6}>
<Divider />

<HStack
gap={8}
alignItems="flex-start"
justifyContent="space-between"
w="full"
>
<Flex flexDir="column" gap={1}>
<Heading size="label.lg">Reminders</Heading>
<Text>Approaching and exceeding usage limits.</Text>
</Flex>
<Switch
isChecked={preferences?.billing === "email"}
onChange={(e) => handleChange("billing", e.target.checked)}
/>
</HStack>

<Divider />

<HStack
gap={8}
alignItems="flex-start"
justifyContent="space-between"
w="full"
>
<Flex flexDir="column" gap={1}>
<Heading size="label.lg">Product Updates</Heading>
<Text>New features and key product updates.</Text>
</Flex>
<Switch
isChecked={preferences?.updates === "email"}
onChange={(e) => handleChange("updates", e.target.checked)}
/>
</HStack>
</VStack>
);
};
7 changes: 6 additions & 1 deletion core-ui/sidebar/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand All @@ -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<SettingsSidebarProps> = ({
Expand Down
3 changes: 3 additions & 0 deletions page-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
26 changes: 14 additions & 12 deletions pages/api/email-signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Loading

0 comments on commit 51085ef

Please sign in to comment.