From 39c38a669e3a104be9ee2c0fd3da6f91615ee195 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 18 Jul 2022 10:40:44 -0600 Subject: [PATCH] Referrals bug fix and attribute group --- common/notification.ts | 1 + common/user.ts | 1 + firestore.rules | 4 +- functions/src/create-notification.ts | 65 +++++++++++++++++++++------- functions/src/on-update-user.ts | 25 +++++++---- web/lib/firebase/groups.ts | 6 +-- web/lib/firebase/users.ts | 22 +++++----- web/pages/group/[...slugs]/index.tsx | 2 +- web/pages/notifications.tsx | 10 ++++- 9 files changed, 94 insertions(+), 42 deletions(-) diff --git a/common/notification.ts b/common/notification.ts index 63a44a52..5fd4236b 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -63,3 +63,4 @@ export type notification_reason_types = | 'on_group_you_are_member_of' | 'tip_received' | 'bet_fill' + | 'user_joined_from_your_group_invite' diff --git a/common/user.ts b/common/user.ts index 6eed3bdb..1995ce34 100644 --- a/common/user.ts +++ b/common/user.ts @@ -38,6 +38,7 @@ export type User = { referredByUserId?: string referredByContractId?: string + referredByGroupId?: string lastPingTime?: number } diff --git a/firestore.rules b/firestore.rules index 84c3e990..63df4d16 100644 --- a/firestore.rules +++ b/firestore.rules @@ -22,11 +22,11 @@ service cloud.firestore { allow read; allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId', 'lastPingTime']); + .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime']); // User referral rules allow update: if resource.data.id == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['referredByUserId']) + .hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId']) // only one referral allowed per user && !("referredByUserId" in resource.data) // user can't refer themselves diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 56493043..bf2dd28a 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -253,20 +253,6 @@ export const createNotification = async ( } } - const notifyUserReceivedReferralBonus = async ( - userToReasonTexts: user_to_reason_texts, - relatedUserId: string - ) => { - if (shouldGetNotification(relatedUserId, userToReasonTexts)) - userToReasonTexts[relatedUserId] = { - // If the referrer is the market creator, just tell them they joined to bet on their market - reason: - sourceContract?.creatorId === relatedUserId - ? 'user_joined_to_bet_on_your_market' - : 'you_referred_user', - } - } - const notifyContractCreatorOfUniqueBettorsBonus = async ( userToReasonTexts: user_to_reason_texts, userId: string @@ -284,8 +270,6 @@ export const createNotification = async ( } else if (sourceType === 'group' && relatedUserId) { if (sourceUpdateType === 'created') await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) - } else if (sourceType === 'user' && relatedUserId) { - await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) } // The following functions need sourceContract to be defined. @@ -435,3 +419,52 @@ export const createGroupCommentNotification = async ( } await notificationRef.set(removeUndefinedProps(notification)) } + +export const createReferralNotification = async ( + toUser: User, + referredUser: User, + idempotencyKey: string, + bonusAmount: string, + referredByContract?: Contract, + referredByGroup?: Group +) => { + const notificationRef = firestore + .collection(`/users/${toUser.id}/notifications`) + .doc(idempotencyKey) + const notification: Notification = { + id: idempotencyKey, + userId: toUser.id, + reason: referredByGroup + ? 'user_joined_from_your_group_invite' + : referredByContract?.creatorId === toUser.id + ? 'user_joined_to_bet_on_your_market' + : 'you_referred_user', + createdTime: Date.now(), + isSeen: false, + sourceId: referredUser.id, + sourceType: 'user', + sourceUpdateType: 'updated', + sourceContractId: referredByContract?.id, + sourceUserName: referredUser.name, + sourceUserUsername: referredUser.username, + sourceUserAvatarUrl: referredUser.avatarUrl, + sourceText: bonusAmount, + // Only pass the contract referral details if they weren't referred to a group + sourceContractCreatorUsername: !referredByGroup + ? referredByContract?.creatorUsername + : undefined, + sourceContractTitle: !referredByGroup + ? referredByContract?.question + : undefined, + sourceContractSlug: !referredByGroup ? referredByContract?.slug : undefined, + sourceSlug: referredByGroup + ? groupPath(referredByGroup.slug) + : referredByContract?.slug, + sourceTitle: referredByGroup + ? referredByGroup.name + : referredByContract?.question, + } + await notificationRef.set(removeUndefinedProps(notification)) +} + +const groupPath = (groupSlug: string) => `/group/${groupSlug}` diff --git a/functions/src/on-update-user.ts b/functions/src/on-update-user.ts index 0ace3c53..f5558730 100644 --- a/functions/src/on-update-user.ts +++ b/functions/src/on-update-user.ts @@ -2,11 +2,12 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { REFERRAL_AMOUNT, User } from '../../common/user' import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' -import { createNotification } from './create-notification' +import { createReferralNotification } from './create-notification' import { ReferralTxn } from '../../common/txn' import { Contract } from '../../common/contract' import { LimitBet } from 'common/bet' import { QuerySnapshot } from 'firebase-admin/firestore' +import { Group } from 'common/group' const firestore = admin.firestore() export const onUpdateUser = functions.firestore @@ -54,6 +55,17 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { } console.log(`referredByContract: ${referredByContract}`) + let referredByGroup: Group | undefined = undefined + if (user.referredByGroupId) { + const referredByGroupDoc = firestore.doc( + `groups/${user.referredByGroupId}` + ) + referredByGroup = await transaction + .get(referredByGroupDoc) + .then((snap) => snap.data() as Group) + } + console.log(`referredByGroup: ${referredByGroup}`) + const txns = ( await firestore .collection('txns') @@ -100,18 +112,13 @@ async function handleUserUpdatedReferral(user: User, eventId: string) { totalDeposits: referredByUser.totalDeposits + REFERRAL_AMOUNT, }) - await createNotification( - user.id, - 'user', - 'updated', + await createReferralNotification( + referredByUser, user, eventId, txn.amount.toString(), referredByContract, - 'user', - referredByUser.id, - referredByContract?.slug, - referredByContract?.question + referredByGroup ) }) } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 6dfc1b88..8adb5606 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -89,11 +89,11 @@ export async function getGroupsWithContractId( setGroups(await getValues(q)) } -export async function addUserToGroupViaSlug(groupSlug: string, userId: string) { +export async function addUserToGroupViaId(groupId: string, userId: string) { // get group to get the member ids - const group = await getGroupBySlug(groupSlug) + const group = await getGroup(groupId) if (!group) { - console.error(`Group not found: ${groupSlug}`) + console.error(`Group not found: ${groupId}`) return } return await joinGroup(group, userId) diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 29cc9266..f3242a7e 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -35,7 +35,7 @@ import { feed } from 'common/feed' import { CATEGORY_LIST } from 'common/categories' import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' -import { addUserToGroupViaSlug } from 'web/lib/firebase/groups' +import { addUserToGroupViaId } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' import { randomString } from 'common/util/random' import dayjs from 'dayjs' @@ -99,13 +99,13 @@ export function listenForPrivateUser( const CACHED_USER_KEY = 'CACHED_USER_KEY' const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' -const CACHED_REFERRAL_GROUP_SLUG_KEY = 'CACHED_REFERRAL_GROUP_KEY' +const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY' export function writeReferralInfo( defaultReferrerUsername: string, contractId?: string, referralUsername?: string, - groupSlug?: string + groupId?: string ) { const local = safeLocalStorage() const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) @@ -121,7 +121,7 @@ export function writeReferralInfo( local?.setItem(CACHED_REFERRAL_USERNAME_KEY, referralUsername) // Always write the most recent explicit group invite query value - if (groupSlug) local?.setItem(CACHED_REFERRAL_GROUP_SLUG_KEY, groupSlug) + if (groupId) local?.setItem(CACHED_REFERRAL_GROUP_ID_KEY, groupId) // Write the first contract id that we see. const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY) @@ -134,14 +134,14 @@ async function setCachedReferralInfoForUser(user: User | null) { // if the user wasn't created in the last minute, don't bother const now = dayjs().utc() const userCreatedTime = dayjs(user.createdTime) - if (now.diff(userCreatedTime, 'minute') > 1) return + if (now.diff(userCreatedTime, 'minute') > 5) return const local = safeLocalStorage() const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) const cachedReferralContractId = local?.getItem( CACHED_REFERRAL_CONTRACT_ID_KEY ) - const cachedReferralGroupSlug = local?.getItem(CACHED_REFERRAL_GROUP_SLUG_KEY) + const cachedReferralGroupId = local?.getItem(CACHED_REFERRAL_GROUP_ID_KEY) // get user via username if (cachedReferralUsername) @@ -155,6 +155,9 @@ async function setCachedReferralInfoForUser(user: User | null) { referredByContractId: cachedReferralContractId ? cachedReferralContractId : undefined, + referredByGroupId: cachedReferralGroupId + ? cachedReferralGroupId + : undefined, }) ) .catch((err) => { @@ -165,15 +168,14 @@ async function setCachedReferralInfoForUser(user: User | null) { userId: user.id, referredByUserId: referredByUser.id, referredByContractId: cachedReferralContractId, - referredByGroupSlug: cachedReferralGroupSlug, + referredByGroupId: cachedReferralGroupId, }) }) }) - if (cachedReferralGroupSlug) - addUserToGroupViaSlug(cachedReferralGroupSlug, user.id) + if (cachedReferralGroupId) addUserToGroupViaId(cachedReferralGroupId, user.id) - local?.removeItem(CACHED_REFERRAL_GROUP_SLUG_KEY) + local?.removeItem(CACHED_REFERRAL_GROUP_ID_KEY) local?.removeItem(CACHED_REFERRAL_USERNAME_KEY) local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c914b7f0..3ec688b8 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -161,7 +161,7 @@ export default function GroupPage(props: { referrer?: string } if (!user && router.isReady) - writeReferralInfo(creator.username, undefined, referrer, group?.slug) + writeReferralInfo(creator.username, undefined, referrer, group?.id) }, [user, creator, group, router]) const { width } = useWindowSize() diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 2d4d2f2b..9166109f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -713,8 +713,12 @@ function QuestionOrGroupLink(props: { href={ sourceContractCreatorUsername ? `/${sourceContractCreatorUsername}/${sourceContractSlug}` - : (sourceType === 'group' || sourceType === 'tip') && sourceSlug + : // User's added to group or received a tip there + (sourceType === 'group' || sourceType === 'tip') && sourceSlug ? `${groupPath(sourceSlug)}` + : // User referral via group + sourceSlug?.includes('/group/') + ? `${sourceSlug}` : '' } onClick={() => @@ -745,12 +749,16 @@ function getSourceUrl(notification: Notification) { } = notification if (sourceType === 'follow') return `/${sourceUserUsername}` if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` + // User referral via contract: if ( sourceContractCreatorUsername && sourceContractSlug && sourceType === 'user' ) return `/${sourceContractCreatorUsername}/${sourceContractSlug}` + // User referral: + if (sourceType === 'user' && !sourceContractSlug) + return `/${sourceUserUsername}` if (sourceType === 'tip' && sourceContractSlug) return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}`