254 lines
8.2 KiB
TypeScript
254 lines
8.2 KiB
TypeScript
import React, { useState } from 'react'
|
||
import { Trans, useTranslation } from 'react-i18next'
|
||
import { useContext } from 'use-context-selector'
|
||
import { RiCloseLine } from '@remixicon/react'
|
||
import { useAppContext } from '@/context/app-context'
|
||
import { ToastContext } from '@/app/components/base/toast'
|
||
import Modal from '@/app/components/base/modal'
|
||
import Button from '@/app/components/base/button'
|
||
import Input from '@/app/components/base/input'
|
||
import MemberSelector from './member-selector'
|
||
import {
|
||
ownershipTransfer,
|
||
sendOwnerEmail,
|
||
verifyOwnerEmail,
|
||
} from '@/service/common'
|
||
import { noop } from 'lodash-es'
|
||
|
||
type Props = {
|
||
show: boolean
|
||
onClose: () => void
|
||
}
|
||
|
||
enum STEP {
|
||
start = 'start',
|
||
verify = 'verify',
|
||
transfer = 'transfer',
|
||
}
|
||
|
||
const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||
const { t } = useTranslation()
|
||
const { notify } = useContext(ToastContext)
|
||
const { currentWorkspace, userProfile } = useAppContext()
|
||
const [step, setStep] = useState<STEP>(STEP.start)
|
||
const [code, setCode] = useState<string>('')
|
||
const [time, setTime] = useState<number>(0)
|
||
const [stepToken, setStepToken] = useState<string>('')
|
||
const [newOwner, setNewOwner] = useState<string>('')
|
||
const [isTransfer, setIsTransfer] = useState<boolean>(false)
|
||
|
||
const startCount = () => {
|
||
setTime(60)
|
||
const timer = setInterval(() => {
|
||
setTime((prev) => {
|
||
if (prev <= 0) {
|
||
clearInterval(timer)
|
||
return 0
|
||
}
|
||
return prev - 1
|
||
})
|
||
}, 1000)
|
||
}
|
||
|
||
const sendEmail = async () => {
|
||
try {
|
||
const res = await sendOwnerEmail({})
|
||
startCount()
|
||
if (res.data)
|
||
setStepToken(res.data)
|
||
}
|
||
catch (error) {
|
||
notify({
|
||
type: 'error',
|
||
message: `Error sending verification code: ${error ? (error as any).message : ''}`,
|
||
})
|
||
}
|
||
}
|
||
|
||
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
|
||
try {
|
||
const res = await verifyOwnerEmail({
|
||
code,
|
||
token,
|
||
})
|
||
if (res.is_valid) {
|
||
setStepToken(res.token)
|
||
callback?.()
|
||
}
|
||
else {
|
||
notify({
|
||
type: 'error',
|
||
message: 'Verifying email failed',
|
||
})
|
||
}
|
||
}
|
||
catch (error) {
|
||
notify({
|
||
type: 'error',
|
||
message: `Error verifying email: ${error ? (error as any).message : ''}`,
|
||
})
|
||
}
|
||
}
|
||
|
||
const sendCodeToOriginEmail = async () => {
|
||
await sendEmail()
|
||
setStep(STEP.verify)
|
||
}
|
||
|
||
const handleVerifyOriginEmail = async () => {
|
||
await verifyEmailAddress(code, stepToken, () => setStep(STEP.transfer))
|
||
setCode('')
|
||
}
|
||
|
||
const handleTransfer = async () => {
|
||
setIsTransfer(true)
|
||
try {
|
||
await ownershipTransfer(
|
||
newOwner,
|
||
{
|
||
token: stepToken,
|
||
},
|
||
)
|
||
globalThis.location.reload()
|
||
}
|
||
catch (error) {
|
||
notify({
|
||
type: 'error',
|
||
message: `Error ownership transfer: ${error ? (error as any).message : ''}`,
|
||
})
|
||
}
|
||
finally {
|
||
setIsTransfer(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Modal
|
||
isShow={show}
|
||
onClose={noop}
|
||
className='!w-[420px] !p-6'
|
||
>
|
||
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
|
||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||
</div>
|
||
{step === STEP.start && (
|
||
<>
|
||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.title')}</div>
|
||
<div className='space-y-1 pb-2 pt-1'>
|
||
<div className='body-md-medium text-text-destructive'>{t('common.members.transferModal.warning', { workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.warningTip')}</div>
|
||
<div className='body-md-regular text-text-secondary'>
|
||
<Trans
|
||
i18nKey="common.members.transferModal.sendTip"
|
||
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||
values={{ email: userProfile.email }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className='pt-3'></div>
|
||
<div className='space-y-2'>
|
||
<Button
|
||
className='!w-full'
|
||
variant='primary'
|
||
onClick={sendCodeToOriginEmail}
|
||
>
|
||
{t('common.members.transferModal.sendVerifyCode')}
|
||
</Button>
|
||
<Button
|
||
className='!w-full'
|
||
onClick={onClose}
|
||
>
|
||
{t('common.operation.cancel')}
|
||
</Button>
|
||
</div>
|
||
</>
|
||
)}
|
||
{step === STEP.verify && (
|
||
<>
|
||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.verifyEmail')}</div>
|
||
<div className='pb-2 pt-1'>
|
||
<div className='body-md-regular text-text-secondary'>
|
||
<Trans
|
||
i18nKey="common.members.transferModal.verifyContent"
|
||
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||
values={{ email: userProfile.email }}
|
||
/>
|
||
</div>
|
||
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.verifyContent2')}</div>
|
||
</div>
|
||
<div className='pt-3'>
|
||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.members.transferModal.codeLabel')}</div>
|
||
<Input
|
||
className='!w-full'
|
||
placeholder={t('common.members.transferModal.codePlaceholder')}
|
||
value={code}
|
||
onChange={e => setCode(e.target.value)}
|
||
maxLength={6}
|
||
/>
|
||
</div>
|
||
<div className='mt-3 space-y-2'>
|
||
<Button
|
||
disabled={code.length !== 6}
|
||
className='!w-full'
|
||
variant='primary'
|
||
onClick={handleVerifyOriginEmail}
|
||
>
|
||
{t('common.members.transferModal.continue')}
|
||
</Button>
|
||
<Button
|
||
className='!w-full'
|
||
onClick={onClose}
|
||
>
|
||
{t('common.operation.cancel')}
|
||
</Button>
|
||
</div>
|
||
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||
<span>{t('common.members.transferModal.resendTip')}</span>
|
||
{time > 0 && (
|
||
<span>{t('common.members.transferModal.resendCount', { count: time })}</span>
|
||
)}
|
||
{!time && (
|
||
<span onClick={sendCodeToOriginEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.members.transferModal.resend')}</span>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
{step === STEP.transfer && (
|
||
<>
|
||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.title')}</div>
|
||
<div className='space-y-1 pb-2 pt-1'>
|
||
<div className='body-md-medium text-text-destructive'>{t('common.members.transferModal.warning', { workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.warningTip')}</div>
|
||
</div>
|
||
<div className='pt-3'>
|
||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.members.transferModal.transferLabel')}</div>
|
||
<MemberSelector
|
||
exclude={[userProfile.id]}
|
||
value={newOwner}
|
||
onSelect={setNewOwner}
|
||
/>
|
||
</div>
|
||
<div className='mt-4 space-y-2'>
|
||
<Button
|
||
disabled={!newOwner || isTransfer}
|
||
className='!w-full'
|
||
variant='warning'
|
||
onClick={handleTransfer}
|
||
>
|
||
{t('common.members.transferModal.transfer')}
|
||
</Button>
|
||
<Button
|
||
className='!w-full'
|
||
onClick={onClose}
|
||
>
|
||
{t('common.operation.cancel')}
|
||
</Button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
export default TransferOwnershipModal
|