Allow one-click unsubscribe, slight refactor

This commit is contained in:
Ian Philips 2022-09-14 17:17:32 -06:00
parent 68b0539fc1
commit 3efd968058
25 changed files with 723 additions and 749 deletions

View File

@ -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<notification_reason_types, keyof notification_subscription_types>
> = {
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&section=${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`,
},
}

View File

@ -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<notification_reason_types, notification_preference>
> = {
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&section=${subscriptionType}`,
}
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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)

View File

@ -1,321 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Manifold Markets 7th Day Anniversary Gift!</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing:normal;background-color:#F4F4F4;">
<div style="background-color:#F4F4F4;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tbody>
<tr>
<td align="center"
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.gif"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
width="550"></a></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Hi {{name}},</span></p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
using Manifold Markets. Running low
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
</div>
</td>
</tr>
<tr>
<td>
<p></p>
</td>
</tr>
<tr>
<td align="center">
<table cellspacing="0" cellpadding="0">
<tr>
<td>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 2px;" bgcolor="#4337c9">
<a href="{{manalink}}" target="_blank"
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
Claim M$500
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
you know, besides making correct predictions, there are
plenty of other ways to earn mana?</span></p>
<ul>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Receiving
tips on comments</span></li>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Unique
trader bonus for each user who bets on your
markets</span></li>
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
class="link-build-content" style="color:inherit;; text-decoration: none;"
target="_blank" href="https://manifold.markets/referrals"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Referring
friends</u></span></a></span></li>
<li style="line-height:23px;"><a class="link-build-content"
style="color:inherit;; text-decoration: none;" target="_blank"
href="https://manifold.markets/group/bugs?s=most-traded"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Reporting
bugs</u></span></a><span style="font-family:Arial, sans-serif;font-size:18px;">
and </span><a class="link-build-content" style="color:inherit;; text-decoration: none;"
target="_blank"
href="https://manifold.markets/group/manifold-features-25bad7c7792e/chat?s=most-traded"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>giving
feedback</u></span></a></li>
</ul>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;">&nbsp;</p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
</p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">David
from Manifold</span></p>
<p class="text-build-content" data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-bottom: 10px;">&nbsp;</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
data-testid="3Q8BP69fq"></a></li>
</ul>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;">&nbsp;</p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span></p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">David from Manifold</span></p>
<p class="text-build-content" data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-bottom: 10px;">&nbsp;</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
<p style="margin: 10px 0;">This e-mail has been sent to {{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -494,7 +494,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>

View File

@ -443,7 +443,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>

View File

@ -529,7 +529,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -369,7 +369,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -487,7 +487,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -369,7 +369,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -470,7 +470,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -502,7 +502,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -318,7 +318,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>

View File

@ -376,7 +376,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -480,7 +480,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>

View File

@ -283,7 +283,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>

View File

@ -218,7 +218,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>

View File

@ -290,7 +290,7 @@
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>

View File

@ -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&section=${
'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&section=${
'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&section=${
'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&section=${
'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&section=${
'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

View File

@ -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

View File

@ -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) {

View File

@ -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<PrivateUser> = {
...(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(
`
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Manifold Markets 7th Day Anniversary Gift!</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing:normal;background-color:#F4F4F4;">
<div style="background-color:#F4F4F4;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto" src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Hello!</span></p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y">
<span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
${email} has been unsubscribed from email notifications related to:
</span>
<br/>
<br/>
<span style="font-weight: bold; color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">${NOTIFICATION_DESCRIPTIONS[notificationSubscriptionType].detailed}.</span>
</p>
<br/>
<br/>
<br/>
<span>Click
<a href='https://manifold.markets/notifications?tab=settings'>here</a>
to manage the rest of your notification settings.
</span>
</div>
</td>
</tr>
<tr>
<td>
<p></p>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
`
)
},
}

View File

@ -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<keyof notification_subscription_types> = [
const emailsEnabled: Array<notification_preference> = [
'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<keyof notification_subscription_types> = [
const browserDisabled: Array<notification_preference> = [
'trending_markets',
'profit_loss_updates',
'onboarding_flow',
@ -83,91 +84,82 @@ export function NotificationSettings(props: {
type SectionData = {
label: string
subscriptionTypeToDescription: {
[key in keyof Partial<notification_subscription_types>]: string
}
subscriptionTypes: Partial<notification_preference>[]
}
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: {
)}
</Row>
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
{Object.entries(subscriptionTypeToDescription).map(([key, value]) => (
{subscriptionTypes.map((subType) => (
<NotificationSettingLine
subscriptionTypeKey={key as keyof notification_subscription_types}
subscriptionTypeKey={subType as notification_preference}
destinations={getUsersSavedPreference(
key as keyof notification_subscription_types
subType as notification_preference
)}
description={value}
description={NOTIFICATION_DESCRIPTIONS[subType].simple}
/>
))}
</Col>