mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Fix: Donation for custom wallet address throws error when paying from the same wallet
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.1.10-beta.11",
|
||||
"version": "0.1.10-beta.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
|
||||
@@ -152,9 +152,13 @@ export type WalletStackParamList = {
|
||||
unit: MintUnit,
|
||||
encodedInvoice?: string,
|
||||
paymentRequest?: PaymentRequest,
|
||||
lnurlParams?: LNURLPayParams & {address?: string},
|
||||
lnurlParams?: LNURLPayParams & {address?: string},
|
||||
fixedAmount?: number,
|
||||
comment?: string
|
||||
paymentOption?: SendOption,
|
||||
mintUrl?: string,
|
||||
mintUrl?: string,
|
||||
isDonation?: boolean,
|
||||
donationForName?: string
|
||||
}
|
||||
Topup: {
|
||||
unit: MintUnit,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'
|
||||
import {Text as RNText, TextStyle, View, ViewStyle, InteractionManager, TextInput, ScrollView } from 'react-native'
|
||||
import {Text as RNText, TextStyle, View, ViewStyle, TextInput } from 'react-native'
|
||||
import {colors, spacing, typography, useThemeColor} from '../../theme'
|
||||
import {BottomModal, Button, Card, ErrorModal, Icon, InfoModal, ListItem, Loading, Screen, Text} from '../../components'
|
||||
import {useStores} from '../../models'
|
||||
@@ -17,16 +17,17 @@ import { CurrencyAmount } from '../Wallet/CurrencyAmount'
|
||||
import { CurrencyCode } from '../../services/wallet/currency'
|
||||
import { QRCodeBlock } from '../Wallet/QRCode'
|
||||
import { MintBalance } from '../../models/Mint'
|
||||
import { MintListItem } from '../Mints/MintListItem'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { round, roundUp } from '../../utils/number'
|
||||
import { roundUp } from '../../utils/number'
|
||||
import { LnurlClient } from '../../services/lnurlService'
|
||||
|
||||
const DEFAULT_DONATION_AMOUNT = 500
|
||||
const DONATION_LNURL_ADDRESS = 'minibits@minibits.cash'
|
||||
|
||||
export const OwnName = observer(function (props: {navigation: any, pubkey: string}) {
|
||||
// const navigation = useNavigation()
|
||||
const ownNameInputRef = useRef<TextInput>(null)
|
||||
const {proofsStore, walletProfileStore, userSettingsStore, mintsStore} = useStores()
|
||||
const {proofsStore, walletProfileStore} = useStores()
|
||||
const {pubkey, navigation} = props
|
||||
|
||||
const [ownName, setOwnName] = useState<string>('')
|
||||
@@ -34,8 +35,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
const [selectedBalance, setSelectedBalance] = useState<MintBalance | undefined>(undefined)
|
||||
const [donationAmount, setDonationAmount] = useState(DEFAULT_DONATION_AMOUNT)
|
||||
const [donationInvoice, setDonationInvoice] = useState<{payment_hash: string, payment_request: string} | undefined>(undefined)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isQRcodeVisible, setIsQRCodeVisible] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
// const [isNameInputEnabled, setIsNameInputEnabled] = useState(true)
|
||||
const [isInvoicePaid, setIsInvoicePaid] = useState<boolean>(false)
|
||||
@@ -80,7 +80,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
checkDonationPaid,
|
||||
{
|
||||
interval: 2 * 1000, // every 2s to make it responsive.
|
||||
maxPolls: 120,
|
||||
maxPolls: 60,
|
||||
maxErrors: 10
|
||||
})
|
||||
.then(() => log.trace('[checkDonationPaid]', 'Polling completed'))
|
||||
@@ -126,8 +126,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
setInfo('')
|
||||
setIsLoading(false)
|
||||
setDonationInvoice(undefined)
|
||||
setDonationAmount(DEFAULT_DONATION_AMOUNT)
|
||||
setIsQRCodeVisible(false)
|
||||
setDonationAmount(DEFAULT_DONATION_AMOUNT)
|
||||
setIsInvoicePaid(false)
|
||||
}
|
||||
|
||||
@@ -179,20 +178,35 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
const onCreateDonation = async function () {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const memo = `Donation for ${ownName+MINIBITS_NIP05_DOMAIN}`
|
||||
const invoice = await MinibitsClient.createDonation(
|
||||
donationAmount,
|
||||
memo,
|
||||
pubkey
|
||||
)
|
||||
|
||||
if(invoice) {
|
||||
setDonationInvoice(invoice)
|
||||
const feeReserve = roundUp(donationAmount / 100, 0)
|
||||
const comment = `Donation for ${ownName+MINIBITS_NIP05_DOMAIN}`
|
||||
const feeReserve = roundUp(donationAmount / 100, 0)
|
||||
|
||||
if(!selectedBalance || donationAmount >= selectedBalance.balances['sat']! + feeReserve) {
|
||||
setIsQRCodeVisible(true)
|
||||
}
|
||||
if(selectedBalance && selectedBalance.balances['sat']! + feeReserve >= donationAmount) {
|
||||
|
||||
const addressParamsResult = await LnurlClient.getLnurlAddressParams(DONATION_LNURL_ADDRESS) // throws
|
||||
|
||||
return navigation.navigate('WalletNavigator', {
|
||||
screen: 'Transfer',
|
||||
params: {
|
||||
lnurlParams: addressParamsResult.lnurlParams,
|
||||
paymentOption: SendOption.LNURL_PAY,
|
||||
fixedAmount: donationAmount,
|
||||
unit: 'sat',
|
||||
comment,
|
||||
mintUrl: selectedBalance.mintUrl,
|
||||
isDonation: true,
|
||||
donationForName: ownName
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const invoice = await MinibitsClient.createDonation(
|
||||
donationAmount,
|
||||
comment,
|
||||
pubkey
|
||||
)
|
||||
|
||||
setDonationInvoice(invoice)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
@@ -211,22 +225,6 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onPayDonation = async function () {
|
||||
try {
|
||||
return navigation.navigate('WalletNavigator', {
|
||||
screen: 'Transfer',
|
||||
params: {
|
||||
encodedInvoice: donationInvoice?.payment_request,
|
||||
unit: userSettingsStore.preferredUnit || 'sat',
|
||||
paymentOption: SendOption.DONATION
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
// poll handler
|
||||
const checkDonationPaid = async function (): Promise<void> {
|
||||
try {
|
||||
@@ -331,70 +329,13 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
text={translate("contactsScreen.ownName.payToGetOwnName", { name: ownName+MINIBITS_NIP05_DOMAIN })}
|
||||
style={[$supportText, {color: hint}]}
|
||||
/>
|
||||
{isQRcodeVisible && (
|
||||
<QRCodeBlock
|
||||
qrCodeData={donationInvoice.payment_request}
|
||||
title='Lightning invoice to pay'
|
||||
type='Bolt11Invoice'
|
||||
size={270}
|
||||
/>
|
||||
)}
|
||||
{(!!selectedBalance && selectedBalance.balances['sat']! > donationAmount) ? (
|
||||
<>
|
||||
<ListItem
|
||||
text='Invoice'
|
||||
subText={donationInvoice.payment_request.slice(0, 20) + '...'}
|
||||
RightComponent={
|
||||
<CurrencyAmount
|
||||
amount={donationAmount}
|
||||
currencyCode={CurrencyCode.SAT}
|
||||
size='medium'
|
||||
/>
|
||||
}
|
||||
topSeparator={true}
|
||||
bottomSeparator={true}
|
||||
leftIcon='faBolt'
|
||||
onPress={onCopyInvoice}
|
||||
/>
|
||||
<Text style={[$supportText, {color: hint}]} text={`Pay from`} />
|
||||
<MintListItem
|
||||
mint={mintsStore.findByUrl(selectedBalance.mintUrl)!}
|
||||
mintBalance={selectedBalance}
|
||||
selectedUnit='sat'
|
||||
isSelectable={true}
|
||||
isSelected={true}
|
||||
separator='both'
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="default"
|
||||
style={{marginRight: spacing.small}}
|
||||
tx='contactsScreen.ownName.ctaPay'
|
||||
onPress={onPayDonation}
|
||||
/>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx='common.cancel'
|
||||
onPress={resetState}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View>
|
||||
<Text
|
||||
size='xs'
|
||||
style={{textAlign: 'center', margin: spacing.medium}}
|
||||
tx="contactsScreen.ownName.insufficient"
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx='common.cancel'
|
||||
onPress={resetState}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<QRCodeBlock
|
||||
qrCodeData={donationInvoice.payment_request}
|
||||
title='Lightning invoice to pay'
|
||||
type='Bolt11Invoice'
|
||||
size={270}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -553,6 +494,7 @@ const $ownNameInput: TextStyle = {
|
||||
|
||||
const $ownNameDomain: TextStyle = {
|
||||
marginRight: spacing.small,
|
||||
marginLeft: -spacing.small,
|
||||
borderTopRightRadius: spacing.extraSmall,
|
||||
borderBottomRightRadius: spacing.extraSmall,
|
||||
padding: spacing.extraSmall,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useState, useCallback, useEffect} from 'react'
|
||||
import {Alert, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {spacing, useThemeColor, colors} from '../theme'
|
||||
import {AppStackScreenProps, SettingsStackScreenProps, WalletStackScreenProps} from '../navigation'
|
||||
import {AppStackScreenProps} from '../navigation'
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
@@ -21,7 +21,7 @@ import { SyncStateTaskResult, WalletTask } from '../services/walletService'
|
||||
import EventEmitter from '../utils/eventEmitter'
|
||||
import { translate } from '../i18n'
|
||||
|
||||
export enum RecoveryOption {
|
||||
/* export enum RecoveryOption {
|
||||
SEND_TOKEN = 'SEND_TOKEN',
|
||||
PASTE_OR_SCAN_INVOICE = 'PASTE_OR_SCAN_INVOICE',
|
||||
SHOW_TOKEN = 'SHOW_TOKEN',
|
||||
@@ -29,7 +29,7 @@ export enum RecoveryOption {
|
||||
LNURL_PAY = 'LNURL_PAY',
|
||||
LNURL_ADDRESS = 'LNURL_ADDRESS',
|
||||
DONATION = 'DONATION',
|
||||
}
|
||||
} */
|
||||
|
||||
export const RecoveryOptionsScreen: FC<AppStackScreenProps<'RecoveryOptions'>> = observer(
|
||||
function RecoveryOptionsScreen({route, navigation}) {
|
||||
|
||||
@@ -70,7 +70,7 @@ export enum SendOption {
|
||||
PAY_PAYMENT_REQUEST = 'PAY_PAYMENT_REQUEST',
|
||||
LNURL_PAY = 'LNURL_PAY',
|
||||
LNURL_ADDRESS = 'LNURL_ADDRESS',
|
||||
DONATION = 'DONATION',
|
||||
// DONATION = 'DONATION',
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android' &&
|
||||
|
||||
@@ -65,7 +65,7 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
|
||||
const amountInputRef = useRef<TextInput>(null)
|
||||
const lnurlCommentInputRef = useRef<TextInput>(null)
|
||||
|
||||
const {proofsStore, mintsStore, paymentRequestsStore, walletStore} = useStores()
|
||||
const {proofsStore, mintsStore, paymentRequestsStore, walletStore, walletProfileStore} = useStores()
|
||||
// const {walletStore} = nonPersistedStores
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
@@ -83,6 +83,7 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
|
||||
const [lnurlDescription, setLnurlDescription] = useState('')
|
||||
const [lnurlPayCommentAllowed, setLnurlPayCommentAllowed] = useState(0)
|
||||
const [lnurlPayComment, setLnurlPayComment] = useState('')
|
||||
const [donationForName, setDonationForName] = useState<string | undefined>()
|
||||
const [availableMintBalances, setAvailableMintBalances] = useState<MintBalance[]>([])
|
||||
const [mintBalanceToTransferFrom, setMintBalanceToTransferFrom] = useState<MintBalance | undefined>()
|
||||
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
|
||||
@@ -90,6 +91,7 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
|
||||
const [info, setInfo] = useState('')
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isAmountEditable, setIsAmountEditable] = useState(true)
|
||||
const [isNotEnoughFunds, setIsNotEnoughFunds] = useState(false)
|
||||
const [isInvoiceDonation, setIsInvoiceDonation] = useState(false)
|
||||
const [isTransferTaskSentToQueue, setIsTransferTaskSentToQueue] = useState(false)
|
||||
@@ -181,13 +183,27 @@ useFocusEffect(
|
||||
|
||||
const handleLnurlPay = async () => {
|
||||
try {
|
||||
const {lnurlParams, unit} = route.params
|
||||
// amountSats allows to default amount to be paid to a lightning address
|
||||
const {lnurlParams, unit, fixedAmount, comment, isDonation, donationForName} = route.params
|
||||
|
||||
log.trace('[handleLnurlPay] start', {unit})
|
||||
|
||||
if (!lnurlParams) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, translate('missingLNURLParamsError'))
|
||||
}
|
||||
|
||||
if(fixedAmount && fixedAmount > 0) {
|
||||
setIsAmountEditable(false)
|
||||
}
|
||||
|
||||
if(comment) {
|
||||
setLnurlPayComment(comment)
|
||||
}
|
||||
|
||||
if(isDonation) {
|
||||
setIsInvoiceDonation(true)
|
||||
setDonationForName(donationForName)
|
||||
}
|
||||
|
||||
const metadata = lnurlParams.decodedMetadata
|
||||
|
||||
@@ -208,6 +224,7 @@ useFocusEffect(
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ('commentAllowed' in lnurlParams && lnurlParams.commentAllowed > 0) {
|
||||
setLnurlPayCommentAllowed(lnurlParams.commentAllowed)
|
||||
}
|
||||
@@ -222,16 +239,16 @@ useFocusEffect(
|
||||
}
|
||||
}
|
||||
|
||||
const amountSats = roundUp(lnurlParams.minSendable / 1000, 0)
|
||||
const defaultAmount = fixedAmount || roundUp(lnurlParams.minSendable / 1000, 0)
|
||||
|
||||
// Set minSendable into amountToTransfer in unit currency
|
||||
if (unit !== 'sat') {
|
||||
const rate = await walletStore.getExchangeRate(getCurrency(unit).code)
|
||||
const amountUnit = convertToFromSats(amountSats, CurrencyCode.SAT, rate)
|
||||
const defaultAmountUnit = convertToFromSats(defaultAmount, CurrencyCode.SAT, rate)
|
||||
|
||||
log.trace('[handleLnurlPay] minSendable conversion from SAT', {amountSats, rate, amountUnit})
|
||||
log.trace('[handleLnurlPay] minSendable conversion from SAT', {defaultAmount, rate, defaultAmountUnit})
|
||||
|
||||
setAmountToTransfer(`${numbro(amountUnit / getCurrency(unit).precision)
|
||||
setAmountToTransfer(`${numbro(defaultAmountUnit / getCurrency(unit).precision)
|
||||
.format({
|
||||
thousandSeparated: true,
|
||||
mantissa: getCurrency(unit).mantissa
|
||||
@@ -239,7 +256,7 @@ useFocusEffect(
|
||||
)
|
||||
|
||||
} else {
|
||||
setAmountToTransfer(`${numbro(amountSats)
|
||||
setAmountToTransfer(`${numbro(defaultAmount)
|
||||
.format({
|
||||
thousandSeparated: true,
|
||||
mantissa: 0
|
||||
@@ -253,26 +270,6 @@ useFocusEffect(
|
||||
}
|
||||
}
|
||||
|
||||
const handleDonation = () => {
|
||||
try {
|
||||
const {encodedInvoice} = route.params
|
||||
|
||||
if (!encodedInvoice) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing donation invoice.')
|
||||
}
|
||||
|
||||
if(unit !== 'sat') {
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Donations can currently be paid only with ${CurrencyCode.SAT} balances.`)
|
||||
}
|
||||
|
||||
log.trace('[handleDonation]', {encodedInvoice})
|
||||
|
||||
setIsInvoiceDonation(true)
|
||||
onEncodedInvoice(encodedInvoice)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
if(paymentOption && paymentOption === SendOption.PASTE_OR_SCAN_INVOICE) {
|
||||
handleInvoice()
|
||||
@@ -286,10 +283,6 @@ useFocusEffect(
|
||||
handleLnurlPay()
|
||||
}
|
||||
|
||||
if(paymentOption && paymentOption === SendOption.DONATION) {
|
||||
handleDonation()
|
||||
}
|
||||
|
||||
|
||||
}, [route.params?.paymentOption]),
|
||||
)
|
||||
@@ -400,11 +393,20 @@ useEffect(() => {
|
||||
}
|
||||
|
||||
} else {
|
||||
if(!isInvoiceDonation) { // Donation has own polling to avoid paying with test ecash and triggers own ResultModal on paid invoice
|
||||
setResultModalInfo({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
if(isInvoiceDonation && donationForName) {
|
||||
|
||||
await walletProfileStore.updateName(donationForName)
|
||||
|
||||
setResultModalInfo({
|
||||
status,
|
||||
message: `Donation for ${donationForName} has been successfully paid and your wallet address has been updated. Thank you!`,
|
||||
})
|
||||
|
||||
} else {
|
||||
setResultModalInfo({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
// update related paymentRequest status if exists
|
||||
@@ -421,9 +423,7 @@ useEffect(() => {
|
||||
setFinalFee(finalFee)
|
||||
}
|
||||
|
||||
if(!isInvoiceDonation || error) {
|
||||
toggleResultModal()
|
||||
}
|
||||
toggleResultModal()
|
||||
}
|
||||
|
||||
// Subscribe to the task result event
|
||||
@@ -450,8 +450,10 @@ const resetState = function () {
|
||||
setMintBalanceToTransferFrom(undefined)
|
||||
setTransactionStatus(undefined)
|
||||
setInfo('')
|
||||
setDonationForName(undefined)
|
||||
setError(undefined)
|
||||
setIsLoading(false)
|
||||
setIsLoading(false)
|
||||
setIsAmountEditable(true)
|
||||
setIsInvoiceDonation(false)
|
||||
setIsTransferTaskSentToQueue(false)
|
||||
setIsResultModalVisible(false)
|
||||
@@ -559,7 +561,10 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
|
||||
navigation.setParams({encodedInvoice: undefined})
|
||||
navigation.setParams({paymentRequest: undefined})
|
||||
navigation.setParams({lnurlParams: undefined})
|
||||
navigation.setParams({paymentOption: undefined})
|
||||
navigation.setParams({paymentOption: undefined})
|
||||
navigation.setParams({fixedAmount: undefined})
|
||||
navigation.setParams({isDonation: undefined})
|
||||
navigation.setParams({donationForName: undefined})
|
||||
|
||||
const invoice = LightningUtils.decodeInvoice(encoded)
|
||||
const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice)
|
||||
@@ -572,6 +577,7 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
|
||||
|
||||
if(!isInternetReachable) setInfo(translate('common.offlinePretty'));
|
||||
|
||||
setIsAmountEditable(false)
|
||||
setEncodedInvoice(encoded)
|
||||
setInvoice(invoice)
|
||||
setInvoiceExpiry(expiresAt)
|
||||
@@ -715,7 +721,7 @@ const amountInputColor = useThemeColor('amountInput')
|
||||
maxLength={9}
|
||||
keyboardType="numeric"
|
||||
selectTextOnFocus={true}
|
||||
editable={encodedInvoice ? false : true}
|
||||
editable={isAmountEditable ? true : false}
|
||||
/>
|
||||
|
||||
{encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
|
||||
|
||||
@@ -202,7 +202,7 @@ const navigateWithIncomingData = async function (
|
||||
|
||||
return navigation.navigate('Transfer', {
|
||||
lnurlParams: addressParamsResult.lnurlParams,
|
||||
paymentOption: SendOption.LNURL_PAY,
|
||||
paymentOption: SendOption.LNURL_PAY,
|
||||
unit,
|
||||
mintUrl
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ const getLnurlAddressParams = async (lnurlAddress: string) => {
|
||||
headers,
|
||||
})
|
||||
|
||||
log.trace(`Got LNURL address params from ${domain}`, lnurlParams, 'getLnurlAddressParams')
|
||||
log.trace(`[getLnurlAddressParams] Got LNURL address params from ${domain}`, lnurlParams)
|
||||
|
||||
if(lnurlParams.status && lnurlParams.status === 'ERROR') {
|
||||
throw new AppError(Err.CONNECTION_ERROR, lnurlParams.reason, {domain, caller: 'getLnurlAddressParams'})
|
||||
|
||||
Reference in New Issue
Block a user