Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev/channels' into dev/epic
Browse files Browse the repository at this point in the history
  • Loading branch information
samchuk-vlad committed Sep 5, 2024
2 parents ac3f64a + 2cea066 commit c54b078
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 79 deletions.
Binary file added src/assets/emojis/confused.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
221 changes: 167 additions & 54 deletions src/components/chats/ChatRoom/PostMemeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import Diamond from '@/assets/emojis/diamond.png'
import Button from '@/components/Button'
import LinkEvmAddressModal from '@/components/modals/LinkEvmAddressModal'
import Confused from '@/assets/emojis/confused.png'
import BottomDrawer from '@/components/BottomDrawer'
import Button, { ButtonProps } from '@/components/Button'
import Card from '@/components/Card'
import Meme2EarnIntroModal, {
hasOpenedMeme2EarnIntroStorage,
} from '@/components/modals/Meme2EarnIntroModal'
import Modal, { ModalFunctionalityProps } from '@/components/modals/Modal'
import { ModalFunctionalityProps } from '@/components/modals/Modal'
import EvmConnectWalletModal from '@/components/wallets/evm/EvmConnectWalletModal'
import useIsAddressBlockedInChat from '@/hooks/useIsAddressBlockedInChat'
import useLinkedEvmAddress from '@/hooks/useLinkedEvmAddress'
import usePostMemeThreshold from '@/hooks/usePostMemeThreshold'
import useToastError from '@/hooks/useToastError'
import SolanaButton from '@/modules/telegram/AirdropPage/solana'
import { ContentContainer } from '@/services/datahub/content-containers/query'
import { useSyncExternalTokenBalances } from '@/services/datahub/externalTokenBalances/mutation'
import { ExternalTokenChain } from '@/services/datahub/generated-query'
import { getBalanceQuery } from '@/services/datahub/leaderboard/points-balance/query'
import { getTimeLeftUntilCanPostQuery } from '@/services/datahub/posts/query'
import { useSendEvent } from '@/stores/analytics'
import { useExtensionData } from '@/stores/extension'
import { useMessageData } from '@/stores/message'
import { useMyMainAddress } from '@/stores/my-account'
import { truncateAddress } from '@/utils/account'
import { formatNumber } from '@/utils/strings'
import dayjs from 'dayjs'
import Image from 'next/image'
Expand Down Expand Up @@ -124,7 +131,7 @@ export default function PostMemeButton({
size='lg'
variant={isMoreThanThreshold ? 'primary' : 'primaryOutline'}
onClick={() => {
if (passRequirement) {
if (!passRequirement) {
sendEvent('post_meme_token_gated_modal_opened')
setIsOpenTokenGatedModal(true)
return
Expand Down Expand Up @@ -168,7 +175,7 @@ export default function PostMemeButton({
setIsOpenIntroModal(false)
}}
/>
<LinkEvmAddressModal
<EvmConnectWalletModal
isOpen={isOpenLinkEvm}
closeModal={() => setIsOpenLinkEvm(false)}
/>
Expand All @@ -177,6 +184,7 @@ export default function PostMemeButton({
contentContainer={contentContainer}
isOpen={isOpenTokenGatedModal}
closeModal={() => setIsOpenTokenGatedModal(false)}
openEvmLinkModal={() => setIsOpenLinkEvm(true)}
/>
)}
</>
Expand All @@ -185,60 +193,165 @@ export default function PostMemeButton({

function TokenGatedModal({
contentContainer,
openEvmLinkModal,
...props
}: ModalFunctionalityProps & { contentContainer: ContentContainer }) {
const { amountRequired, requiredToken } =
}: ModalFunctionalityProps & {
contentContainer: ContentContainer
openEvmLinkModal: () => void
}) {
const { amountRequired, requiredToken, hasToLinkWallet, currentToken } =
useTokenGatedRequirement(contentContainer)
const sendEvent = useSendEvent()

const [isWaitingSyncDone, setIsWaitingSyncDone] = useState(false)
useEffect(() => {
if (props.isOpen) setIsWaitingSyncDone(false)
}, [props.isOpen])

const {
mutate: syncExternalTokenBalances,
error,
isLoading,
} = useSyncExternalTokenBalances({
onMutate: () => setIsWaitingSyncDone(true),
onSuccessSync: () => {
// TODO: onSuccessSync
setIsWaitingSyncDone(false)
},
onError: () => {
setIsWaitingSyncDone(false)
},
onErrorSync: () => {
// TODO: onErrorSync
setIsWaitingSyncDone(false)
},
})
useToastError(error, 'Failed to check balance')

const [isOpenEvmConnect, setIsOpenEvmConnect] = useState(false)

const externalToken = contentContainer.externalToken

return (
<Modal
{...props}
title='🔒 Hold Up, Meme Master!'
description={`You need at least ${formatNumber(
amountRequired
)} ${requiredToken} to unlock meme-posting powers in this channel.`}
>
<div className='flex flex-col gap-6'>
<div className='flex flex-col items-center justify-center gap-1.5 rounded-2xl bg-background-lighter p-4'>
<span className='text-center font-medium text-text-muted'>
Required amount:
</span>
<div className='-ml-2 flex items-center gap-2.5'>
<Image src={Diamond} alt='' className='h-12 w-12' />
<span className='flex items-center text-3xl font-bold'>
{formatNumber(amountRequired)} {requiredToken}
</span>
<>
<BottomDrawer
{...props}
title={hasToLinkWallet ? 'Account not yet linked' : '🔒 Hold On'}
description={
hasToLinkWallet
? `To gain access to posting in this channel, you first need to connect your ${hasToLinkWallet} account.`
: `You need at least ${formatNumber(
amountRequired
)} ${requiredToken} to unlock posting access in this channel.`
}
>
{hasToLinkWallet ? (
<div className='flex flex-col items-center gap-6'>
<Image src={Confused} alt='' className='h-28 w-28' />
{hasToLinkWallet === 'Ethereum' ? (
<Button
className='w-full'
size='lg'
onClick={() => {
sendEvent('token_gated_connect_evm')
openEvmLinkModal()
props.closeModal()
}}
>
Connect Ethereum
</Button>
) : (
<SolanaButton
className='w-full'
size='lg'
onClick={() => {
sendEvent('token_gated_connect_solana')
props.closeModal()
}}
>
Connect Solana
</SolanaButton>
)}
</div>
</div>
<div className='flex flex-col gap-3 text-text-muted'>
<div className='flex items-center gap-4'>
<span className='font-medium text-text-muted'>
Already got the magic tokens? Click the button below to verify
your balance and start casting your memes!
</span>
) : (
<div className='flex flex-col gap-6'>
<div className='flex flex-col gap-3'>
<Card className='flex flex-col gap-2 p-4'>
<span className='text-3xl font-bold'>
{formatNumber(amountRequired)} {requiredToken}
</span>
<span className='text-sm text-text-muted'>
Required tokens amount
</span>
</Card>
{currentToken && (
<Card className='flex flex-col gap-2 p-4'>
<span className='text-3xl font-bold'>
{formatNumber(currentToken.parsedAmount)} {requiredToken}
</span>
<span className='text-sm text-text-muted'>
Your balance (
{truncateAddress(currentToken.blockchainAddress)})
</span>
</Card>
)}
</div>
<div className='flex flex-col gap-4'>
<Button
size='lg'
isLoading={isWaitingSyncDone || isLoading}
onClick={() => {
if (!externalToken) return
sendEvent('token_gated_check_balance')
syncExternalTokenBalances({
externalTokenId: externalToken.id,
})
}}
>
Check my balance
</Button>
{externalToken && (
<BuyTokenButton
address={externalToken.address}
chain={externalToken.chain}
size='lg'
variant='primaryOutline'
>
Buy {currentToken ? 'more ' : ''}
{requiredToken}
</BuyTokenButton>
)}
</div>
</div>
</div>
<div className='flex flex-col gap-3'>
<Button
size='lg'
onClick={() => {
props.closeModal()
}}
>
I have the token in my wallet!
</Button>
<Button
variant='primaryOutline'
size='lg'
onClick={() => {
props.closeModal()
}}
>
Buy {requiredToken}
</Button>
</div>
</div>
</Modal>
)}
</BottomDrawer>
<EvmConnectWalletModal
isOpen={isOpenEvmConnect}
closeModal={() => setIsOpenEvmConnect(false)}
/>
</>
)
}

const linkGenerator: Record<ExternalTokenChain, (address: string) => string> = {
ETHEREUM: (address) =>
`https://app.uniswap.org/swap?chain=mainnet&outputCurrency=${address}`,
SOLANA: (address) => `https://raydium.io/swap/?outputMint=${address}`,
}
function BuyTokenButton({
address,
chain,
...props
}: {
address: string
chain: ExternalTokenChain
} & ButtonProps) {
return (
<Button
{...props}
href={linkGenerator[chain]?.(address)}
target='_blank'
rel='noopener noreferrer'
/>
)
}
48 changes: 39 additions & 9 deletions src/components/chats/hooks/useTokenGatedRequirement.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import useLinkedEvmAddress from '@/hooks/useLinkedEvmAddress'
import { ContentContainer } from '@/services/datahub/content-containers/query'
import { getExternalTokenBalancesQuery } from '@/services/datahub/externalTokenBalances/query'
import {
ExternalTokenBalance,
getExternalTokenBalancesQuery,
} from '@/services/datahub/externalTokenBalances/query'
import { ExternalTokenChain } from '@/services/datahub/generated-query'
import { getBalanceQuery } from '@/services/datahub/leaderboard/points-balance/query'
import { useMyMainAddress } from '@/stores/my-account'
import { convertToBigInt } from '@/utils/strings'

type HasToLinkWallet = 'Solana' | 'Ethereum' | undefined

export default function useTokenGatedRequirement(
contentContainer?: ContentContainer
) {
const { evmAddress, isLoading: loadingAddress } = useLinkedEvmAddress()

const externalTokenRequirement = convertToBigInt(
contentContainer?.accessThresholdExternalTokenAmount ?? 0
)
Expand All @@ -30,16 +39,21 @@ export default function useTokenGatedRequirement(
let isLoading = false
let passRequirement = true
let amountRequired = 0
let currentToken: ExternalTokenBalance | undefined
let requiredToken = ''
if (pointsRequirement > 0) {
isLoading = loadingPoints
passRequirement = (points ?? 0) >= pointsRequirement
amountRequired = pointsRequirement
requiredToken = 'points'
} else if (externalTokenRequirement > 0) {
let hasToLinkWallet: HasToLinkWallet
if (externalTokenRequirement > 0) {
const tokenChain = contentContainer?.externalToken?.chain
if (tokenChain === ExternalTokenChain.Ethereum && !evmAddress) {
hasToLinkWallet = 'Ethereum'
// TODO: validate solana address
} else if (tokenChain === ExternalTokenChain.Solana) {
hasToLinkWallet = 'Solana'
}

isLoading = loadingExternalTokens
const tokenBalance = externalTokens?.find(
(token) => token.id === contentContainer?.externalToken?.id
(token) => token.externalToken.id === contentContainer?.externalToken?.id
)
passRequirement =
convertToBigInt(tokenBalance?.amount ?? 0) >= externalTokenRequirement
Expand All @@ -48,7 +62,23 @@ export default function useTokenGatedRequirement(
BigInt(10 ** Number(contentContainer?.externalToken?.decimals ?? 0))
)
requiredToken = contentContainer?.externalToken?.name ?? ''
currentToken = tokenBalance
} else if (pointsRequirement > 0) {
isLoading = loadingPoints
passRequirement = (points ?? 0) >= pointsRequirement
amountRequired = pointsRequirement
requiredToken = 'points'
}

return { passRequirement, isLoading, amountRequired, requiredToken }
isLoading = isLoading || loadingAddress

return {
passRequirement,
isLoading,
amountRequired,
requiredToken,
chain: contentContainer?.externalToken?.chain,
hasToLinkWallet,
currentToken,
}
}
1 change: 0 additions & 1 deletion src/modules/telegram/AirdropPage/solana.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import LinkText from '@/components/LinkText'
import { env } from '@/env.mjs'
import { useEncryptData } from '@/services/api/mutation'
import { useMyAccount, useMyMainAddress } from '@/stores/my-account'
Expand Down
12 changes: 6 additions & 6 deletions src/server/datahub-queue/external-token.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { SocialEventDataApiInput } from '@subsocial/data-hub-sdk'
import { gql } from 'graphql-request'
import {
LinkIdentityMutation,
LinkIdentityMutationVariables,
SyncExternalTokenBalancesMutation,
SyncExternalTokenBalancesMutationVariables,
} from './generated'
import { datahubQueueRequest, throwErrorIfNotProcessed } from './utils'

const SYNC_EXTERNAL_TOKEN_BALANCES = gql`
mutation SyncExternalTokenBalances($args: SocialProfileAddReferrerIdInput!!) {
mutation SyncExternalTokenBalances($args: SocialProfileAddReferrerIdInput!) {
socialProfileSyncExternalTokenBalance(args: $args) {
processed
callId
Expand All @@ -20,16 +20,16 @@ export async function syncExternalTokenBalances(
input: SocialEventDataApiInput
) {
const res = await datahubQueueRequest<
LinkIdentityMutation,
LinkIdentityMutationVariables
SyncExternalTokenBalancesMutation,
SyncExternalTokenBalancesMutationVariables
>({
document: SYNC_EXTERNAL_TOKEN_BALANCES,
variables: {
args: input as any,
},
})
throwErrorIfNotProcessed(
res.initLinkedIdentity,
res.socialProfileSyncExternalTokenBalance,
'Failed to sync external token balances'
)
}
Loading

0 comments on commit c54b078

Please sign in to comment.