diff --git a/common/notification.ts b/common/notification.ts index d91dc300..b75e3d4a 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -96,6 +96,7 @@ type notification_descriptions = { [key in notification_preference]: { simple: string detailed: string + necessary?: boolean } } export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = { @@ -208,8 +209,9 @@ export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = { detailed: 'Bonuses for unique predictors on your markets', }, your_contract_closed: { - simple: 'Your market has closed and you need to resolve it', - detailed: 'Your market has closed and you need to resolve it', + simple: 'Your market has closed and you need to resolve it (necessary)', + detailed: 'Your market has closed and you need to resolve it (necessary)', + necessary: true, }, all_comments_on_watched_markets: { simple: 'All new comments', @@ -235,6 +237,11 @@ export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = { simple: `Only on markets you're invested in`, detailed: `Answers on markets that you're watching and that you're invested in`, }, + opt_out_all: { + simple: 'Opt out of all notifications (excludes when your markets close)', + detailed: + 'Opt out of all notifications excluding your own market closure notifications', + }, } export type BettingStreakData = { diff --git a/common/user-notification-preferences.ts b/common/user-notification-preferences.ts index 3fc0fb2f..ba9ade9d 100644 --- a/common/user-notification-preferences.ts +++ b/common/user-notification-preferences.ts @@ -53,6 +53,9 @@ export type notification_preferences = { profit_loss_updates: notification_destination_types[] onboarding_flow: notification_destination_types[] thank_you_for_purchases: notification_destination_types[] + + opt_out_all: notification_destination_types[] + // When adding a new notification preference, use add-new-notification-preference.ts to existing users } export const getDefaultNotificationPreferences = ( @@ -65,7 +68,7 @@ export const getDefaultNotificationPreferences = ( const email = noEmails ? undefined : emailIf ? 'email' : undefined return filterDefined([browser, email]) as notification_destination_types[] } - return { + const defaults: notification_preferences = { // Watched Markets all_comments_on_watched_markets: constructPref(true, false), all_answers_on_watched_markets: constructPref(true, false), @@ -121,7 +124,10 @@ export const getDefaultNotificationPreferences = ( probability_updates_on_watched_markets: constructPref(true, false), thank_you_for_purchases: constructPref(false, false), onboarding_flow: constructPref(false, false), - } as notification_preferences + + opt_out_all: [], + } + return defaults } // Adding a new key:value here is optional, you can just use a key of notification_subscription_types @@ -184,10 +190,18 @@ export const getNotificationDestinationsForUser = ( ? notificationSettings[subscriptionType] : [] } + const optOutOfAllSettings = notificationSettings['opt_out_all'] + // Your market closure notifications are high priority, opt-out doesn't affect their delivery + const optedOutOfEmail = + optOutOfAllSettings.includes('email') && + subscriptionType !== 'your_contract_closed' + const optedOutOfBrowser = + optOutOfAllSettings.includes('browser') && + subscriptionType !== 'your_contract_closed' const unsubscribeEndpoint = getFunctionUrl('unsubscribe') return { - sendToEmail: destinations.includes('email'), - sendToBrowser: destinations.includes('browser'), + sendToEmail: destinations.includes('email') && !optedOutOfEmail, + sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser, unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`, urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`, } diff --git a/functions/src/scripts/add-new-notification-preference.ts b/functions/src/scripts/add-new-notification-preference.ts new file mode 100644 index 00000000..d7e7072b --- /dev/null +++ b/functions/src/scripts/add-new-notification-preference.ts @@ -0,0 +1,27 @@ +import * as admin from 'firebase-admin' + +import { initAdmin } from './script-init' +import { getAllPrivateUsers } from 'functions/src/utils' +initAdmin() + +const firestore = admin.firestore() + +async function main() { + const privateUsers = await getAllPrivateUsers() + await Promise.all( + privateUsers.map((privateUser) => { + if (!privateUser.id) return Promise.resolve() + return firestore + .collection('private-users') + .doc(privateUser.id) + .update({ + notificationPreferences: { + ...privateUser.notificationPreferences, + opt_out_all: [], + }, + }) + }) + ) +} + +if (require.main === module) main().then(() => process.exit()) diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts index 418282c7..57a6d183 100644 --- a/functions/src/unsubscribe.ts +++ b/functions/src/unsubscribe.ts @@ -4,6 +4,7 @@ import { getPrivateUser } from './utils' import { PrivateUser } from '../../common/user' import { NOTIFICATION_DESCRIPTIONS } from '../../common/notification' import { notification_preference } from '../../common/user-notification-preferences' +import { getFunctionUrl } from '../../common/api' export const unsubscribe: EndpointDefinition = { opts: { method: 'GET', minInstances: 1 }, @@ -20,6 +21,8 @@ export const unsubscribe: EndpointDefinition = { res.status(400).send('Invalid subscription type parameter.') return } + const optOutAllType: notification_preference = 'opt_out_all' + const wantsToOptOutAll = notificationSubscriptionType === optOutAllType const user = await getPrivateUser(id) @@ -37,17 +40,22 @@ export const unsubscribe: EndpointDefinition = { const update: Partial = { notificationPreferences: { ...user.notificationPreferences, - [notificationSubscriptionType]: previousDestinations.filter( - (destination) => destination !== 'email' - ), + [notificationSubscriptionType]: wantsToOptOutAll + ? previousDestinations.push('email') + : previousDestinations.filter( + (destination) => destination !== 'email' + ), }, } await firestore.collection('private-users').doc(id).update(update) + const unsubscribeEndpoint = getFunctionUrl('unsubscribe') - res.send( - ` - + const optOutAllUrl = `${unsubscribeEndpoint}?id=${id}&type=${optOutAllType}` + if (wantsToOptOutAll) { + res.send( + ` + @@ -163,19 +171,6 @@ export const unsubscribe: EndpointDefinition = { - - -
-

- Hello!

-
- - @@ -186,20 +181,9 @@ export const unsubscribe: EndpointDefinition = { data-testid="4XoHRGw1Y"> - ${email} has been unsubscribed from email notifications related to: + ${email} has opted out of receiving unnecessary email notifications -
-
- ${NOTIFICATION_DESCRIPTIONS[notificationSubscriptionType].detailed}. -

-
-
-
- Click - here - to manage the rest of your notification settings. - @@ -219,9 +203,193 @@ export const unsubscribe: EndpointDefinition = { +` + ) + } else { + res.send( + ` + + + + + Manifold Markets 7th Day Anniversary Gift! + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + +
+ + banner logo + +
+
+

+ Hello!

+
+
+
+

+ + ${email} has been unsubscribed from email notifications related to: + +
+
+ + ${NOTIFICATION_DESCRIPTIONS[notificationSubscriptionType].detailed}. +

+
+
+
+ Click + here + to unsubscribe from all unnecessary emails. + +
+
+ Click + here + to manage the rest of your notification settings. + +
+ +
+

+
+
+
+
+
+ ` - ) + ) + } }, } diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx index f0b9591e..1b5cac40 100644 --- a/web/components/notification-settings.tsx +++ b/web/components/notification-settings.tsx @@ -10,6 +10,7 @@ import { ChevronDownIcon, ChevronUpIcon, CurrencyDollarIcon, + ExclamationIcon, InboxInIcon, InformationCircleIcon, LightBulbIcon, @@ -63,6 +64,7 @@ export function NotificationSettings(props: { 'contract_from_followed_user', 'unique_bettors_on_your_contract', 'profit_loss_updates', + 'opt_out_all', // TODO: add these // biggest winner, here are the rest of your markets @@ -157,20 +159,56 @@ export function NotificationSettings(props: { ], } + const optOut: SectionData = { + label: 'Opt Out', + subscriptionTypes: ['opt_out_all'], + } + function NotificationSettingLine(props: { description: string subscriptionTypeKey: notification_preference destinations: notification_destination_types[] + optOutAll: notification_destination_types[] }) { - const { description, subscriptionTypeKey, destinations } = props + const { description, subscriptionTypeKey, destinations, optOutAll } = props const previousInAppValue = destinations.includes('browser') const previousEmailValue = destinations.includes('email') const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue) const [emailEnabled, setEmailEnabled] = useState(previousEmailValue) + const [error, setError] = useState('') const loading = 'Changing Notifications Settings' const success = 'Changed Notification Settings!' const highlight = navigateToSection === subscriptionTypeKey + const attemptToChangeSetting = ( + setting: 'browser' | 'email', + newValue: boolean + ) => { + const necessaryError = + 'This notification type is necessary. At least one destination must be enabled.' + const necessarySetting = + NOTIFICATION_DESCRIPTIONS[subscriptionTypeKey].necessary + if ( + necessarySetting && + setting === 'browser' && + !emailEnabled && + !newValue + ) { + setError(necessaryError) + return + } else if ( + necessarySetting && + setting === 'email' && + !inAppEnabled && + !newValue + ) { + setError(necessaryError) + return + } + + changeSetting(setting, newValue) + } + const changeSetting = (setting: 'browser' | 'email', newValue: boolean) => { toast .promise( @@ -212,18 +250,21 @@ export function NotificationSettings(props: { {!browserDisabled.includes(subscriptionTypeKey) && ( changeSetting('browser', newVal)} + onChange={(newVal) => attemptToChangeSetting('browser', newVal)} label={'Web'} + disabled={optOutAll.includes('browser')} /> )} {emailsEnabled.includes(subscriptionTypeKey) && ( changeSetting('email', newVal)} + onChange={(newVal) => attemptToChangeSetting('email', newVal)} label={'Email'} + disabled={optOutAll.includes('email')} /> )} + {error && {error}} ) @@ -283,6 +324,11 @@ export function NotificationSettings(props: { subType as notification_preference )} description={NOTIFICATION_DESCRIPTIONS[subType].simple} + optOutAll={ + subType === 'opt_out_all' || subType === 'your_contract_closed' + ? [] + : getUsersSavedPreference('opt_out_all') + } /> ))} @@ -332,6 +378,10 @@ export function NotificationSettings(props: { icon={} data={generalOther} /> +
} + data={optOut} + /> diff --git a/web/components/switch-setting.tsx b/web/components/switch-setting.tsx index 0e93c420..608b936b 100644 --- a/web/components/switch-setting.tsx +++ b/web/components/switch-setting.tsx @@ -1,33 +1,52 @@ import { Switch } from '@headlessui/react' import clsx from 'clsx' import React from 'react' +import { Tooltip } from 'web/components/tooltip' export const SwitchSetting = (props: { checked: boolean onChange: (checked: boolean) => void label: string + disabled: boolean }) => { - const { checked, onChange, label } = props + const { checked, onChange, label, disabled } = props return ( - - + disabled={disabled} + > + )