Skip to content

Commit

Permalink
One free like per day + purchase likes (manifoldmarkets#2424)
Browse files Browse the repository at this point in the history
* Likes with cost

* Show mana

* Confirmation dialogs

* Include alternate love domain in fromLove calculation

* Get one free like per day

* Request all sign up bonuses if you create a lover

* Make likes clearer
  • Loading branch information
jahooma authored Jan 28, 2024
1 parent bbd109a commit b383f5a
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 44 deletions.
2 changes: 2 additions & 0 deletions backend/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import { shipLovers } from './love/ship-lovers'
import { createManalink } from './create-manalink'
import { requestSignupBonus } from 'api/request-signup-bonus'
import { getLikesAndShips } from './love/get-likes-and-ships'
import { hasFreeLike } from './love/has-free-like'


const allowCorsUnrestricted: RequestHandler = cors({})
Expand Down Expand Up @@ -247,6 +248,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'ship-lovers': shipLovers,
'request-signup-bonus': requestSignupBonus,
'get-likes-and-ships': getLikesAndShips,
'has-free-like': hasFreeLike,
}

Object.entries(handlers).forEach(([path, handler]) => {
Expand Down
4 changes: 2 additions & 2 deletions backend/api/src/create-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { generateAvatarUrl } from 'shared/helpers/generate-and-update-avatar-url
import { getStorage } from 'firebase-admin/storage'
import { DEV_CONFIG } from 'common/envs/dev'
import { PROD_CONFIG } from 'common/envs/prod'
import { RESERVED_PATHS } from 'common/envs/constants'
import { LOVE_DOMAIN, LOVE_DOMAIN_ALTERNATE, RESERVED_PATHS } from 'common/envs/constants'
import { GCPLog, isProd } from 'shared/utils'
import { trackSignupFB } from 'shared/fb-analytics'
import {
Expand Down Expand Up @@ -64,7 +64,7 @@ export const createuser = authEndpoint(async (req, auth, log) => {
const fromLove =
(host?.includes('localhost')
? process.env.IS_MANIFOLD_LOVE === 'true'
: host?.includes('manifold.love')) || undefined
: host?.includes(LOVE_DOMAIN) || host?.includes(LOVE_DOMAIN_ALTERNATE)) || undefined
const ip = getIp(req)
const deviceToken = isTestUser ? randomString(20) : preDeviceToken
const deviceUsedBefore =
Expand Down
29 changes: 29 additions & 0 deletions backend/api/src/love/has-free-like.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const hasFreeLike: APIHandler<'has-free-like'> = async (
_props,
auth
) => {
return {
status: 'success',
hasFreeLike: await getHasFreeLike(auth.uid),
}
}

export const getHasFreeLike = async (userId: string) => {
const pg = createSupabaseDirectClient()

const likeGivenToday = await pg.oneOrNone<object>(
`
select 1
from love_likes
where creator_id = $1
and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' >= (now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date
and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' < ((now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date + interval '1 day')
limit 1
`,
[userId]
)
return !likeGivenToday
}
63 changes: 40 additions & 23 deletions backend/api/src/love/like-lover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createSupabaseClient } from 'shared/supabase/init'
import { APIError, APIHandler } from '../helpers/endpoint'
import { createLoveLikeNotification } from 'shared/create-love-notification'
import { runLikePurchaseTxn } from 'shared/txn/run-like-purchase-txn'
import { getHasFreeLike } from './has-free-like'

export const likeLover: APIHandler<'like-lover'> = async (
props,
Expand All @@ -22,35 +24,50 @@ export const likeLover: APIHandler<'like-lover'> = async (
if (error) {
throw new APIError(500, 'Failed to remove like: ' + error.message)
}
} else {
// Check if like already exists
const existing = await db
.from('love_likes')
.select()
.eq('creator_id', creatorId)
.eq('target_id', targetUserId)
return { status: 'success' }
}

if (existing.data?.length) {
log('Like already exists, do nothing')
return { status: 'success' }
}
// Check if like already exists
const existing = await db
.from('love_likes')
.select()
.eq('creator_id', creatorId)
.eq('target_id', targetUserId)

// Insert the new like
const { data, error } = await db
.from('love_likes')
.insert({
creator_id: creatorId,
target_id: targetUserId,
})
.select()
.single()
if (existing.data?.length) {
log('Like already exists, do nothing')
return { status: 'success' }
}

if (error) {
throw new APIError(500, 'Failed to add like: ' + error.message)
const hasFreeLike = await getHasFreeLike(creatorId)

if (!hasFreeLike) {
// Charge for like.
const { status, message } = await runLikePurchaseTxn(
creatorId,
targetUserId
)

if (status === 'error' && message) {
throw new APIError(400, message)
}
}

// Insert the new like
const { data, error } = await db
.from('love_likes')
.insert({
creator_id: creatorId,
target_id: targetUserId,
})
.select()
.single()

await createLoveLikeNotification(data)
if (error) {
throw new APIError(500, 'Failed to add like: ' + error.message)
}

await createLoveLikeNotification(data)

return { status: 'success' }
}
18 changes: 18 additions & 0 deletions backend/shared/src/txn/run-like-purchase-txn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as admin from 'firebase-admin'
import { runTxn } from './run-txn'
import { LIKE_COST } from 'common/love/constants'

export async function runLikePurchaseTxn(userId: string, targetId: string) {
return admin.firestore().runTransaction(async (fbTransaction) => {
return await runTxn(fbTransaction, {
amount: LIKE_COST,
fromId: userId,
fromType: 'USER',
toId: 'BANK',
toType: 'BANK',
category: 'LIKE_PURCHASE',
token: 'M$',
data: { targetId },
})
})
}
18 changes: 15 additions & 3 deletions common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,16 +656,28 @@ export const API = (_apiTypeCheck = {
method: 'GET',
visibility: 'public',
authed: false,
props: z.object({
userId: z.string(),
}),
props: z
.object({
userId: z.string(),
})
.strict(),
returns: {} as {
status: 'success'
likesReceived: LikeData[]
likesGiven: LikeData[]
ships: ShipData[]
},
},
'has-free-like': {
method: 'GET',
visibility: 'private',
authed: true,
props: z.object({}).strict(),
returns: {} as {
status: 'success'
hasFreeLike: boolean
},
},
} as const)

export type APIPath = keyof typeof API
Expand Down
4 changes: 3 additions & 1 deletion common/src/love/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export const isManifoldLoveContract = (contract: Contract) =>

export const MIN_BET_AMOUNT_FOR_NEW_MATCH = 50

export const MAX_COMPATIBILITY_QUESTION_LENGTH = 240
export const MAX_COMPATIBILITY_QUESTION_LENGTH = 240

export const LIKE_COST = 50
9 changes: 9 additions & 0 deletions common/src/txn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type AnyTxnType =
| ManaPay
| Loan
| PushNotificationBonus
| LikePurchase

export type SourceType =
| 'USER'
Expand Down Expand Up @@ -368,6 +369,13 @@ type PushNotificationBonus = {
token: 'M$'
}

type LikePurchase = {
category: 'LIKE_PURCHASE'
fromType: 'USER'
toType: 'BANK'
token: 'M$'
}

export type DonationTxn = Txn & Donation
export type TipTxn = Txn & Tip
export type ManalinkTxn = Txn & Manalink
Expand Down Expand Up @@ -406,3 +414,4 @@ export type BountyCanceledTxn = Txn & BountyCanceled
export type ManaPayTxn = Txn & ManaPay
export type LoanTxn = Txn & Loan
export type PushNotificationBonusTxn = Txn & PushNotificationBonus
export type LikePurchaseTxn = Txn & LikePurchase
2 changes: 1 addition & 1 deletion love/components/nav/love-bottom-nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function ProfileItem(props: {
const { user, item, touched, setTouched, currentPage, track } = props
const balance = useAnimatedNumber(user?.balance ?? 0)
const lover = useLover()
const manaEnabled = false
const manaEnabled = true
return (
<Link
href={item.href ?? '#'}
Expand Down
2 changes: 1 addition & 1 deletion love/components/nav/love-profile-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ProfileSummary(props: { user: User; className?: string }) {

const [buyModalOpen, setBuyModalOpen] = useState(false)
const balance = useAnimatedNumber(user.balance)
const manaEnabled = false
const manaEnabled = true

return (
<Link
Expand Down
2 changes: 1 addition & 1 deletion love/components/profile/lover-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function LoverProfile(props: {
((!fromLoverPage && !isCurrentUser) ||
(fromLoverPage && fromLoverPage.user_id === currentUser?.id)) && (
<Row className="sticky bottom-[70px] right-0 mr-1 self-end lg:bottom-6">
<LikeButton targetId={user.id} liked={liked} refresh={refresh} />
<LikeButton targetLover={lover} liked={liked} refresh={refresh} />
</Row>
)}
{fromLoverPage &&
Expand Down
Loading

0 comments on commit b383f5a

Please sign in to comment.