From 3efd968058176905018cdd0af8beab9894a187fc Mon Sep 17 00:00:00 2001
From: Ian Philips
Date: Wed, 14 Sep 2022 17:17:32 -0600
Subject: [PATCH] Allow one-click unsubscribe, slight refactor
---
common/notification.ts | 219 ++++++++----
common/user-notification-preferences.ts | 243 +++++++++++++
common/user.ts | 174 +---------
functions/src/create-notification.ts | 24 +-
functions/src/create-user.ts | 9 +-
functions/src/email-templates/500-mana.html | 321 ------------------
.../src/email-templates/creating-market.html | 2 +-
.../email-templates/interesting-markets.html | 2 +-
.../market-answer-comment.html | 2 +-
.../src/email-templates/market-answer.html | 2 +-
.../src/email-templates/market-close.html | 2 +-
.../src/email-templates/market-comment.html | 2 +-
.../market-resolved-no-bets.html | 2 +-
.../src/email-templates/market-resolved.html | 2 +-
.../new-market-from-followed-user.html | 2 +-
.../email-templates/new-unique-bettor.html | 2 +-
.../email-templates/new-unique-bettors.html | 2 +-
functions/src/email-templates/one-week.html | 2 +-
functions/src/email-templates/thank-you.html | 2 +-
functions/src/email-templates/welcome.html | 2 +-
functions/src/emails.ts | 61 ++--
.../create-new-notification-preferences.ts | 4 +-
functions/src/scripts/create-private-users.ts | 5 +-
functions/src/unsubscribe.ts | 252 +++++++++++---
web/components/notification-settings.tsx | 132 ++++---
25 files changed, 723 insertions(+), 749 deletions(-)
create mode 100644 common/user-notification-preferences.ts
delete mode 100644 functions/src/email-templates/500-mana.html
diff --git a/common/notification.ts b/common/notification.ts
index affa33cb..c34f5b9c 100644
--- a/common/notification.ts
+++ b/common/notification.ts
@@ -1,5 +1,4 @@
-import { notification_subscription_types, PrivateUser } from './user'
-import { DOMAIN } from './envs/constants'
+import { notification_preference } from './user-notification-preferences'
export type Notification = {
id: string
@@ -29,6 +28,7 @@ export type Notification = {
isSeenOnHref?: string
}
+
export type notification_source_types =
| 'contract'
| 'comment'
@@ -54,7 +54,7 @@ export type notification_source_update_types =
| 'deleted'
| 'closed'
-/* Optional - if possible use a keyof notification_subscription_types */
+/* Optional - if possible use a notification_preference */
export type notification_reason_types =
| 'tagged_user'
| 'on_new_follow'
@@ -92,75 +92,152 @@ export type notification_reason_types =
| 'your_contract_closed'
| 'subsidized_your_market'
-// Adding a new key:value here is optional, you can just use a key of notification_subscription_types
-// You might want to add a key:value here if there will be multiple notification reasons that map to the same
-// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to
-// 'all_comments_on_watched_markets' subscription type
-// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types
-export const notificationReasonToSubscriptionType: Partial<
- Record
-> = {
- you_referred_user: 'referral_bonuses',
- user_joined_to_bet_on_your_market: 'referral_bonuses',
- tip_received: 'tips_on_your_comments',
- bet_fill: 'limit_order_fills',
- user_joined_from_your_group_invite: 'referral_bonuses',
- challenge_accepted: 'limit_order_fills',
- betting_streak_incremented: 'betting_streaks',
- liked_and_tipped_your_contract: 'tips_on_your_markets',
- comment_on_your_contract: 'all_comments_on_my_markets',
- answer_on_your_contract: 'all_answers_on_my_markets',
- comment_on_contract_you_follow: 'all_comments_on_watched_markets',
- answer_on_contract_you_follow: 'all_answers_on_watched_markets',
- update_on_contract_you_follow: 'market_updates_on_watched_markets',
- resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
- comment_on_contract_with_users_shares_in:
- 'all_comments_on_contracts_with_shares_in_on_watched_markets',
- answer_on_contract_with_users_shares_in:
- 'all_answers_on_contracts_with_shares_in_on_watched_markets',
- update_on_contract_with_users_shares_in:
- 'market_updates_on_watched_markets_with_shares_in',
- resolution_on_contract_with_users_shares_in:
- 'resolutions_on_watched_markets_with_shares_in',
- comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
- update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
- resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
- answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
- comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
- answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
- update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
- resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
- reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets',
- reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets',
-}
-
-export const getDestinationsForUser = async (
- privateUser: PrivateUser,
- reason: notification_reason_types | keyof notification_subscription_types
-) => {
- const notificationSettings = privateUser.notificationPreferences
- let destinations
- let subscriptionType: keyof notification_subscription_types | undefined
- if (Object.keys(notificationSettings).includes(reason)) {
- subscriptionType = reason as keyof notification_subscription_types
- destinations = notificationSettings[subscriptionType]
- } else {
- const key = reason as notification_reason_types
- subscriptionType = notificationReasonToSubscriptionType[key]
- destinations = subscriptionType
- ? notificationSettings[subscriptionType]
- : []
- }
- // const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
- return {
- sendToEmail: destinations.includes('email'),
- sendToBrowser: destinations.includes('browser'),
- // unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
- urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
- }
-}
-
export type BettingStreakData = {
streak: number
bonusAmount: number
}
+
+type notification_descriptions = {
+ [key in notification_preference]: {
+ simple: string
+ detailed: string
+ }
+}
+export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = {
+ all_answers_on_my_markets: {
+ simple: 'Answers on your markets',
+ detailed: 'Answers on your own markets',
+ },
+ all_comments_on_my_markets: {
+ simple: 'Comments on your markets',
+ detailed: 'Comments on your own markets',
+ },
+ answers_by_followed_users_on_watched_markets: {
+ simple: 'Only answers by users you follow',
+ detailed: "Only answers by users you follow on markets you're watching",
+ },
+ answers_by_market_creator_on_watched_markets: {
+ simple: 'Only answers by market creator',
+ detailed: "Only answers by market creator on markets you're watching",
+ },
+ betting_streaks: {
+ simple: 'For predictions made over consecutive days',
+ detailed: 'Bonuses for predictions made over consecutive days',
+ },
+ comments_by_followed_users_on_watched_markets: {
+ simple: 'Only comments by users you follow',
+ detailed:
+ 'Only comments by users that you follow on markets that you watch',
+ },
+ contract_from_followed_user: {
+ simple: 'New markets from users you follow',
+ detailed: 'New markets from users you follow',
+ },
+ limit_order_fills: {
+ simple: 'Limit order fills',
+ detailed: 'When your limit order is filled by another user',
+ },
+ loan_income: {
+ simple: 'Automatic loans from your predictions in unresolved markets',
+ detailed:
+ 'Automatic loans from your predictions that are locked in unresolved markets',
+ },
+ market_updates_on_watched_markets: {
+ simple: 'All creator updates',
+ detailed: 'All market updates made by the creator',
+ },
+ market_updates_on_watched_markets_with_shares_in: {
+ simple: "Only creator updates on markets that you're invested in",
+ detailed:
+ "Only updates made by the creator on markets that you're invested in",
+ },
+ on_new_follow: {
+ simple: 'A user followed you',
+ detailed: 'A user followed you',
+ },
+ onboarding_flow: {
+ simple: 'Emails to help you get started using Manifold',
+ detailed: 'Emails to help you learn how to use Manifold',
+ },
+ probability_updates_on_watched_markets: {
+ simple: 'Large changes in probability on markets that you watch',
+ detailed: 'Large changes in probability on markets that you watch',
+ },
+ profit_loss_updates: {
+ simple: 'Weekly profit and loss updates',
+ detailed: 'Weekly profit and loss updates',
+ },
+ referral_bonuses: {
+ simple: 'For referring new users',
+ detailed: 'Bonuses you receive from referring a new user',
+ },
+ resolutions_on_watched_markets: {
+ simple: 'All market resolutions',
+ detailed: "All resolutions on markets that you're watching",
+ },
+ resolutions_on_watched_markets_with_shares_in: {
+ simple: "Only market resolutions that you're invested in",
+ detailed:
+ "Only resolutions of markets you're watching and that you're invested in",
+ },
+ subsidized_your_market: {
+ simple: 'Your market was subsidized',
+ detailed: 'When someone subsidizes your market',
+ },
+ tagged_user: {
+ simple: 'A user tagged you',
+ detailed: 'When another use tags you',
+ },
+ thank_you_for_purchases: {
+ simple: 'Thank you notes for your purchases',
+ detailed: 'Thank you notes for your purchases',
+ },
+ tipped_comments_on_watched_markets: {
+ simple: 'Only highly tipped comments on markets that you watch',
+ detailed: 'Only highly tipped comments on markets that you watch',
+ },
+ tips_on_your_comments: {
+ simple: 'Tips on your comments',
+ detailed: 'Tips on your comments',
+ },
+ tips_on_your_markets: {
+ simple: 'Tips/Likes on your markets',
+ detailed: 'Tips/Likes on your markets',
+ },
+ trending_markets: {
+ simple: 'Weekly interesting markets',
+ detailed: 'Weekly interesting markets',
+ },
+ unique_bettors_on_your_contract: {
+ simple: 'For unique predictors on your markets',
+ 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',
+ },
+ all_comments_on_watched_markets: {
+ simple: 'All new comments',
+ detailed: 'All new comments on markets you follow',
+ },
+ all_comments_on_contracts_with_shares_in_on_watched_markets: {
+ simple: `Only on markets you're invested in`,
+ detailed: `Comments on markets that you're watching and you're invested in`,
+ },
+ all_replies_to_my_comments_on_watched_markets: {
+ simple: 'Only replies to your comments',
+ detailed: "Only replies to your comments on markets you're watching",
+ },
+ all_replies_to_my_answers_on_watched_markets: {
+ simple: 'Only replies to your answers',
+ detailed: "Only replies to your answers on markets you're watching",
+ },
+ all_answers_on_watched_markets: {
+ simple: 'All new answers',
+ detailed: "All new answers on markets you're watching",
+ },
+ all_answers_on_contracts_with_shares_in_on_watched_markets: {
+ simple: `Only on markets you're invested in`,
+ detailed: `Answers on markets that you're watching and that you're invested in`,
+ },
+}
diff --git a/common/user-notification-preferences.ts b/common/user-notification-preferences.ts
new file mode 100644
index 00000000..e2402ea9
--- /dev/null
+++ b/common/user-notification-preferences.ts
@@ -0,0 +1,243 @@
+import { filterDefined } from './util/array'
+import { notification_reason_types } from './notification'
+import { getFunctionUrl } from './api'
+import { DOMAIN } from './envs/constants'
+import { PrivateUser } from './user'
+
+export type notification_destination_types = 'email' | 'browser'
+export type notification_preference = keyof notification_preferences
+export type notification_preferences = {
+ // Watched Markets
+ all_comments_on_watched_markets: notification_destination_types[]
+ all_answers_on_watched_markets: notification_destination_types[]
+
+ // Comments
+ tipped_comments_on_watched_markets: notification_destination_types[]
+ comments_by_followed_users_on_watched_markets: notification_destination_types[]
+ all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
+ all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
+ all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
+
+ // Answers
+ answers_by_followed_users_on_watched_markets: notification_destination_types[]
+ answers_by_market_creator_on_watched_markets: notification_destination_types[]
+ all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
+
+ // On users' markets
+ your_contract_closed: notification_destination_types[]
+ all_comments_on_my_markets: notification_destination_types[]
+ all_answers_on_my_markets: notification_destination_types[]
+ subsidized_your_market: notification_destination_types[]
+
+ // Market updates
+ resolutions_on_watched_markets: notification_destination_types[]
+ resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
+ market_updates_on_watched_markets: notification_destination_types[]
+ market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
+ probability_updates_on_watched_markets: notification_destination_types[]
+
+ // Balance Changes
+ loan_income: notification_destination_types[]
+ betting_streaks: notification_destination_types[]
+ referral_bonuses: notification_destination_types[]
+ unique_bettors_on_your_contract: notification_destination_types[]
+ tips_on_your_comments: notification_destination_types[]
+ tips_on_your_markets: notification_destination_types[]
+ limit_order_fills: notification_destination_types[]
+
+ // General
+ tagged_user: notification_destination_types[]
+ on_new_follow: notification_destination_types[]
+ contract_from_followed_user: notification_destination_types[]
+ trending_markets: notification_destination_types[]
+ profit_loss_updates: notification_destination_types[]
+ onboarding_flow: notification_destination_types[]
+ thank_you_for_purchases: notification_destination_types[]
+}
+
+export const getDefaultNotificationPreferences = (
+ userId: string,
+ privateUser?: PrivateUser,
+ noEmails?: boolean
+) => {
+ const {
+ unsubscribedFromCommentEmails,
+ unsubscribedFromAnswerEmails,
+ unsubscribedFromResolutionEmails,
+ unsubscribedFromWeeklyTrendingEmails,
+ unsubscribedFromGenericEmails,
+ } = privateUser || {}
+
+ const constructPref = (browserIf: boolean, emailIf: boolean) => {
+ const browser = browserIf ? 'browser' : undefined
+ const email = noEmails ? undefined : emailIf ? 'email' : undefined
+ return filterDefined([browser, email]) as notification_destination_types[]
+ }
+ return {
+ // Watched Markets
+ all_comments_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromCommentEmails
+ ),
+ all_answers_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromAnswerEmails
+ ),
+
+ // Comments
+ tips_on_your_comments: constructPref(true, !unsubscribedFromCommentEmails),
+ comments_by_followed_users_on_watched_markets: constructPref(true, false),
+ all_replies_to_my_comments_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromCommentEmails
+ ),
+ all_replies_to_my_answers_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromCommentEmails
+ ),
+ all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromCommentEmails
+ ),
+
+ // Answers
+ answers_by_followed_users_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromAnswerEmails
+ ),
+ answers_by_market_creator_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromAnswerEmails
+ ),
+ all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromAnswerEmails
+ ),
+
+ // On users' markets
+ your_contract_closed: constructPref(
+ true,
+ !unsubscribedFromResolutionEmails
+ ), // High priority
+ all_comments_on_my_markets: constructPref(
+ true,
+ !unsubscribedFromCommentEmails
+ ),
+ all_answers_on_my_markets: constructPref(
+ true,
+ !unsubscribedFromAnswerEmails
+ ),
+ subsidized_your_market: constructPref(true, true),
+
+ // Market updates
+ resolutions_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromResolutionEmails
+ ),
+ market_updates_on_watched_markets: constructPref(true, false),
+ market_updates_on_watched_markets_with_shares_in: constructPref(
+ true,
+ false
+ ),
+ resolutions_on_watched_markets_with_shares_in: constructPref(
+ true,
+ !unsubscribedFromResolutionEmails
+ ),
+
+ //Balance Changes
+ loan_income: constructPref(true, false),
+ betting_streaks: constructPref(true, false),
+ referral_bonuses: constructPref(true, true),
+ unique_bettors_on_your_contract: constructPref(true, false),
+ tipped_comments_on_watched_markets: constructPref(
+ true,
+ !unsubscribedFromCommentEmails
+ ),
+ tips_on_your_markets: constructPref(true, true),
+ limit_order_fills: constructPref(true, false),
+
+ // General
+ tagged_user: constructPref(true, true),
+ on_new_follow: constructPref(true, true),
+ contract_from_followed_user: constructPref(true, true),
+ trending_markets: constructPref(
+ false,
+ !unsubscribedFromWeeklyTrendingEmails
+ ),
+ profit_loss_updates: constructPref(false, true),
+ probability_updates_on_watched_markets: constructPref(true, false),
+ thank_you_for_purchases: constructPref(
+ false,
+ !unsubscribedFromGenericEmails
+ ),
+ onboarding_flow: constructPref(false, !unsubscribedFromGenericEmails),
+ } as notification_preferences
+}
+
+// Adding a new key:value here is optional, you can just use a key of notification_subscription_types
+// You might want to add a key:value here if there will be multiple notification reasons that map to the same
+// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to
+// 'all_comments_on_watched_markets' subscription type
+// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types
+const notificationReasonToSubscriptionType: Partial<
+ Record
+> = {
+ you_referred_user: 'referral_bonuses',
+ user_joined_to_bet_on_your_market: 'referral_bonuses',
+ tip_received: 'tips_on_your_comments',
+ bet_fill: 'limit_order_fills',
+ user_joined_from_your_group_invite: 'referral_bonuses',
+ challenge_accepted: 'limit_order_fills',
+ betting_streak_incremented: 'betting_streaks',
+ liked_and_tipped_your_contract: 'tips_on_your_markets',
+ comment_on_your_contract: 'all_comments_on_my_markets',
+ answer_on_your_contract: 'all_answers_on_my_markets',
+ comment_on_contract_you_follow: 'all_comments_on_watched_markets',
+ answer_on_contract_you_follow: 'all_answers_on_watched_markets',
+ update_on_contract_you_follow: 'market_updates_on_watched_markets',
+ resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
+ comment_on_contract_with_users_shares_in:
+ 'all_comments_on_contracts_with_shares_in_on_watched_markets',
+ answer_on_contract_with_users_shares_in:
+ 'all_answers_on_contracts_with_shares_in_on_watched_markets',
+ update_on_contract_with_users_shares_in:
+ 'market_updates_on_watched_markets_with_shares_in',
+ resolution_on_contract_with_users_shares_in:
+ 'resolutions_on_watched_markets_with_shares_in',
+ comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
+ update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
+ resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
+ answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
+ comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
+ answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
+ update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
+ resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
+ reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets',
+ reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets',
+}
+
+export const getNotificationDestinationsForUser = (
+ privateUser: PrivateUser,
+ reason: notification_reason_types | notification_preference
+) => {
+ const notificationSettings = privateUser.notificationPreferences
+ let destinations
+ let subscriptionType: notification_preference | undefined
+ if (Object.keys(notificationSettings).includes(reason)) {
+ subscriptionType = reason as notification_preference
+ destinations = notificationSettings[subscriptionType]
+ } else {
+ const key = reason as notification_reason_types
+ subscriptionType = notificationReasonToSubscriptionType[key]
+ destinations = subscriptionType
+ ? notificationSettings[subscriptionType]
+ : []
+ }
+ const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
+ return {
+ sendToEmail: destinations.includes('email'),
+ sendToBrowser: destinations.includes('browser'),
+ unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
+ urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
+ }
+}
diff --git a/common/user.ts b/common/user.ts
index 7bd89906..16a2b437 100644
--- a/common/user.ts
+++ b/common/user.ts
@@ -1,4 +1,4 @@
-import { filterDefined } from './util/array'
+import { notification_preferences } from './user-notification-preferences'
export type User = {
id: string
@@ -65,7 +65,7 @@ export type PrivateUser = {
initialDeviceToken?: string
initialIpAddress?: string
apiKey?: string
- notificationPreferences: notification_subscription_types
+ notificationPreferences: notification_preferences
twitchInfo?: {
twitchName: string
controlToken: string
@@ -73,57 +73,6 @@ export type PrivateUser = {
}
}
-export type notification_destination_types = 'email' | 'browser'
-export type notification_subscription_types = {
- // Watched Markets
- all_comments_on_watched_markets: notification_destination_types[]
- all_answers_on_watched_markets: notification_destination_types[]
-
- // Comments
- tipped_comments_on_watched_markets: notification_destination_types[]
- comments_by_followed_users_on_watched_markets: notification_destination_types[]
- all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
- all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
- all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
-
- // Answers
- answers_by_followed_users_on_watched_markets: notification_destination_types[]
- answers_by_market_creator_on_watched_markets: notification_destination_types[]
- all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
-
- // On users' markets
- your_contract_closed: notification_destination_types[]
- all_comments_on_my_markets: notification_destination_types[]
- all_answers_on_my_markets: notification_destination_types[]
- subsidized_your_market: notification_destination_types[]
-
- // Market updates
- resolutions_on_watched_markets: notification_destination_types[]
- resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
- market_updates_on_watched_markets: notification_destination_types[]
- market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
- probability_updates_on_watched_markets: notification_destination_types[]
-
- // Balance Changes
- loan_income: notification_destination_types[]
- betting_streaks: notification_destination_types[]
- referral_bonuses: notification_destination_types[]
- unique_bettors_on_your_contract: notification_destination_types[]
- tips_on_your_comments: notification_destination_types[]
- tips_on_your_markets: notification_destination_types[]
- limit_order_fills: notification_destination_types[]
-
- // General
- tagged_user: notification_destination_types[]
- on_new_follow: notification_destination_types[]
- contract_from_followed_user: notification_destination_types[]
- trending_markets: notification_destination_types[]
- profit_loss_updates: notification_destination_types[]
- onboarding_flow: notification_destination_types[]
- thank_you_for_purchases: notification_destination_types[]
-}
-export type notification_subscribe_types = 'all' | 'less' | 'none'
-
export type PortfolioMetrics = {
investmentValue: number
balance: number
@@ -134,122 +83,3 @@ export type PortfolioMetrics = {
export const MANIFOLD_USERNAME = 'ManifoldMarkets'
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
-
-export const getDefaultNotificationSettings = (
- userId: string,
- privateUser?: PrivateUser,
- noEmails?: boolean
-) => {
- const {
- unsubscribedFromCommentEmails,
- unsubscribedFromAnswerEmails,
- unsubscribedFromResolutionEmails,
- unsubscribedFromWeeklyTrendingEmails,
- unsubscribedFromGenericEmails,
- } = privateUser || {}
-
- const constructPref = (browserIf: boolean, emailIf: boolean) => {
- const browser = browserIf ? 'browser' : undefined
- const email = noEmails ? undefined : emailIf ? 'email' : undefined
- return filterDefined([browser, email]) as notification_destination_types[]
- }
- return {
- // Watched Markets
- all_comments_on_watched_markets: constructPref(
- true,
- !unsubscribedFromCommentEmails
- ),
- all_answers_on_watched_markets: constructPref(
- true,
- !unsubscribedFromAnswerEmails
- ),
-
- // Comments
- tips_on_your_comments: constructPref(true, !unsubscribedFromCommentEmails),
- comments_by_followed_users_on_watched_markets: constructPref(true, false),
- all_replies_to_my_comments_on_watched_markets: constructPref(
- true,
- !unsubscribedFromCommentEmails
- ),
- all_replies_to_my_answers_on_watched_markets: constructPref(
- true,
- !unsubscribedFromCommentEmails
- ),
- all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
- true,
- !unsubscribedFromCommentEmails
- ),
-
- // Answers
- answers_by_followed_users_on_watched_markets: constructPref(
- true,
- !unsubscribedFromAnswerEmails
- ),
- answers_by_market_creator_on_watched_markets: constructPref(
- true,
- !unsubscribedFromAnswerEmails
- ),
- all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
- true,
- !unsubscribedFromAnswerEmails
- ),
-
- // On users' markets
- your_contract_closed: constructPref(
- true,
- !unsubscribedFromResolutionEmails
- ), // High priority
- all_comments_on_my_markets: constructPref(
- true,
- !unsubscribedFromCommentEmails
- ),
- all_answers_on_my_markets: constructPref(
- true,
- !unsubscribedFromAnswerEmails
- ),
- subsidized_your_market: constructPref(true, true),
-
- // Market updates
- resolutions_on_watched_markets: constructPref(
- true,
- !unsubscribedFromResolutionEmails
- ),
- market_updates_on_watched_markets: constructPref(true, false),
- market_updates_on_watched_markets_with_shares_in: constructPref(
- true,
- false
- ),
- resolutions_on_watched_markets_with_shares_in: constructPref(
- true,
- !unsubscribedFromResolutionEmails
- ),
-
- //Balance Changes
- loan_income: constructPref(true, false),
- betting_streaks: constructPref(true, false),
- referral_bonuses: constructPref(true, true),
- unique_bettors_on_your_contract: constructPref(true, false),
- tipped_comments_on_watched_markets: constructPref(
- true,
- !unsubscribedFromCommentEmails
- ),
- tips_on_your_markets: constructPref(true, true),
- limit_order_fills: constructPref(true, false),
-
- // General
- tagged_user: constructPref(true, true),
- on_new_follow: constructPref(true, true),
- contract_from_followed_user: constructPref(true, true),
- trending_markets: constructPref(
- false,
- !unsubscribedFromWeeklyTrendingEmails
- ),
- profit_loss_updates: constructPref(false, true),
- probability_updates_on_watched_markets: constructPref(true, false),
- thank_you_for_purchases: constructPref(
- false,
- !unsubscribedFromGenericEmails
- ),
- onboarding_flow: constructPref(false, !unsubscribedFromGenericEmails),
- } as notification_subscription_types
-}
diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts
index 34a8f218..ba9fa5c4 100644
--- a/functions/src/create-notification.ts
+++ b/functions/src/create-notification.ts
@@ -1,7 +1,6 @@
import * as admin from 'firebase-admin'
import {
BettingStreakData,
- getDestinationsForUser,
Notification,
notification_reason_types,
} from '../../common/notification'
@@ -27,6 +26,7 @@ import {
sendNewUniqueBettorsEmail,
} from './emails'
import { filterDefined } from '../../common/util/array'
+import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
const firestore = admin.firestore()
type recipients_to_reason_texts = {
@@ -66,7 +66,7 @@ export const createNotification = async (
const { reason } = userToReasonTexts[userId]
const privateUser = await getPrivateUser(userId)
if (!privateUser) continue
- const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
+ const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
privateUser,
reason
)
@@ -236,7 +236,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
return
const privateUser = await getPrivateUser(userId)
if (!privateUser) return
- const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
+ const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
privateUser,
reason
)
@@ -468,7 +468,7 @@ export const createTipNotification = async (
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'tip_received'
)
@@ -513,7 +513,7 @@ export const createBetFillNotification = async (
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'bet_fill'
)
@@ -558,7 +558,7 @@ export const createReferralNotification = async (
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'you_referred_user'
)
@@ -612,7 +612,7 @@ export const createLoanIncomeNotification = async (
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'loan_income'
)
@@ -650,7 +650,7 @@ export const createChallengeAcceptedNotification = async (
) => {
const privateUser = await getPrivateUser(challengeCreator.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'challenge_accepted'
)
@@ -692,7 +692,7 @@ export const createBettingStreakBonusNotification = async (
) => {
const privateUser = await getPrivateUser(user.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'betting_streak_incremented'
)
@@ -739,7 +739,7 @@ export const createLikeNotification = async (
) => {
const privateUser = await getPrivateUser(toUser.id)
if (!privateUser) return
- const { sendToBrowser } = await getDestinationsForUser(
+ const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'liked_and_tipped_your_contract'
)
@@ -786,7 +786,7 @@ export const createUniqueBettorBonusNotification = async (
) => {
const privateUser = await getPrivateUser(contractCreatorId)
if (!privateUser) return
- const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
+ const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
privateUser,
'unique_bettors_on_your_contract'
)
@@ -876,7 +876,7 @@ export const createNewContractNotification = async (
) => {
const privateUser = await getPrivateUser(userId)
if (!privateUser) return
- const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
+ const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
privateUser,
reason
)
diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts
index ab5f014a..ab70b4e6 100644
--- a/functions/src/create-user.ts
+++ b/functions/src/create-user.ts
@@ -1,11 +1,7 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
-import {
- getDefaultNotificationSettings,
- PrivateUser,
- User,
-} from '../../common/user'
+import { PrivateUser, User } from '../../common/user'
import { getUser, getUserByUsername, getValues } from './utils'
import { randomString } from '../../common/util/random'
import {
@@ -22,6 +18,7 @@ import { track } from './analytics'
import { APIError, newEndpoint, validate } from './api'
import { Group } from '../../common/group'
import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy'
+import { getDefaultNotificationPreferences } from '../../common/user-notification-preferences'
const bodySchema = z.object({
deviceToken: z.string().optional(),
@@ -83,7 +80,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
email,
initialIpAddress: req.ip,
initialDeviceToken: deviceToken,
- notificationPreferences: getDefaultNotificationSettings(auth.uid),
+ notificationPreferences: getDefaultNotificationPreferences(auth.uid),
}
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
diff --git a/functions/src/email-templates/500-mana.html b/functions/src/email-templates/500-mana.html
deleted file mode 100644
index c8f6a171..00000000
--- a/functions/src/email-templates/500-mana.html
+++ /dev/null
@@ -1,321 +0,0 @@
-
-
-
-
- Manifold Markets 7th Day Anniversary Gift!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
- |
-
-
-
-
- |
-
-
-
-
- Thanks for
- using Manifold Markets. Running low
- on mana (M$)? Click the link below to receive a one time gift of M$500!
-
- |
-
-
-
-
- |
-
-
-
-
- |
-
-
-
-
- Did
- you know, besides making correct predictions, there are
- plenty of other ways to earn mana?
-
-
- Cheers,
-
- David
- from Manifold
-
-
- |
-
-
-
-
-
-
-
- Cheers,
- David from Manifold
-
-
- |
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
- |
-
-
-
- |
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/functions/src/email-templates/creating-market.html b/functions/src/email-templates/creating-market.html
index c73f7458..bf163f69 100644
--- a/functions/src/email-templates/creating-market.html
+++ b/functions/src/email-templates/creating-market.html
@@ -494,7 +494,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/interesting-markets.html b/functions/src/email-templates/interesting-markets.html
index 7c3e653d..0cee6269 100644
--- a/functions/src/email-templates/interesting-markets.html
+++ b/functions/src/email-templates/interesting-markets.html
@@ -443,7 +443,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/market-answer-comment.html b/functions/src/email-templates/market-answer-comment.html
index a19aa7c3..4b98730f 100644
--- a/functions/src/email-templates/market-answer-comment.html
+++ b/functions/src/email-templates/market-answer-comment.html
@@ -529,7 +529,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/market-answer.html b/functions/src/email-templates/market-answer.html
index b2d7f727..e3d42b9d 100644
--- a/functions/src/email-templates/market-answer.html
+++ b/functions/src/email-templates/market-answer.html
@@ -369,7 +369,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
|
diff --git a/functions/src/email-templates/market-close.html b/functions/src/email-templates/market-close.html
index ee7976b0..4abd225e 100644
--- a/functions/src/email-templates/market-close.html
+++ b/functions/src/email-templates/market-close.html
@@ -487,7 +487,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
|
diff --git a/functions/src/email-templates/market-comment.html b/functions/src/email-templates/market-comment.html
index 23e20dac..ce0669f1 100644
--- a/functions/src/email-templates/market-comment.html
+++ b/functions/src/email-templates/market-comment.html
@@ -369,7 +369,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/market-resolved-no-bets.html b/functions/src/email-templates/market-resolved-no-bets.html
index ff5f541f..5d886adf 100644
--- a/functions/src/email-templates/market-resolved-no-bets.html
+++ b/functions/src/email-templates/market-resolved-no-bets.html
@@ -470,7 +470,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/market-resolved.html b/functions/src/email-templates/market-resolved.html
index de29a0f1..767202b6 100644
--- a/functions/src/email-templates/market-resolved.html
+++ b/functions/src/email-templates/market-resolved.html
@@ -502,7 +502,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/new-market-from-followed-user.html b/functions/src/email-templates/new-market-from-followed-user.html
index 877d554f..49633fb2 100644
--- a/functions/src/email-templates/new-market-from-followed-user.html
+++ b/functions/src/email-templates/new-market-from-followed-user.html
@@ -318,7 +318,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/new-unique-bettor.html b/functions/src/email-templates/new-unique-bettor.html
index 30da8b99..51026121 100644
--- a/functions/src/email-templates/new-unique-bettor.html
+++ b/functions/src/email-templates/new-unique-bettor.html
@@ -376,7 +376,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/new-unique-bettors.html b/functions/src/email-templates/new-unique-bettors.html
index eb4c04e2..09c44d03 100644
--- a/functions/src/email-templates/new-unique-bettors.html
+++ b/functions/src/email-templates/new-unique-bettors.html
@@ -480,7 +480,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/one-week.html b/functions/src/email-templates/one-week.html
index b8e233d5..e7d14a7e 100644
--- a/functions/src/email-templates/one-week.html
+++ b/functions/src/email-templates/one-week.html
@@ -283,7 +283,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/thank-you.html b/functions/src/email-templates/thank-you.html
index 7ac72d0a..beef11ee 100644
--- a/functions/src/email-templates/thank-you.html
+++ b/functions/src/email-templates/thank-you.html
@@ -218,7 +218,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/email-templates/welcome.html b/functions/src/email-templates/welcome.html
index dccec695..d6caaa0c 100644
--- a/functions/src/email-templates/welcome.html
+++ b/functions/src/email-templates/welcome.html
@@ -290,7 +290,7 @@
click here to manage your notifications.
+ " target="_blank">click here to unsubscribe from this type of notification.
diff --git a/functions/src/emails.ts b/functions/src/emails.ts
index bb9f7195..98309ebe 100644
--- a/functions/src/emails.ts
+++ b/functions/src/emails.ts
@@ -2,11 +2,7 @@ import { DOMAIN } from '../../common/envs/constants'
import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate'
import { Contract } from '../../common/contract'
-import {
- notification_subscription_types,
- PrivateUser,
- User,
-} from '../../common/user'
+import { PrivateUser, User } from '../../common/user'
import {
formatLargeNumber,
formatMoney,
@@ -18,11 +14,12 @@ import { formatNumericProbability } from '../../common/pseudo-numeric'
import { sendTemplateEmail, sendTextEmail } from './send-email'
import { getUser } from './utils'
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
-import {
- notification_reason_types,
- getDestinationsForUser,
-} from '../../common/notification'
+import { notification_reason_types } from '../../common/notification'
import { Dictionary } from 'lodash'
+import {
+ getNotificationDestinationsForUser,
+ notification_preference,
+} from '../../common/user-notification-preferences'
export const sendMarketResolutionEmail = async (
reason: notification_reason_types,
@@ -36,8 +33,10 @@ export const sendMarketResolutionEmail = async (
resolutionProbability?: number,
resolutions?: { [outcome: string]: number }
) => {
- const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
- await getDestinationsForUser(privateUser, reason)
+ const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
+ privateUser,
+ reason
+ )
if (!privateUser || !privateUser.email || !sendToEmail) return
const user = await getUser(privateUser.id)
@@ -154,7 +153,7 @@ export const sendWelcomeEmail = async (
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
- 'onboarding_flow' as keyof notification_subscription_types
+ 'onboarding_flow' as notification_preference
}`
return await sendTemplateEmail(
@@ -222,7 +221,7 @@ export const sendOneWeekBonusEmail = async (
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
- 'onboarding_flow' as keyof notification_subscription_types
+ 'onboarding_flow' as notification_preference
}`
return await sendTemplateEmail(
privateUser.email,
@@ -255,7 +254,7 @@ export const sendCreatorGuideEmail = async (
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
- 'onboarding_flow' as keyof notification_subscription_types
+ 'onboarding_flow' as notification_preference
}`
return await sendTemplateEmail(
privateUser.email,
@@ -289,7 +288,7 @@ export const sendThankYouEmail = async (
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
- 'thank_you_for_purchases' as keyof notification_subscription_types
+ 'thank_you_for_purchases' as notification_preference
}`
return await sendTemplateEmail(
@@ -312,8 +311,10 @@ export const sendMarketCloseEmail = async (
privateUser: PrivateUser,
contract: Contract
) => {
- const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
- await getDestinationsForUser(privateUser, reason)
+ const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
+ privateUser,
+ reason
+ )
if (!privateUser.email || !sendToEmail) return
@@ -350,8 +351,10 @@ export const sendNewCommentEmail = async (
answerText?: string,
answerId?: string
) => {
- const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
- await getDestinationsForUser(privateUser, reason)
+ const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
+ privateUser,
+ reason
+ )
if (!privateUser || !privateUser.email || !sendToEmail) return
const { question } = contract
@@ -425,8 +428,10 @@ export const sendNewAnswerEmail = async (
// Don't send the creator's own answers.
if (privateUser.id === creatorId) return
- const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
- await getDestinationsForUser(privateUser, reason)
+ const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
+ privateUser,
+ reason
+ )
if (!privateUser.email || !sendToEmail) return
const { question, creatorUsername, slug } = contract
@@ -465,7 +470,7 @@ export const sendInterestingMarketsEmail = async (
return
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
- 'trending_markets' as keyof notification_subscription_types
+ 'trending_markets' as notification_preference
}`
const { name } = user
@@ -516,8 +521,10 @@ export const sendNewFollowedMarketEmail = async (
privateUser: PrivateUser,
contract: Contract
) => {
- const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
- await getDestinationsForUser(privateUser, reason)
+ const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
+ privateUser,
+ reason
+ )
if (!privateUser.email || !sendToEmail) return
const user = await getUser(privateUser.id)
if (!user) return
@@ -553,8 +560,10 @@ export const sendNewUniqueBettorsEmail = async (
userBets: Dictionary<[Bet, ...Bet[]]>,
bonusAmount: number
) => {
- const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
- await getDestinationsForUser(privateUser, reason)
+ const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
+ privateUser,
+ reason
+ )
if (!privateUser.email || !sendToEmail) return
const user = await getUser(privateUser.id)
if (!user) return
diff --git a/functions/src/scripts/create-new-notification-preferences.ts b/functions/src/scripts/create-new-notification-preferences.ts
index 2796f2f7..4ba2e25e 100644
--- a/functions/src/scripts/create-new-notification-preferences.ts
+++ b/functions/src/scripts/create-new-notification-preferences.ts
@@ -1,8 +1,8 @@
import * as admin from 'firebase-admin'
import { initAdmin } from './script-init'
-import { getDefaultNotificationSettings } from 'common/user'
import { getAllPrivateUsers, isProd } from 'functions/src/utils'
+import { getDefaultNotificationPreferences } from 'common/user-notification-preferences'
initAdmin()
const firestore = admin.firestore()
@@ -17,7 +17,7 @@ async function main() {
.collection('private-users')
.doc(privateUser.id)
.update({
- notificationPreferences: getDefaultNotificationSettings(
+ notificationPreferences: getDefaultNotificationPreferences(
privateUser.id,
privateUser,
disableEmails
diff --git a/functions/src/scripts/create-private-users.ts b/functions/src/scripts/create-private-users.ts
index 21e117cf..762e801a 100644
--- a/functions/src/scripts/create-private-users.ts
+++ b/functions/src/scripts/create-private-users.ts
@@ -3,8 +3,9 @@ import * as admin from 'firebase-admin'
import { initAdmin } from './script-init'
initAdmin()
-import { getDefaultNotificationSettings, PrivateUser, User } from 'common/user'
+import { PrivateUser, User } from 'common/user'
import { STARTING_BALANCE } from 'common/economy'
+import { getDefaultNotificationPreferences } from 'common/user-notification-preferences'
const firestore = admin.firestore()
@@ -21,7 +22,7 @@ async function main() {
id: user.id,
email,
username,
- notificationPreferences: getDefaultNotificationSettings(user.id),
+ notificationPreferences: getDefaultNotificationPreferences(user.id),
}
if (user.totalDeposits === undefined) {
diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts
index da7b507f..418282c7 100644
--- a/functions/src/unsubscribe.ts
+++ b/functions/src/unsubscribe.ts
@@ -1,79 +1,227 @@
import * as admin from 'firebase-admin'
import { EndpointDefinition } from './api'
-import { getUser } from './utils'
+import { getPrivateUser } from './utils'
import { PrivateUser } from '../../common/user'
+import { NOTIFICATION_DESCRIPTIONS } from '../../common/notification'
+import { notification_preference } from '../../common/user-notification-preferences'
export const unsubscribe: EndpointDefinition = {
opts: { method: 'GET', minInstances: 1 },
handler: async (req, res) => {
const id = req.query.id as string
- let type = req.query.type as string
+ const type = req.query.type as string
if (!id || !type) {
- res.status(400).send('Empty id or type parameter.')
+ res.status(400).send('Empty id or subscription type parameter.')
+ return
+ }
+ console.log(`Unsubscribing ${id} from ${type}`)
+ const notificationSubscriptionType = type as notification_preference
+ if (notificationSubscriptionType === undefined) {
+ res.status(400).send('Invalid subscription type parameter.')
return
}
- if (type === 'market-resolved') type = 'market-resolve'
-
- if (
- ![
- 'market-resolve',
- 'market-comment',
- 'market-answer',
- 'generic',
- 'weekly-trending',
- ].includes(type)
- ) {
- res.status(400).send('Invalid type parameter.')
- return
- }
-
- const user = await getUser(id)
+ const user = await getPrivateUser(id)
if (!user) {
res.send('This user is not currently subscribed or does not exist.')
return
}
- const { name } = user
+ const previousDestinations =
+ user.notificationPreferences[notificationSubscriptionType]
+
+ console.log(previousDestinations)
+ const { email } = user
const update: Partial = {
- ...(type === 'market-resolve' && {
- unsubscribedFromResolutionEmails: true,
- }),
- ...(type === 'market-comment' && {
- unsubscribedFromCommentEmails: true,
- }),
- ...(type === 'market-answer' && {
- unsubscribedFromAnswerEmails: true,
- }),
- ...(type === 'generic' && {
- unsubscribedFromGenericEmails: true,
- }),
- ...(type === 'weekly-trending' && {
- unsubscribedFromWeeklyTrendingEmails: true,
- }),
+ notificationPreferences: {
+ ...user.notificationPreferences,
+ [notificationSubscriptionType]: previousDestinations.filter(
+ (destination) => destination !== 'email'
+ ),
+ },
}
await firestore.collection('private-users').doc(id).update(update)
- if (type === 'market-resolve')
- res.send(
- `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
- )
- else if (type === 'market-comment')
- res.send(
- `${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
- )
- else if (type === 'market-answer')
- res.send(
- `${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
- )
- else if (type === 'weekly-trending')
- res.send(
- `${name}, you have been unsubscribed from weekly trending emails on Manifold Markets.`
- )
- else res.send(`${name}, you have been unsubscribed.`)
+ res.send(
+ `
+
+
+
+
+ Manifold Markets 7th Day Anniversary Gift!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ ${email} has been unsubscribed from email notifications related to:
+
+
+
+
+ ${NOTIFICATION_DESCRIPTIONS[notificationSubscriptionType].detailed}.
+
+
+
+
+ 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 d18896bd..8730ce7f 100644
--- a/web/components/notification-settings.tsx
+++ b/web/components/notification-settings.tsx
@@ -1,11 +1,7 @@
import React, { memo, ReactNode, useEffect, useState } from 'react'
import { Row } from 'web/components/layout/row'
import clsx from 'clsx'
-import {
- notification_subscription_types,
- notification_destination_types,
- PrivateUser,
-} from 'common/user'
+import { PrivateUser } from 'common/user'
import { updatePrivateUser } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col'
import {
@@ -30,6 +26,11 @@ import {
usePersistentState,
} from 'web/hooks/use-persistent-state'
import { safeLocalStorage } from 'web/lib/util/local'
+import { NOTIFICATION_DESCRIPTIONS } from 'common/notification'
+import {
+ notification_destination_types,
+ notification_preference,
+} from 'common/user-notification-preferences'
export function NotificationSettings(props: {
navigateToSection: string | undefined
@@ -38,7 +39,7 @@ export function NotificationSettings(props: {
const { navigateToSection, privateUser } = props
const [showWatchModal, setShowWatchModal] = useState(false)
- const emailsEnabled: Array = [
+ const emailsEnabled: Array = [
'all_comments_on_watched_markets',
'all_replies_to_my_comments_on_watched_markets',
'all_comments_on_contracts_with_shares_in_on_watched_markets',
@@ -74,7 +75,7 @@ export function NotificationSettings(props: {
// 'probability_updates_on_watched_markets',
// 'limit_order_fills',
]
- const browserDisabled: Array = [
+ const browserDisabled: Array = [
'trending_markets',
'profit_loss_updates',
'onboarding_flow',
@@ -83,91 +84,82 @@ export function NotificationSettings(props: {
type SectionData = {
label: string
- subscriptionTypeToDescription: {
- [key in keyof Partial]: string
- }
+ subscriptionTypes: Partial[]
}
const comments: SectionData = {
label: 'New Comments',
- subscriptionTypeToDescription: {
- all_comments_on_watched_markets: 'All new comments',
- all_comments_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
+ subscriptionTypes: [
+ 'all_comments_on_watched_markets',
+ 'all_comments_on_contracts_with_shares_in_on_watched_markets',
// TODO: combine these two
- all_replies_to_my_comments_on_watched_markets:
- 'Only replies to your comments',
- all_replies_to_my_answers_on_watched_markets:
- 'Only replies to your answers',
- // comments_by_followed_users_on_watched_markets: 'By followed users',
- },
+ 'all_replies_to_my_comments_on_watched_markets',
+ 'all_replies_to_my_answers_on_watched_markets',
+ ],
}
const answers: SectionData = {
label: 'New Answers',
- subscriptionTypeToDescription: {
- all_answers_on_watched_markets: 'All new answers',
- all_answers_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
- // answers_by_followed_users_on_watched_markets: 'By followed users',
- // answers_by_market_creator_on_watched_markets: 'By market creator',
- },
+ subscriptionTypes: [
+ 'all_answers_on_watched_markets',
+ 'all_answers_on_contracts_with_shares_in_on_watched_markets',
+ ],
}
const updates: SectionData = {
label: 'Updates & Resolutions',
- subscriptionTypeToDescription: {
- market_updates_on_watched_markets: 'All creator updates',
- market_updates_on_watched_markets_with_shares_in: `Only creator updates on markets you're invested in`,
- resolutions_on_watched_markets: 'All market resolutions',
- resolutions_on_watched_markets_with_shares_in: `Only market resolutions you're invested in`,
- // probability_updates_on_watched_markets: 'Probability updates',
- },
+ subscriptionTypes: [
+ 'market_updates_on_watched_markets',
+ 'market_updates_on_watched_markets_with_shares_in',
+ 'resolutions_on_watched_markets',
+ 'resolutions_on_watched_markets_with_shares_in',
+ ],
}
const yourMarkets: SectionData = {
label: 'Markets You Created',
- subscriptionTypeToDescription: {
- your_contract_closed: 'Your market has closed (and needs resolution)',
- all_comments_on_my_markets: 'Comments on your markets',
- all_answers_on_my_markets: 'Answers on your markets',
- subsidized_your_market: 'Your market was subsidized',
- tips_on_your_markets: 'Likes on your markets',
- },
+ subscriptionTypes: [
+ 'your_contract_closed',
+ 'all_comments_on_my_markets',
+ 'all_answers_on_my_markets',
+ 'subsidized_your_market',
+ 'tips_on_your_markets',
+ ],
}
const bonuses: SectionData = {
label: 'Bonuses',
- subscriptionTypeToDescription: {
- betting_streaks: 'Prediction streak bonuses',
- referral_bonuses: 'Referral bonuses from referring users',
- unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets',
- },
+ subscriptionTypes: [
+ 'betting_streaks',
+ 'referral_bonuses',
+ 'unique_bettors_on_your_contract',
+ ],
}
const otherBalances: SectionData = {
label: 'Other',
- subscriptionTypeToDescription: {
- loan_income: 'Automatic loans from your profitable bets',
- limit_order_fills: 'Limit order fills',
- tips_on_your_comments: 'Tips on your comments',
- },
+ subscriptionTypes: [
+ 'loan_income',
+ 'limit_order_fills',
+ 'tips_on_your_comments',
+ ],
}
const userInteractions: SectionData = {
label: 'Users',
- subscriptionTypeToDescription: {
- tagged_user: 'A user tagged you',
- on_new_follow: 'Someone followed you',
- contract_from_followed_user: 'New markets created by users you follow',
- },
+ subscriptionTypes: [
+ 'tagged_user',
+ 'on_new_follow',
+ 'contract_from_followed_user',
+ ],
}
const generalOther: SectionData = {
label: 'Other',
- subscriptionTypeToDescription: {
- trending_markets: 'Weekly interesting markets',
- thank_you_for_purchases: 'Thank you notes for your purchases',
- onboarding_flow: 'Explanatory emails to help you get started',
- // profit_loss_updates: 'Weekly profit/loss updates',
- },
+ subscriptionTypes: [
+ 'trending_markets',
+ 'thank_you_for_purchases',
+ 'onboarding_flow',
+ ],
}
function NotificationSettingLine(props: {
description: string
- subscriptionTypeKey: keyof notification_subscription_types
+ subscriptionTypeKey: notification_preference
destinations: notification_destination_types[]
}) {
const { description, subscriptionTypeKey, destinations } = props
@@ -237,9 +229,7 @@ export function NotificationSettings(props: {
)
}
- const getUsersSavedPreference = (
- key: keyof notification_subscription_types
- ) => {
+ const getUsersSavedPreference = (key: notification_preference) => {
return privateUser.notificationPreferences[key] ?? []
}
@@ -248,17 +238,17 @@ export function NotificationSettings(props: {
data: SectionData
}) {
const { icon, data } = props
- const { label, subscriptionTypeToDescription } = data
+ const { label, subscriptionTypes } = data
const expand =
navigateToSection &&
- Object.keys(subscriptionTypeToDescription).includes(navigateToSection)
+ Object.keys(subscriptionTypes).includes(navigateToSection)
// Not sure how to prevent re-render (and collapse of an open section)
// due to a private user settings change. Just going to persist expanded state here
const [expanded, setExpanded] = usePersistentState(expand ?? false, {
key:
'NotificationsSettingsSection-' +
- Object.keys(subscriptionTypeToDescription).join('-'),
+ Object.keys(subscriptionTypes).join('-'),
store: storageStore(safeLocalStorage()),
})
@@ -287,13 +277,13 @@ export function NotificationSettings(props: {
)}
- {Object.entries(subscriptionTypeToDescription).map(([key, value]) => (
+ {subscriptionTypes.map((subType) => (
))}