Add referral bonuses and notifications for them
This commit is contained in:
parent
60e41a7f79
commit
359fef7ab4
|
@ -33,6 +33,7 @@ export type notification_source_types =
|
|||
| 'tip'
|
||||
| 'admin_message'
|
||||
| 'group'
|
||||
| 'user'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
|
@ -53,3 +54,5 @@ export type notification_reason_types =
|
|||
| 'on_new_follow'
|
||||
| 'you_follow_user'
|
||||
| 'added_you_to_group'
|
||||
| 'you_referred_user'
|
||||
| 'user_joined_to_bet_on_your_market'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||
type AnyTxnType = Donation | Tip | Manalink
|
||||
type AnyTxnType = Donation | Tip | Manalink | Referral
|
||||
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
||||
|
||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||
|
@ -16,7 +16,7 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' // | 'BET'
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET'
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
||||
|
@ -46,6 +46,13 @@ type Manalink = {
|
|||
category: 'MANALINK'
|
||||
}
|
||||
|
||||
type Referral = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'REFERRAL'
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
export type TipTxn = Txn & Tip
|
||||
export type ManalinkTxn = Txn & Manalink
|
||||
export type ReferralTxn = Txn & Referral
|
||||
|
|
|
@ -33,6 +33,9 @@ export type User = {
|
|||
followerCountCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
|
||||
referredByUserId?: string
|
||||
referredByContractId?: string
|
||||
}
|
||||
|
||||
export const STARTING_BALANCE = 1000
|
||||
|
|
|
@ -16,7 +16,7 @@ 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']);
|
||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByUserId']);
|
||||
}
|
||||
|
||||
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
|
||||
|
|
|
@ -5,6 +5,15 @@ import { User } from 'common/user'
|
|||
import { Manalink } from 'common/manalink'
|
||||
import { runTxn, TxnData } from './transact'
|
||||
|
||||
// if the userId claiming the manalink just signed up:
|
||||
// - the manalink funds come from the house
|
||||
// -
|
||||
// if the manalink has a refergroupid:
|
||||
// - add the user to the group
|
||||
// in on-create-txn detect if it was a referral bonus, create notification for both parties
|
||||
// necessary for manalink: authentication from fromUser
|
||||
// - sending user's funds
|
||||
// - allowing new user to join closed group
|
||||
export const claimManalink = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
.https.onCall(async (slug: string, context) => {
|
||||
|
|
|
@ -68,6 +68,7 @@ export const createNotification = async (
|
|||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||
// TODO: move away from sourceContractTitle to sourceTitle
|
||||
sourceContractTitle: sourceContract?.question,
|
||||
// TODO: move away from sourceContractSlug to sourceSlug
|
||||
sourceContractSlug: sourceContract?.slug,
|
||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
||||
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
||||
|
@ -252,44 +253,62 @@ 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 getUsersToNotify = async () => {
|
||||
const userToReasonTexts: user_to_reason_texts = {}
|
||||
// The following functions modify the userToReasonTexts object in place.
|
||||
if (sourceContract) {
|
||||
if (
|
||||
sourceType === 'comment' ||
|
||||
sourceType === 'answer' ||
|
||||
(sourceType === 'contract' &&
|
||||
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
||||
) {
|
||||
if (sourceType === 'comment') {
|
||||
if (relatedUserId && relatedSourceType)
|
||||
await notifyRepliedUsers(
|
||||
userToReasonTexts,
|
||||
relatedUserId,
|
||||
relatedSourceType
|
||||
)
|
||||
if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText)
|
||||
}
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
||||
await notifyLiquidityProviders(userToReasonTexts, sourceContract)
|
||||
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
|
||||
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
||||
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
||||
await notifyUsersFollowers(userToReasonTexts)
|
||||
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
||||
force: true,
|
||||
})
|
||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||
}
|
||||
} else if (sourceType === 'follow' && relatedUserId) {
|
||||
if (sourceType === 'follow' && relatedUserId) {
|
||||
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
||||
} 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.
|
||||
if (!sourceContract) return userToReasonTexts
|
||||
if (
|
||||
sourceType === 'comment' ||
|
||||
sourceType === 'answer' ||
|
||||
(sourceType === 'contract' &&
|
||||
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
||||
) {
|
||||
if (sourceType === 'comment') {
|
||||
if (relatedUserId && relatedSourceType)
|
||||
await notifyRepliedUsers(
|
||||
userToReasonTexts,
|
||||
relatedUserId,
|
||||
relatedSourceType
|
||||
)
|
||||
if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText)
|
||||
}
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
||||
await notifyLiquidityProviders(userToReasonTexts, sourceContract)
|
||||
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
|
||||
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
||||
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
||||
await notifyUsersFollowers(userToReasonTexts)
|
||||
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
||||
force: true,
|
||||
})
|
||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||
}
|
||||
return userToReasonTexts
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export * from './on-unfollow-user'
|
|||
export * from './on-create-liquidity-provision'
|
||||
export * from './on-update-group'
|
||||
export * from './on-create-group'
|
||||
export * from './on-update-user'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
|
|
@ -1,18 +1,102 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { User } from '../../common/user'
|
||||
import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
||||
import { getValues, getContract } from './utils'
|
||||
import { createNotification } from './create-notification'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
import { ReferralTxn, Txn } from '../../common/txn'
|
||||
import { Contract } from '../../common/contract'
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const onUpdateGroup = functions.firestore
|
||||
export const ReferredUserDescriptionPrefix = 'Referred user id'
|
||||
|
||||
export const onUpdateUser = functions.firestore
|
||||
.document('users/{userId}')
|
||||
.onUpdate(async (change) => {
|
||||
.onUpdate(async (change, context) => {
|
||||
const prevUser = change.before.data() as User
|
||||
const user = change.after.data() as User
|
||||
const { eventId } = context
|
||||
|
||||
// if they're updating their referredId, send the
|
||||
if (prevUser.referredByUserId === user.referredByUserId) {
|
||||
console.log("referredByUserId hasn't changed")
|
||||
return // We only handle referrals right now.
|
||||
}
|
||||
|
||||
await firestore
|
||||
.collection('groups')
|
||||
.doc(group.id)
|
||||
.update({ mostRecentActivityTime: Date.now() })
|
||||
// Only create a referral txn if the user has a referredByUserId
|
||||
if (!user.referredByUserId) {
|
||||
console.log(`Not set: referredByUserId ${user.referredByUserId}`)
|
||||
return
|
||||
}
|
||||
const referredByUserId = user.referredByUserId
|
||||
|
||||
// get user that referred this user
|
||||
const referredByUserDoc = firestore.doc(`users/${referredByUserId}`)
|
||||
const referredByUserSnap = await referredByUserDoc.get()
|
||||
if (!referredByUserSnap.exists) {
|
||||
console.log(`User ${referredByUserId} not found`)
|
||||
return
|
||||
}
|
||||
const referredByUser = referredByUserSnap.data() as User
|
||||
|
||||
let referredByContract: Contract | undefined = undefined
|
||||
if (user.referredByContractId)
|
||||
referredByContract = await getContract(user.referredByContractId)
|
||||
console.log(`referredByContract: ${referredByContract}`)
|
||||
|
||||
const txnQuery = firestore
|
||||
.collection('txns')
|
||||
.where('toId', '==', referredByUserId)
|
||||
.where('category', '==', 'REFERRAL')
|
||||
// find txns to this user, find if they have a txn that contains a BANK category txn that contains 'referred by' the current username in the description
|
||||
const referralTxns = await getValues<Txn>(txnQuery).catch((err) => {
|
||||
console.error('error getting txns:', err)
|
||||
return []
|
||||
})
|
||||
if (referralTxns.map((txn) => txn.description).includes(referredByUserId)) {
|
||||
console.log('found referral txn with the same details, aborting')
|
||||
return
|
||||
}
|
||||
console.log('creating referral txns')
|
||||
// TODO: change this to prod id
|
||||
const fromId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' //HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
const referralAmount = 500
|
||||
|
||||
await firestore.runTransaction(async (transaction) => {
|
||||
// if they're updating their referredId, create a txn for both
|
||||
const txn: ReferralTxn = {
|
||||
id: eventId,
|
||||
createdTime: Date.now(),
|
||||
fromId,
|
||||
fromType: 'BANK',
|
||||
toId: referredByUserId,
|
||||
toType: 'USER',
|
||||
amount: referralAmount,
|
||||
token: 'M$',
|
||||
category: 'REFERRAL',
|
||||
description: `${ReferredUserDescriptionPrefix}: ${user.id} for ${referralAmount}`,
|
||||
}
|
||||
|
||||
const txnDoc = await firestore.collection(`txns/`).doc(txn.id)
|
||||
await transaction.set(txnDoc, removeUndefinedProps(txn))
|
||||
console.log('created referral with txn id:', txn.id)
|
||||
transaction.update(referredByUserDoc, {
|
||||
balance: referredByUser.balance + referralAmount,
|
||||
totalDeposits: referredByUser.totalDeposits + referralAmount,
|
||||
})
|
||||
|
||||
await createNotification(
|
||||
user.id,
|
||||
'user',
|
||||
'updated',
|
||||
user,
|
||||
eventId,
|
||||
txn.amount.toString(),
|
||||
referredByContract,
|
||||
'user',
|
||||
referredByUser.id,
|
||||
referredByContract?.slug,
|
||||
referredByContract?.question
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -29,6 +29,9 @@ import { groupPath } from 'web/lib/firebase/groups'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { DAY_MS } from 'common/util/time'
|
||||
import { useGroupsWithContract } from 'web/hooks/use-group'
|
||||
import { CopyLinkButton } from 'web/components/copy-link-button'
|
||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
|
||||
export type ShowTime = 'resolve-date' | 'close-date'
|
||||
|
||||
|
@ -130,6 +133,7 @@ export function ContractDetails(props: {
|
|||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||
// Find a group that this contract id is in
|
||||
const groups = useGroupsWithContract(contract.id)
|
||||
const user = useUser()
|
||||
return (
|
||||
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||
<Row className="items-center gap-2">
|
||||
|
@ -192,6 +196,11 @@ export function ContractDetails(props: {
|
|||
|
||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||
</Row>
|
||||
<ShareIconButton
|
||||
contract={contract}
|
||||
toastClassName={'sm:-left-40 -left-24 min-w-[250%]'}
|
||||
username={user?.username}
|
||||
/>
|
||||
|
||||
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
||||
</Row>
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
getBinaryProbPercent,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import { LiquidityPanel } from '../liquidity-panel'
|
||||
import { CopyLinkButton } from '../copy-link-button'
|
||||
import { Col } from '../layout/col'
|
||||
import { Modal } from '../layout/modal'
|
||||
import { Row } from '../layout/row'
|
||||
|
@ -23,6 +22,9 @@ import { TweetButton } from '../tweet-button'
|
|||
import { InfoTooltip } from '../info-tooltip'
|
||||
import { TagsInput } from 'web/components/tags-input'
|
||||
|
||||
export const ContractDetailsButtonClassName =
|
||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||
|
||||
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||
const { contract, bets } = props
|
||||
|
||||
|
@ -48,13 +50,11 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
return (
|
||||
<>
|
||||
<button
|
||||
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"
|
||||
className={ContractDetailsButtonClassName}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<DotsHorizontalIcon
|
||||
className={clsx(
|
||||
'h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500'
|
||||
)}
|
||||
className={clsx('h-6 w-6 flex-shrink-0')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
@ -66,10 +66,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
<div>Share</div>
|
||||
|
||||
<Row className="justify-start gap-4">
|
||||
<CopyLinkButton
|
||||
contract={contract}
|
||||
toastClassName={'sm:-left-10 -left-4 min-w-[250%]'}
|
||||
/>
|
||||
<TweetButton
|
||||
className="self-start"
|
||||
tweetText={getTweetText(contract, false)}
|
||||
|
|
49
web/components/share-icon-button.tsx
Normal file
49
web/components/share-icon-button.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React, { useState } from 'react'
|
||||
import { ShareIcon } from '@heroicons/react/outline'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
import { copyToClipboard } from 'web/lib/util/copy'
|
||||
import { contractPath } from 'web/lib/firebase/contracts'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { ContractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog'
|
||||
|
||||
function copyContractWithReferral(contract: Contract, username?: string) {
|
||||
const postFix =
|
||||
username && contract.creatorUsername !== username
|
||||
? '?referrer=' + username
|
||||
: ''
|
||||
copyToClipboard(
|
||||
`https://${ENV_CONFIG.domain}${contractPath(contract)}${postFix}`
|
||||
)
|
||||
}
|
||||
|
||||
export function ShareIconButton(props: {
|
||||
contract: Contract
|
||||
buttonClassName?: string
|
||||
toastClassName?: string
|
||||
username?: string
|
||||
}) {
|
||||
const { contract, buttonClassName, toastClassName, username } = props
|
||||
const [showToast, setShowToast] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex-shrink-0">
|
||||
<button
|
||||
className={clsx(ContractDetailsButtonClassName, buttonClassName)}
|
||||
onClick={() => {
|
||||
copyContractWithReferral(contract, username)
|
||||
track('copy share link')
|
||||
setShowToast(true)
|
||||
setTimeout(() => setShowToast(false), 2000)
|
||||
}}
|
||||
>
|
||||
<ShareIcon className="h-[24px] w-5" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{showToast && <ToastClipboard className={toastClassName} />}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -94,3 +94,13 @@ export async function getGroupsWithContractId(
|
|||
const groups = await getValues<Group>(q)
|
||||
setGroups(groups)
|
||||
}
|
||||
|
||||
export async function addUserToGroupViaSlug(groupSlug: string, userId: string) {
|
||||
// get group to get the member ids
|
||||
const group = await getGroupBySlug(groupSlug)
|
||||
if (group && !group.memberIds.includes(userId))
|
||||
return await updateGroup(group, {
|
||||
memberIds: [userId, ...group.memberIds],
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -32,6 +32,9 @@ 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 { removeUndefinedProps } from 'common/util/object'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export type { User }
|
||||
|
||||
|
@ -88,12 +91,83 @@ 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'
|
||||
|
||||
// used to avoid weird race condition
|
||||
let createUserPromise: Promise<User | null> | undefined = undefined
|
||||
|
||||
const warmUpCreateUser = throttle(createUser, 5000 /* ms */)
|
||||
|
||||
export function writeReferralInfo(
|
||||
contractUsername: string,
|
||||
contractId?: string,
|
||||
referralUsername?: string,
|
||||
groupSlug?: string
|
||||
) {
|
||||
const local = safeLocalStorage()
|
||||
const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)
|
||||
// Write the first referral username we see.
|
||||
if (!cachedReferralUser)
|
||||
local?.setItem(
|
||||
CACHED_REFERRAL_USERNAME_KEY,
|
||||
referralUsername || contractUsername
|
||||
)
|
||||
|
||||
// If an explicit referral query is passed, overwrite the cached referral username.
|
||||
if (referralUsername)
|
||||
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)
|
||||
|
||||
// Write the first contract id that we see.
|
||||
const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
||||
if (!cachedReferralContract && contractId)
|
||||
local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)
|
||||
}
|
||||
|
||||
async function setCachedReferralInfoForUser(user: User | null) {
|
||||
if (!user || user.referredByUserId) return
|
||||
// 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
|
||||
|
||||
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)
|
||||
|
||||
// get user via username
|
||||
if (cachedReferralUsername)
|
||||
getUserByUsername(cachedReferralUsername).then((referredByUser) => {
|
||||
if (!referredByUser) return
|
||||
// update user's referralId
|
||||
updateUser(
|
||||
user.id,
|
||||
removeUndefinedProps({
|
||||
referredByUserId: referredByUser.id,
|
||||
referredByContractId: cachedReferralContractId
|
||||
? cachedReferralContractId
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
.then((data) => console.log('done!', data))
|
||||
.catch(console.error)
|
||||
})
|
||||
|
||||
if (cachedReferralGroupSlug)
|
||||
addUserToGroupViaSlug(cachedReferralGroupSlug, user.id)
|
||||
|
||||
local?.removeItem(CACHED_REFERRAL_GROUP_SLUG_KEY)
|
||||
local?.removeItem(CACHED_REFERRAL_USERNAME_KEY)
|
||||
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
||||
}
|
||||
|
||||
export function listenForLogin(onUser: (user: User | null) => void) {
|
||||
const local = safeLocalStorage()
|
||||
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
||||
|
@ -117,6 +191,7 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
|||
// Persist to local storage, to reduce login blink next time.
|
||||
// Note: Cap on localStorage size is ~5mb
|
||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||
setCachedReferralInfoForUser(user)
|
||||
} else {
|
||||
// User logged out; reset to null
|
||||
onUser(null)
|
||||
|
@ -126,6 +201,21 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
|||
})
|
||||
}
|
||||
|
||||
// create new signup banner on markets for referrees
|
||||
// markets that someone signs up to bet on are referred to the market's creator
|
||||
// it would be great if you could invite someone to your group/allowlist their email
|
||||
// invite a user to your group with a unique link
|
||||
// pass the referrer id to this function
|
||||
// add the referrer id to the new user's field
|
||||
// in on-update-user check if the new user had a referrer,add the new user's ide to the referrees field
|
||||
// create a txn for both
|
||||
// create on-create-txn that checks why it was created, create a notification for it
|
||||
|
||||
// manalinks no required for:
|
||||
// referral from: user to site, user to another user's market, open group, implicit from market creator
|
||||
// from groups or market page, get the manalink id from the url and pass it here
|
||||
// then claim it upon successful signup
|
||||
//
|
||||
export async function firebaseLogin() {
|
||||
const provider = new GoogleAuthProvider()
|
||||
return signInWithPopup(auth, provider)
|
||||
|
|
|
@ -10,7 +10,13 @@ import { useUser } from 'web/hooks/use-user'
|
|||
import { ResolutionPanel } from 'web/components/resolution-panel'
|
||||
import { Title } from 'web/components/title'
|
||||
import { Spacer } from 'web/components/layout/spacer'
|
||||
import { listUsers, User } from 'web/lib/firebase/users'
|
||||
import {
|
||||
getUserByUsername,
|
||||
listUsers,
|
||||
updateUser,
|
||||
User,
|
||||
writeReferralInfo,
|
||||
} from 'web/lib/firebase/users'
|
||||
import {
|
||||
Contract,
|
||||
getContractFromSlug,
|
||||
|
@ -42,6 +48,9 @@ import { useBets } from 'web/hooks/use-bets'
|
|||
import { AlertBox } from 'web/components/alert-box'
|
||||
import { useTracking } from 'web/hooks/use-tracking'
|
||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||
import { useRouter } from 'next/router'
|
||||
import dayjs from 'dayjs'
|
||||
import { addUserToGroupViaSlug, getGroup } from 'web/lib/firebase/groups'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
|
@ -147,6 +156,14 @@ export function ContractPageContent(
|
|||
|
||||
const ogCardProps = getOpenGraphProps(contract)
|
||||
|
||||
const router = useRouter()
|
||||
const { referrer, username } = router.query as {
|
||||
username: string
|
||||
referrer?: string
|
||||
}
|
||||
if (!user && router.isReady)
|
||||
writeReferralInfo(username, contract.id, referrer)
|
||||
|
||||
const rightSidebar = hasSidePanel ? (
|
||||
<Col className="gap-4">
|
||||
{allowTrade &&
|
||||
|
|
|
@ -13,7 +13,12 @@ import {
|
|||
} from 'web/lib/firebase/groups'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { UserLink } from 'web/components/user-page'
|
||||
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
|
||||
import {
|
||||
firebaseLogin,
|
||||
getUser,
|
||||
User,
|
||||
writeReferralInfo,
|
||||
} from 'web/lib/firebase/users'
|
||||
import { Spacer } from 'web/components/layout/spacer'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
|
@ -146,6 +151,12 @@ export default function GroupPage(props: {
|
|||
const isCreator = user && group && user.id === group.creatorId
|
||||
const isMember = user && memberIds.includes(user.id)
|
||||
|
||||
const { referrer } = router.query as {
|
||||
referrer?: string
|
||||
}
|
||||
if (!user && router.isReady)
|
||||
writeReferralInfo(creator.username, undefined, referrer, group.slug)
|
||||
|
||||
const rightSidebar = (
|
||||
<Col className="mt-6 hidden xl:block">
|
||||
<GroupOverview
|
||||
|
|
|
@ -14,9 +14,6 @@ import { Title } from 'web/components/title'
|
|||
import { doc, updateDoc } from 'firebase/firestore'
|
||||
import { db } from 'web/lib/firebase/init'
|
||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||
import { Answer } from 'common/answer'
|
||||
import { Comment } from 'web/lib/firebase/comments'
|
||||
import { getValue } from 'web/lib/firebase/utils'
|
||||
import Custom404 from 'web/pages/404'
|
||||
import { UserLink } from 'web/components/user-page'
|
||||
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
||||
|
@ -38,7 +35,6 @@ import {
|
|||
NotificationGroup,
|
||||
usePreferredGroupedNotifications,
|
||||
} from 'web/hooks/use-notifications'
|
||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
@ -182,7 +178,7 @@ function NotificationGroupItem(props: {
|
|||
className?: string
|
||||
}) {
|
||||
const { notificationGroup, className } = props
|
||||
const { sourceContractId, notifications } = notificationGroup
|
||||
const { notifications } = notificationGroup
|
||||
const {
|
||||
sourceContractTitle,
|
||||
sourceContractSlug,
|
||||
|
@ -191,28 +187,6 @@ function NotificationGroupItem(props: {
|
|||
const numSummaryLines = 3
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [contract, setContract] = useState<Contract | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
sourceContractTitle &&
|
||||
sourceContractSlug &&
|
||||
sourceContractCreatorUsername
|
||||
)
|
||||
return
|
||||
if (sourceContractId) {
|
||||
getContractFromId(sourceContractId)
|
||||
.then((contract) => {
|
||||
if (contract) setContract(contract)
|
||||
})
|
||||
.catch((e) => console.log(e))
|
||||
}
|
||||
}, [
|
||||
sourceContractCreatorUsername,
|
||||
sourceContractId,
|
||||
sourceContractSlug,
|
||||
sourceContractTitle,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationsAsSeen(notifications)
|
||||
|
@ -240,20 +214,20 @@ function NotificationGroupItem(props: {
|
|||
onClick={() => setExpanded(!expanded)}
|
||||
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||
>
|
||||
{sourceContractTitle || contract ? (
|
||||
{sourceContractTitle ? (
|
||||
<span>
|
||||
{'Activity on '}
|
||||
<a
|
||||
href={
|
||||
sourceContractCreatorUsername
|
||||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
: `/${contract?.creatorUsername}/${contract?.slug}`
|
||||
: ''
|
||||
}
|
||||
className={
|
||||
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
}
|
||||
>
|
||||
{sourceContractTitle || contract?.question}
|
||||
{sourceContractTitle}
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
|
@ -515,7 +489,6 @@ function NotificationItem(props: {
|
|||
const { notification, justSummary } = props
|
||||
const {
|
||||
sourceType,
|
||||
sourceContractId,
|
||||
sourceId,
|
||||
sourceUserName,
|
||||
sourceUserAvatarUrl,
|
||||
|
@ -534,60 +507,15 @@ function NotificationItem(props: {
|
|||
|
||||
const [defaultNotificationText, setDefaultNotificationText] =
|
||||
useState<string>('')
|
||||
const [contract, setContract] = useState<Contract | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!sourceContractId ||
|
||||
(sourceContractSlug && sourceContractCreatorUsername)
|
||||
)
|
||||
return
|
||||
getContractFromId(sourceContractId)
|
||||
.then((contract) => {
|
||||
if (contract) setContract(contract)
|
||||
})
|
||||
.catch((e) => console.log(e))
|
||||
}, [
|
||||
sourceContractCreatorUsername,
|
||||
sourceContractId,
|
||||
sourceContractSlug,
|
||||
sourceContractTitle,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceText) {
|
||||
setDefaultNotificationText(sourceText)
|
||||
} else if (!contract || !sourceContractId || !sourceId) return
|
||||
else if (
|
||||
sourceType === 'answer' ||
|
||||
sourceType === 'comment' ||
|
||||
sourceType === 'contract'
|
||||
) {
|
||||
try {
|
||||
parseOldStyleNotificationText(
|
||||
sourceId,
|
||||
sourceContractId,
|
||||
sourceType,
|
||||
sourceUpdateType,
|
||||
setDefaultNotificationText,
|
||||
contract
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
} else if (reasonText) {
|
||||
// Handle arbitrary notifications with reason text here.
|
||||
setDefaultNotificationText(reasonText)
|
||||
}
|
||||
}, [
|
||||
contract,
|
||||
reasonText,
|
||||
sourceContractId,
|
||||
sourceId,
|
||||
sourceText,
|
||||
sourceType,
|
||||
sourceUpdateType,
|
||||
])
|
||||
}, [reasonText, sourceText])
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationsAsSeen([notification])
|
||||
|
@ -596,14 +524,16 @@ function NotificationItem(props: {
|
|||
function getSourceUrl() {
|
||||
if (sourceType === 'follow') return `/${sourceUserUsername}`
|
||||
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
|
||||
if (
|
||||
sourceContractCreatorUsername &&
|
||||
sourceContractSlug &&
|
||||
sourceType === 'user'
|
||||
)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||
sourceId ?? ''
|
||||
)}`
|
||||
if (!contract) return ''
|
||||
return `/${contract.creatorUsername}/${
|
||||
contract.slug
|
||||
}#${getSourceIdForLinkComponent(sourceId ?? '')}`
|
||||
}
|
||||
|
||||
function getSourceIdForLinkComponent(sourceId: string) {
|
||||
|
@ -619,38 +549,6 @@ function NotificationItem(props: {
|
|||
}
|
||||
}
|
||||
|
||||
async function parseOldStyleNotificationText(
|
||||
sourceId: string,
|
||||
sourceContractId: string,
|
||||
sourceType: 'answer' | 'comment' | 'contract',
|
||||
sourceUpdateType: notification_source_update_types | undefined,
|
||||
setText: (text: string) => void,
|
||||
contract: Contract
|
||||
) {
|
||||
if (sourceType === 'contract') {
|
||||
if (
|
||||
isNotificationAboutContractResolution(
|
||||
sourceType,
|
||||
sourceUpdateType,
|
||||
contract
|
||||
) &&
|
||||
contract.resolution
|
||||
)
|
||||
setText(contract.resolution)
|
||||
else setText(contract.question)
|
||||
} else if (sourceType === 'answer') {
|
||||
const answer = await getValue<Answer>(
|
||||
doc(db, `contracts/${sourceContractId}/answers/`, sourceId)
|
||||
)
|
||||
setText(answer?.text ?? '')
|
||||
} else {
|
||||
const comment = await getValue<Comment>(
|
||||
doc(db, `contracts/${sourceContractId}/comments/`, sourceId)
|
||||
)
|
||||
setText(comment?.text ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
if (justSummary) {
|
||||
return (
|
||||
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
||||
|
@ -669,13 +567,13 @@ function NotificationItem(props: {
|
|||
sourceType,
|
||||
reason,
|
||||
sourceUpdateType,
|
||||
contract,
|
||||
undefined,
|
||||
true
|
||||
).replace(' on', '')}
|
||||
</span>
|
||||
<div className={'ml-1 text-black'}>
|
||||
<NotificationTextLabel
|
||||
contract={contract}
|
||||
contract={null}
|
||||
defaultText={defaultNotificationText}
|
||||
className={'line-clamp-1'}
|
||||
notification={notification}
|
||||
|
@ -717,7 +615,9 @@ function NotificationItem(props: {
|
|||
sourceType,
|
||||
reason,
|
||||
sourceUpdateType,
|
||||
contract
|
||||
undefined,
|
||||
false,
|
||||
sourceSlug
|
||||
)}
|
||||
<a
|
||||
href={
|
||||
|
@ -725,13 +625,13 @@ function NotificationItem(props: {
|
|||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
: sourceType === 'group' && sourceSlug
|
||||
? `${groupPath(sourceSlug)}`
|
||||
: `/${contract?.creatorUsername}/${contract?.slug}`
|
||||
: ''
|
||||
}
|
||||
className={
|
||||
'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
}
|
||||
>
|
||||
{contract?.question || sourceContractTitle || sourceTitle}
|
||||
{sourceContractTitle || sourceTitle}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
@ -752,7 +652,7 @@ function NotificationItem(props: {
|
|||
</Row>
|
||||
<div className={'mt-1 ml-1 md:text-base'}>
|
||||
<NotificationTextLabel
|
||||
contract={contract}
|
||||
contract={null}
|
||||
defaultText={defaultNotificationText}
|
||||
notification={notification}
|
||||
/>
|
||||
|
@ -811,6 +711,16 @@ function NotificationTextLabel(props: {
|
|||
</span>
|
||||
)
|
||||
}
|
||||
} else if (sourceType === 'user' && sourceText) {
|
||||
return (
|
||||
<span>
|
||||
As a thank you, we sent you{' '}
|
||||
<span className="text-primary">
|
||||
{formatMoney(parseInt(sourceText))}
|
||||
</span>
|
||||
!
|
||||
</span>
|
||||
)
|
||||
} else if (sourceType === 'liquidity' && sourceText) {
|
||||
return (
|
||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||
|
@ -829,7 +739,8 @@ function getReasonForShowingNotification(
|
|||
reason: notification_reason_types,
|
||||
sourceUpdateType: notification_source_update_types | undefined,
|
||||
contract: Contract | undefined | null,
|
||||
simple?: boolean
|
||||
simple?: boolean,
|
||||
sourceSlug?: string
|
||||
) {
|
||||
let reasonText: string
|
||||
switch (source) {
|
||||
|
@ -883,6 +794,12 @@ function getReasonForShowingNotification(
|
|||
case 'group':
|
||||
reasonText = 'added you to the group'
|
||||
break
|
||||
case 'user':
|
||||
if (sourceSlug && reason === 'user_joined_to_bet_on_your_market')
|
||||
reasonText = 'joined to bet on your market'
|
||||
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||
else reasonText = 'joined because of you'
|
||||
break
|
||||
default:
|
||||
reasonText = ''
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user