Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Anonymous Login in Certain Chats #476

Merged
merged 11 commits into from
Nov 28, 2023
64 changes: 37 additions & 27 deletions src/components/auth/LoginModal/LoginModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ModalFunctionalityProps } from '@/components/modals/Modal'
import ProfilePreview from '@/components/ProfilePreview'
import Toast from '@/components/Toast'
import useLoginAndRequestToken from '@/hooks/useLoginAndRequestToken'
import useLoginOptions from '@/hooks/useLoginOptions'
import useSignMessageAndLinkEvmAddress from '@/hooks/useSignMessageAndLinkEvmAddress'
import useToastError from '@/hooks/useToastError'
import { ApiRequestTokenResponse } from '@/pages/api/request-token'
Expand Down Expand Up @@ -70,6 +71,7 @@ export const LoginContent = ({
}: ContentProps) => {
const [hasStartCaptcha, setHasStartCaptcha] = useState(false)
const sendEvent = useSendEvent()
const { isNonAnonLoginRequired } = useLoginOptions()

const {
mutateAsync: loginAndRequestToken,
Expand All @@ -88,7 +90,12 @@ export const LoginContent = ({
<div>
<div className='flex w-full flex-col justify-center'>
<Logo className='mb-8 mt-4 text-5xl' />
<div className='flex flex-col gap-4'>
<div
className={cx(
'flex flex-col gap-4',
isNonAnonLoginRequired && 'pb-4'
)}
>
<Button
onClick={() => {
setCurrentState('connect-wallet')
Expand All @@ -111,32 +118,35 @@ export const LoginContent = ({
Enter Grill secret key
</div>
</Button>
<Button
type='button'
variant='primaryOutline'
size='lg'
className='w-full'
isLoading={isLoading}
onClick={async () => {
setHasStartCaptcha(true)
const token = await runCaptcha()
if (!token) return
setHasStartCaptcha(false)
const newAddress = await loginAndRequestToken({
captchaToken: token,
})
if (newAddress) {
setCurrentState('account-created')
}
}}
>
<div className='flex items-center justify-center gap-2'>
<IncognitoIcon className='text-text-muted' />
Continue anonymously
</div>
</Button>

{termsAndService('mt-4')}
{!isNonAnonLoginRequired && (
<>
<Button
type='button'
variant='primaryOutline'
size='lg'
className='w-full'
isLoading={isLoading}
onClick={async () => {
setHasStartCaptcha(true)
const token = await runCaptcha()
if (!token) return
setHasStartCaptcha(false)
const newAddress = await loginAndRequestToken({
captchaToken: token,
})
if (newAddress) {
setCurrentState('account-created')
}
}}
>
<div className='flex items-center justify-center gap-2'>
<IncognitoIcon className='text-text-muted' />
Continue anonymously
</div>
</Button>
{termsAndService('mt-4')}
</>
)}
</div>
</div>
</div>
Expand Down
9 changes: 6 additions & 3 deletions src/components/chats/ChatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TextArea, { TextAreaProps } from '@/components/inputs/TextArea'
import EmailSubscribeModal from '@/components/modals/EmailSubscribeModal'
import { ERRORS } from '@/constants/error'
import useAutofocus from '@/hooks/useAutofocus'
import useLoginOptions from '@/hooks/useLoginOptions'
import useRequestTokenAndSendMessage from '@/hooks/useRequestTokenAndSendMessage'
import { showErrorToast } from '@/hooks/useToastError'
import { useConfigContext } from '@/providers/ConfigProvider'
Expand Down Expand Up @@ -91,6 +92,7 @@ export default function ChatForm({
const messageToEdit = useMessageData((state) => state.messageToEdit)
const clearAction = useMessageData((state) => state.clearAction)
const setMessageBody = useMessageData((state) => state.setMessageBody)
const { isNonAnonLoginRequired } = useLoginOptions()

const { data: editedMessage } = getPostQuery.useQuery(messageToEdit, {
enabled: !!messageToEdit,
Expand Down Expand Up @@ -188,6 +190,7 @@ export default function ChatForm({
clearAction?.()
}

const needCaptcha = !shouldSendMessage
const handleSubmit = async (captchaToken: string | null) => {
if (
shouldSendMessage &&
Expand Down Expand Up @@ -222,7 +225,8 @@ export default function ChatForm({
return
}

if (!hasSentMessageStorage.get()) {
const willOpenLoginModal = isNonAnonLoginRequired && !isLoggedIn
if (!hasSentMessageStorage.get() && !willOpenLoginModal) {
setTimeout(() => {
setIsOpenCtaModal(true)
}, 2000)
Expand All @@ -244,7 +248,6 @@ export default function ChatForm({
resetForm()
sendMessage(messageParams)
} else {
if (!captchaToken) return
resetForm()
requestTokenAndSendMessage({
captchaToken,
Expand Down Expand Up @@ -274,7 +277,7 @@ export default function ChatForm({
{(runCaptcha) => {
const submitForm = async (e?: SyntheticEvent) => {
e?.preventDefault()
if (shouldSendMessage) {
if (!needCaptcha) {
handleSubmit(null)
return
}
Expand Down
10 changes: 6 additions & 4 deletions src/components/navbar/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import usePrevious from '@/hooks/usePrevious'
import { useConfigContext } from '@/providers/ConfigProvider'
import { getUnreadCountQuery } from '@/services/subsocial/datahub/posts/query'
import { useSendEvent } from '@/stores/analytics'
import { useLoginModal } from '@/stores/login-modal'
import { useMyAccount, useMyMainAddress } from '@/stores/my-account'
import { cx } from '@/utils/class-names'
import { getDatahubConfig } from '@/utils/env/client'
Expand Down Expand Up @@ -58,7 +59,8 @@ export default function Navbar({
const prevAddress = usePrevious(address)
const isLoggedIn = !!address

const [openLoginModal, setOpenLoginModal] = useState(false)
const isOpen = useLoginModal((state) => state.isOpen)
const setIsOpen = useLoginModal((state) => state.setIsOpen)
const [openPrivateKeyNotice, setOpenPrivateKeyNotice] = useState(false)
const isLoggingInWithKey = useRef(false)
const timeoutRef = useRef<any>()
Expand All @@ -79,7 +81,7 @@ export default function Navbar({
}, [address, isInitializedAddress, prevAddress])

const login = () => {
setOpenLoginModal(true)
setIsOpen(true)
}

const renderAuthComponent = () => {
Expand Down Expand Up @@ -154,8 +156,8 @@ export default function Navbar({
</Container>
</nav>
<LoginModal
isOpen={openLoginModal}
closeModal={() => setOpenLoginModal(false)}
isOpen={isOpen}
closeModal={() => setIsOpen(false)}
beforeLogin={() => (isLoggingInWithKey.current = true)}
afterLogin={() => (isLoggingInWithKey.current = false)}
/>
Expand Down
5 changes: 5 additions & 0 deletions src/constants/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ const INCLUDED_CHAT_IDS_FOR_UNREAD_COUNT: string[] = ['754', '7465']
export function getIncludedChatIdsForUnreadCount() {
return INCLUDED_CHAT_IDS_FOR_UNREAD_COUNT
}

const CHAT_IDS_WITHOUT_ANON_LOGIN_OPTIONS: string[] = ['19361']
export function getChatIdsWithoutAnonLoginOptions() {
return CHAT_IDS_WITHOUT_ANON_LOGIN_OPTIONS
}
37 changes: 37 additions & 0 deletions src/hooks/useLoginOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getChatIdsWithoutAnonLoginOptions } from '@/constants/chat'
import { useLoginModal } from '@/stores/login-modal'
import { useMyAccount } from '@/stores/my-account'
import { generateManuallyTriggeredPromise } from '@/utils/promise'
import { getIdFromSlug } from '@/utils/slug'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useRef } from 'react'

export default function useLoginOptions() {
const { query } = useRouter()
const chatId =
typeof query.slug === 'string' ? getIdFromSlug(query.slug) : undefined
const isNonAnonLoginRequired = getChatIdsWithoutAnonLoginOptions().includes(
chatId ?? ''
)

const isOpen = useLoginModal((state) => state.isOpen)
const waitingLoginResolvers = useRef<VoidFunction[]>([])
useEffect(() => {
if (!isOpen) {
waitingLoginResolvers.current.forEach((resolver) => resolver())
waitingLoginResolvers.current = []
}
}, [isOpen])

const setIsOpen = useLoginModal((state) => state.setIsOpen)

const promptUserForLogin = useCallback(async () => {
setIsOpen(true)
const { getPromise, getResolver } = generateManuallyTriggeredPromise()
waitingLoginResolvers.current.push(getResolver())
await getPromise()
return useMyAccount.getState().address
}, [setIsOpen])

return { isNonAnonLoginRequired, promptUserForLogin }
}
26 changes: 18 additions & 8 deletions src/hooks/useRequestTokenAndSendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import {
} from '@/services/subsocial/commentIds'
import { useMyAccount, useMyMainAddress } from '@/stores/my-account'
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import useLoginOptions from './useLoginOptions'

type Params = SendMessageParams & {
captchaToken: string
captchaToken: string | null
}
export default function useRequestTokenAndSendMessage(
options?: UseMutationOptions<void, unknown, Params, unknown>
) {
const address = useMyMainAddress()
const { isNonAnonLoginRequired, promptUserForLogin } = useLoginOptions()

const { mutateAsync: requestToken } = useRequestToken()
const { mutateAsync: sendMessage } = useSendMessage()
Expand All @@ -22,15 +24,23 @@ export default function useRequestTokenAndSendMessage(
const { captchaToken, ...sendMessageParams } = params
let usedAddress: string = address ?? ''
if (!address) {
const address = await login()
if (!address) throw new Error('Failed to login')
usedAddress = address
if (isNonAnonLoginRequired) {
const loginAddress = await promptUserForLogin()
if (!loginAddress) return
usedAddress = loginAddress
} else {
const address = await login()
if (!address) throw new Error('Failed to login')
usedAddress = address
}
}

await Promise.all([
requestToken({ address: usedAddress, captchaToken }),
sendMessage(sendMessageParams),
])
const promises: Promise<any>[] = [sendMessage(sendMessageParams)]
if (captchaToken) {
promises.push(requestToken({ address: usedAddress, captchaToken }))
}

await Promise.all(promises)
}

return useMutation(requestTokenAndSendMessage, options)
Expand Down
32 changes: 32 additions & 0 deletions src/pages/api/request-token.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ESTIMATED_ENERGY_FOR_ONE_TX } from '@/constants/subsocial'
import { ApiResponse, handlerWrapper } from '@/server/common'
import { getSubsocialApi } from '@/subsocial-query/subsocial/connection'
import { validateAddress } from '@/utils/account'
Expand Down Expand Up @@ -46,6 +47,12 @@ export default handlerWrapper({
}
}

try {
const isNeedEnergy = getIsAddressNeedEnergy(data.address)
if (!isNeedEnergy)
return res.status(200).send({ success: true, message: 'OK', data: '' })
} catch {}

let hash: string
try {
hash = await sendToken(data.address)
Expand All @@ -72,6 +79,31 @@ export default handlerWrapper({
},
})

async function getIsAddressNeedEnergy(address: string) {
const { getSubsocialApi } = await import(
'@/subsocial-query/subsocial/connection'
)

const subsocialApi = await getSubsocialApi()
const substrateApi = await subsocialApi.substrateApi

return new Promise((resolve) => {
const unsub = substrateApi.query.energy.energyBalance(
address,
(energyAmount) => {
let parsedEnergy: unknown = energyAmount
if (typeof energyAmount.toPrimitive === 'function') {
parsedEnergy = energyAmount.toPrimitive()
}
const energy = parseFloat(parsedEnergy + '')
const isAddressNeedEnergy = energy < ESTIMATED_ENERGY_FOR_ONE_TX
resolve(isAddressNeedEnergy)
unsub.then((unsub) => unsub())
}
)
})
}

async function getServerAccount() {
const mnemonic = getServerMnemonic()
const keyring = new Keyring()
Expand Down
14 changes: 11 additions & 3 deletions src/services/subsocial/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import useLoginOptions from '@/hooks/useLoginOptions'
import { useRequestToken } from '@/services/api/mutation'
import { getHasEnoughEnergy, useMyAccount } from '@/stores/my-account'
import {
Expand Down Expand Up @@ -49,15 +50,22 @@ export default function useCommonTxSteps<Data, ReturnValue>(
const { mutateAsync: requestToken } = useRequestToken()
const login = useMyAccount((state) => state.login)

const { isNonAnonLoginRequired, promptUserForLogin } = useLoginOptions()
const needToRunCaptcha = !address || !hasEnoughEnergy

const workerFunc = async (params: { captchaToken?: string } & Data) => {
const { captchaToken } = params
let usedAddress: string = address ?? ''
if (!address) {
const address = await login()
if (!address) throw new Error('Failed to login')
usedAddress = address
if (isNonAnonLoginRequired) {
const address = await promptUserForLogin()
if (!address) return
usedAddress = address
} else {
const address = await login()
if (!address) throw new Error('Failed to login')
usedAddress = address
}
}

if (!hasEnoughEnergy && captchaToken) {
Expand Down
20 changes: 20 additions & 0 deletions src/stores/login-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { create } from './utils'

type State = {
isOpen: boolean
}

type Actions = {
setIsOpen: (isOpen: boolean) => void
}

const initialState: State = {
isOpen: false,
}

export const useLoginModal = create<State & Actions>()((set) => ({
...initialState,
setIsOpen: (isOpen) => {
set({ isOpen })
},
}))