import React, { FunctionComponent, useEffect, useMemo, useState } from 'react' import { PlusCircleIcon, TrashIcon } from '@heroicons/react/outline' import { Source } from '@notifi-network/notifi-core' import Modal from './Modal' import Input, { Label } from './Input' import { ElementTitle } from './styles' import useMangoStore, { AlertRequest, programId } from '../stores/useMangoStore' import Button, { LinkButton } from './Button' import { notify } from '../utils/notifications' import { useTranslation } from 'next-i18next' import ButtonGroup from './ButtonGroup' import InlineNotification from './InlineNotification' import { NotifiIcon } from './icons' import { BlockchainEnvironment, GqlError, useNotifiClient, isAlertObsolete, } from '@notifi-network/notifi-react-hooks' import { useWallet } from '@solana/wallet-adapter-react' interface CreateAlertModalProps { onClose: () => void isOpen: boolean repayAmount?: string tokenSymbol?: string } const nameForAlert = ( health: number, email: string, phone: string, telegram: string ): string => `Alert for Email: ${email} Phone: ${phone} Telegram: ${telegram} When Health <= ${health}` const CreateAlertModal: FunctionComponent = ({ isOpen, onClose, }) => { const { t } = useTranslation(['common', 'alerts']) const actions = useMangoStore((s) => s.actions) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const activeAlerts = useMangoStore((s) => s.alerts.activeAlerts) const loading = useMangoStore((s) => s.alerts.loading) const submitting = useMangoStore((s) => s.alerts.submitting) const error = useMangoStore((s) => s.alerts.error) const cluster = useMangoStore((s) => s.connection.cluster) const [invalidAmountMessage, setInvalidAmountMessage] = useState('') const [health, setHealth] = useState('') const [showCustomHealthForm, setShowCustomHealthForm] = useState(false) const [showAlertForm, setShowAlertForm] = useState(false) // notifi error message const [errorMessage, setErrorMessage] = useState('') const healthPresets = ['5', '10', '15', '25', '30'] const ALERT_LIMIT = 5 let env = BlockchainEnvironment.MainNetBeta switch (cluster) { case 'mainnet': break case 'devnet': env = BlockchainEnvironment.DevNet break } const { publicKey, connected, signMessage } = useWallet() const { data, fetchData, logIn, isAuthenticated, createAlert, deleteAlert } = useNotifiClient({ dappAddress: programId.toBase58(), walletPublicKey: publicKey?.toString() ?? '', env, }) const [email, setEmail] = useState('') const [phone, setPhone] = useState('+') const [telegramId, setTelegramId] = useState('') const handleError = (errors: { message: string }[]) => { const err = errors.length > 0 ? errors[0] : null if (err instanceof GqlError) { setErrorMessage(`${err.message}: ${err.getErrorMessages().join(', ')}`) } else { setErrorMessage(err?.message ?? 'Unknown error') } } const getSourceToUse = (sources) => { return sources?.find((it) => { const filter = it.applicableFilters?.find((filter) => { return filter.filterType === 'VALUE_THRESHOLD' }) return filter !== undefined }) } let { alerts, sources } = data || {} let sourceToUse: Source | undefined = useMemo(() => { return getSourceToUse(sources) }, [sources]) const handlePhone = (e: React.ChangeEvent) => { let val = e.target.value if (val.length > 0) { val = val.substring(1) } const re = /^[0-9\b]+$/ if (val === '' || (re.test(val) && val.length <= 15)) { setPhone('+' + val) } } const handleTelegramId = (e: React.ChangeEvent) => { setTelegramId(e.target.value) } const createNotifiAlert = async function () { // user is not authenticated if (!isAuthenticated() && publicKey) { try { if (signMessage === undefined) { throw new Error('signMessage is not defined') } await logIn({ signMessage }) } catch (e) { handleError([e]) throw e } // refresh data after login ({ alerts, sources } = await fetchData()) sourceToUse = getSourceToUse(sources) } if (connected && isAuthenticated()) { if (!sourceToUse || !sourceToUse.id) return const filter = sourceToUse?.applicableFilters.find( (f) => f.filterType === 'VALUE_THRESHOLD' ) if (!filter || !filter.id) return try { const healthInt = parseInt(health, 10) const res = await createAlert({ filterId: filter.id, sourceId: sourceToUse.id, groupName: mangoAccount?.publicKey.toBase58(), name: nameForAlert(healthInt, email, phone, telegramId), emailAddress: email === '' ? null : email, phoneNumber: phone.length < 12 || phone.length > 16 ? null : phone, telegramId: telegramId === '' ? null : telegramId, filterOptions: { alertFrequency: 'SINGLE', threshold: healthInt, }, }) if (telegramId) { const telegramTarget = res.targetGroup?.telegramTargets.find( (telegramTarget) => telegramTarget.telegramId === telegramId ) if ( telegramTarget && !telegramTarget.isConfirmed && telegramTarget.confirmationUrl ) { window.open(telegramTarget.confirmationUrl, '_blank') } } // return notifiAlertId return res.id } catch (e) { handleError([e]) throw e } } } const deleteNotifiAlert = async function (alert) { // user is not authenticated if (!isAuthenticated() && publicKey) { try { if (signMessage === undefined) { throw new Error('signMessage is not defined') } await logIn({ signMessage }) } catch (e) { handleError([e]) throw e } } if (connected && isAuthenticated()) { try { await deleteAlert({ alertId: alert.notifiAlertId }) } catch (e) { handleError([e]) throw e } } } // Clean up alerts that don't exist in DB const consolidateNotifiAlerts = async function () { const alertsToCleanUp = alerts?.filter((alert) => { const isAlertExist = activeAlerts?.some( (a) => a.notifiAlertId === alert.id ) return !isAlertExist }) if (alertsToCleanUp === undefined) return alertsToCleanUp.forEach((alert) => { if (alert.id) { deleteAlert({ alertId: alert.id }) } }) } const validateEmailInput = (amount) => { if (Number(amount) <= 0) { setInvalidAmountMessage(t('enter-amount')) } } const onChangeEmailInput = (amount) => { setEmail(amount) setInvalidAmountMessage('') } async function onCreateAlert() { if (!mangoGroup || !mangoAccount) return const parsedHealth = parseFloat(health) if (!email && !phone && !telegramId) { notify({ title: t('alerts:notifi-type-required'), type: 'error', }) return } else if (typeof parsedHealth !== 'number') { notify({ title: t('alerts:alert-health-required'), type: 'error', }) return } let notifiAlertId // send alert to Notifi try { notifiAlertId = await createNotifiAlert() } catch (e) { handleError([e]) return } if (notifiAlertId) { const body: AlertRequest = { mangoGroupPk: mangoGroup.publicKey.toString(), mangoAccountPk: mangoAccount.publicKey.toString(), health: parsedHealth, alertProvider: 'notifi', email, notifiAlertId, } const success: any = await actions.createAlert(body) if (success) { setErrorMessage('') setShowAlertForm(false) } } } async function onDeleteAlert(alert) { // delete alert from db actions.deleteAlert(alert._id) // delete alert from Notifi try { await deleteNotifiAlert(alert) } catch (e) { handleError([e]) } } async function onNewAlert() { if (connected && isAuthenticated()) { try { await consolidateNotifiAlerts() } catch (e) { handleError([e]) throw e } } setShowAlertForm(true) } const handleCancelCreateAlert = () => { if (activeAlerts.length > 0) { setShowAlertForm(false) } else { onClose() } } useEffect(() => { if (mangoAccount) { actions.loadAlerts(mangoAccount?.publicKey) } }, []) // Delete notifi Alerts that have fired useEffect(() => { const firedAlert = alerts?.find(isAlertObsolete) if (firedAlert !== undefined && firedAlert.id !== null) { deleteAlert({ alertId: firedAlert.id }) } }, [alerts, deleteAlert]) return ( {!loading && !submitting ? ( <> {activeAlerts.length > 0 && !showAlertForm ? ( <>
{t('alerts:active-alerts')}
{errorMessage.length > 0 ? (
{errorMessage}
) : null} {activeAlerts.map((alert, index) => (
{t('alerts:alert-info', { health: alert.health })}
onDeleteAlert(alert)} />
))}
{activeAlerts.length >= ALERT_LIMIT ? (
{t('alerts:alerts-max')}
) : null} ) : showAlertForm ? ( <> {t('alerts:create-alert')}

{t('alerts:alerts-disclaimer')}

{error ? (
) : null} validateEmailInput(e.target.value)} value={email || ''} onChange={(e) => onChangeEmailInput(e.target.value)} />
setShowCustomHealthForm(!showCustomHealthForm) } > {showCustomHealthForm ? t('presets') : t('custom')}
{showCustomHealthForm ? ( setHealth(e.target.value)} suffix={
%
} value={health} /> ) : ( setHealth(p)} unit="%" values={healthPresets} /> )}
{errorMessage.length > 0 ? (
{errorMessage}
) : ( !isAuthenticated() && (
{t('alerts:prompted-to-sign-transaction')}
) )} {t('cancel')} ) : error ? (
) : (
{t('alerts:no-alerts')}

{t('alerts:no-alerts-desc')}

)} ) : (
)}
{t('alerts:powered-by')}
) } export default React.memo(CreateAlertModal)