Skip to content

Commit

Permalink
ui: Order submit button state management (#2534)
Browse files Browse the repository at this point in the history
* Order submit button state disabled if insufficient balance

---------

Co-authored-by: martonp <marci93@gmail.com>
  • Loading branch information
peterzen and martonp authored Nov 5, 2023
1 parent 90122f3 commit 08c04a1
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 11 deletions.
4 changes: 4 additions & 0 deletions client/webserver/site/src/css/market.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ div[data-handler=markets] {
padding: 5px 25px;
border-radius: 3px;
color: #555;

&:disabled {
opacity: 0.4;
}
}

button:hover,
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/site/src/html/markets.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@

{{- /* SUBMIT ORDER BUTTON */ -}}
<div class="text-end">
<button id="submitBttn" type="button" class="my-1 fs14 submit text-center buygreen-bg"></button> {{/* textContent set by script */}}
<button id="submitBttn" type="button" class="my-1 fs14 submit text-center buygreen-bg" disabled></button> {{/* textContent set by script */}}
</div>
</div>
<div class="fs15 pt-3 text-center d-hide errcolor text-break" id="orderErr"></div>
Expand Down
8 changes: 8 additions & 0 deletions client/webserver/site/src/js/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,21 @@ export const ID_BROWSER_NTFN_ORDERS = 'ID_BROWSER_NTFN_ORDERS'
export const ID_BROWSER_NTFN_MATCHES = 'ID_BROWSER_NTFN_MATCHES'
export const ID_BROWSER_NTFN_BONDS = 'ID_BROWSER_NTFN_BONDS'
export const ID_BROWSER_NTFN_CONNECTIONS = 'ID_BROWSER_NTFN_CONNECTIONS'
export const ID_ORDER_BUTTON_BUY_BALANCE_ERROR = 'ID_ORDER_BUTTON_BUY_BALANCE_ERROR'
export const ID_ORDER_BUTTON_SELL_BALANCE_ERROR = 'ID_ORDER_BUTTON_SELL_BALANCE_ERROR'
export const ID_ORDER_BUTTON_QTY_ERROR = 'ID_ORDER_BUTTON_QTY_ERROR'
export const ID_ORDER_BUTTON_QTY_RATE_ERROR = 'ID_ORDER_BUTTON_QTY_RATE_ERROR'

export const enUS: Locale = {
[ID_NO_PASS_ERROR_MSG]: 'password cannot be empty',
[ID_NO_APP_PASS_ERROR_MSG]: 'app password cannot be empty',
[ID_PASSWORD_NOT_MATCH]: 'passwords do not match',
[ID_SET_BUTTON_BUY]: 'Place order to buy {{ asset }}',
[ID_SET_BUTTON_SELL]: 'Place order to sell {{ asset }}',
[ID_ORDER_BUTTON_BUY_BALANCE_ERROR]: 'Insufficient balance to buy.',
[ID_ORDER_BUTTON_SELL_BALANCE_ERROR]: 'Insufficient balance to sell.',
[ID_ORDER_BUTTON_QTY_ERROR]: 'Order quantity must be specified.',
[ID_ORDER_BUTTON_QTY_RATE_ERROR]: 'Order quantity and price must be specified.',
[ID_OFF]: 'off',
[ID_READY]: 'ready',
[ID_LOCKED]: 'locked',
Expand Down
119 changes: 109 additions & 10 deletions client/webserver/site/src/js/markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,11 @@ export default class MarketsPage extends BasePage {
const maxSell = this.market.maxSell
if (!maxSell) return
page.lotField.value = String(maxSell.swap.lots)
} else page.lotField.value = String(this.market.maxBuys[this.adjustedRate()].swap.lots)
} else {
const maxBuy = this.market.maxBuys[this.adjustedRate()]
if (!maxBuy) return
page.lotField.value = String(maxBuy.swap.lots)
}
this.lotChanged()
})

Expand Down Expand Up @@ -568,6 +572,12 @@ export default class MarketsPage extends BasePage {
this.setOrderBttnText()
this.setOrderVisibility()
this.drawChartLines()
if (!this.isLimit()) {
this.marketBuyChanged()
} else {
this.currentOrder = this.parseOrder()
this.updateOrderBttnState()
}
}

setSell () {
Expand All @@ -579,6 +589,8 @@ export default class MarketsPage extends BasePage {
this.setOrderBttnText()
this.setOrderVisibility()
this.drawChartLines()
this.currentOrder = this.parseOrder()
this.updateOrderBttnState()
}

/* hasPendingBonds is true if there are pending bonds */
Expand Down Expand Up @@ -716,6 +728,7 @@ export default class MarketsPage extends BasePage {
this.previewQuoteAmt(false)
}
}
this.updateOrderBttnState()
}

/* resolveOrderFormVisibility displays or hides the 'orderForm' based on
Expand Down Expand Up @@ -1008,6 +1021,68 @@ export default class MarketsPage extends BasePage {
} else this.page.submitBttn.textContent = intl.prep(intl.ID_SET_BUTTON_BUY, { asset: Doc.shortSymbol(this.market.baseCfg.unitInfo.conventional.unit) })
}

setOrderBttnEnabled (isEnabled: boolean, disabledTooltipMsg?: string) {
const btn = this.page.submitBttn
if (isEnabled) {
btn.removeAttribute('disabled')
btn.removeAttribute('title')
} else {
btn.setAttribute('disabled', 'true')
if (disabledTooltipMsg) btn.setAttribute('title', disabledTooltipMsg)
}
}

updateOrderBttnState () {
const { market: mkt, currentOrder: { qty: orderQty, rate: orderRate, isLimit, sell } } = this
const baseWallet = app().assets[this.market.base.id].wallet
const quoteWallet = app().assets[mkt.quote.id].wallet
if (!baseWallet || !quoteWallet) return

if (orderQty <= 0 || orderQty < mkt.cfg.lotsize) {
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_ERROR))
return
}

// Market orders
if (!isLimit) {
if (sell) {
this.setOrderBttnEnabled(orderQty <= baseWallet.balance.available, intl.prep(intl.ID_ORDER_BUTTON_SELL_BALANCE_ERROR))
} else {
this.setOrderBttnEnabled(orderQty <= quoteWallet.balance.available, intl.prep(intl.ID_ORDER_BUTTON_BUY_BALANCE_ERROR))
}
return
}

if (!orderRate) {
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_RATE_ERROR))
return
}

// Limit sell
if (sell) {
if (baseWallet.balance.available < mkt.cfg.lotsize) {
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_SELL_BALANCE_ERROR))
return
}
if (mkt.maxSell) {
this.setOrderBttnEnabled(orderQty <= mkt.maxSell.swap.value, intl.prep(intl.ID_ORDER_BUTTON_SELL_BALANCE_ERROR))
}
return
}

// Limit buy
const rate = this.adjustedRate()
const aLot = mkt.cfg.lotsize * (rate / OrderUtil.RateEncodingFactor)
if (quoteWallet.balance.available < aLot) {
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_BUY_BALANCE_ERROR))
return
}
if (mkt.maxBuys[rate]) {
const enable = orderQty <= mkt.maxBuys[rate].swap.lots * mkt.cfg.lotsize
this.setOrderBttnEnabled(enable, intl.prep(intl.ID_ORDER_BUTTON_BUY_BALANCE_ERROR))
}
}

setCandleDurBttns () {
const { page, market } = this
Doc.empty(page.durBttnBox)
Expand Down Expand Up @@ -1118,6 +1193,7 @@ export default class MarketsPage extends BasePage {
this.setRegistrationStatusVisibility()
this.resolveOrderFormVisibility()
this.setOrderBttnText()
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_RATE_ERROR))
this.setCandleDurBttns()
this.previewQuoteAmt(false)
this.updateTitle()
Expand Down Expand Up @@ -1233,7 +1309,7 @@ export default class MarketsPage extends BasePage {
previewQuoteAmt (show: boolean) {
const page = this.page
if (!this.market.base || !this.market.quote) return // Not a supported asset
const order = this.parseOrder()
const order = this.currentOrder = this.parseOrder()
const adjusted = this.adjustedRate()
page.orderErr.textContent = ''
if (adjusted) {
Expand Down Expand Up @@ -1270,12 +1346,15 @@ export default class MarketsPage extends BasePage {
const baseWallet = app().assets[mkt.base.id].wallet
if (baseWallet.balance.available < mkt.cfg.lotsize) {
this.setMaxOrder(null)
this.updateOrderBttnState()
return
}
if (mkt.maxSell) {
this.setMaxOrder(mkt.maxSell.swap)
this.updateOrderBttnState()
return
}

if (mkt.maxSellRequested) return
mkt.maxSellRequested = true
// We only fetch pre-sell once per balance update, so don't delay.
Expand All @@ -1284,6 +1363,7 @@ export default class MarketsPage extends BasePage {
mkt.maxSell = res.maxSell
mkt.sellBalance = baseWallet.balance.available
this.setMaxOrder(res.maxSell.swap)
this.updateOrderBttnState()
})
}

Expand All @@ -1298,10 +1378,12 @@ export default class MarketsPage extends BasePage {
const aLot = mkt.cfg.lotsize * (rate / OrderUtil.RateEncodingFactor)
if (quoteWallet.balance.available < aLot) {
this.setMaxOrder(null)
this.updateOrderBttnState()
return
}
if (mkt.maxBuys[rate]) {
this.setMaxOrder(mkt.maxBuys[rate].swap)
this.updateOrderBttnState()
return
}
// 0 delay for first fetch after balance update or market change, otherwise
Expand All @@ -1311,6 +1393,7 @@ export default class MarketsPage extends BasePage {
mkt.maxBuys[rate] = res.maxBuy
mkt.buyBalance = app().assets[mkt.quote.id].wallet.balance.available
this.setMaxOrder(res.maxBuy.swap)
this.updateOrderBttnState()
})
}

Expand Down Expand Up @@ -2512,13 +2595,10 @@ export default class MarketsPage extends BasePage {
const avail = note.balance.available
switch (note.assetID) {
case mkt.baseCfg.id:
// If we're not showing the max order panel yet, don't do anything.
if (!mkt.maxSell) break
if (typeof mkt.sellBalance === 'number' && mkt.sellBalance !== avail) mkt.maxSell = null
if (this.isSell()) this.preSell()
break
case mkt.quoteCfg.id:
if (!Object.keys(mkt.maxBuys).length) break
if (typeof mkt.buyBalance === 'number' && mkt.buyBalance !== avail) mkt.maxBuys = {}
if (!this.isSell()) this.preBuy()
}
Expand Down Expand Up @@ -2590,12 +2670,19 @@ export default class MarketsPage extends BasePage {
page.lotField.value = '0'
page.qtyField.value = ''
this.previewQuoteAmt(false)
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_ERROR))
return
}
const lotSize = this.market.cfg.lotsize
const orderQty = lots * lotSize
page.lotField.value = String(lots)
// Conversion factor must be a multiple of 10.
page.qtyField.value = String(lots * lotSize / this.market.baseUnitInfo.conventional.conversionFactor)
page.qtyField.value = String(orderQty / this.market.baseUnitInfo.conventional.conversionFactor)

if (!this.isLimit() && this.isSell()) {
const baseWallet = app().assets[this.market.base.id].wallet
this.setOrderBttnEnabled(orderQty <= baseWallet.balance.available, intl.prep(intl.ID_ORDER_BUTTON_SELL_BALANCE_ERROR))
}
this.previewQuoteAmt(true)
}

Expand All @@ -2605,7 +2692,7 @@ export default class MarketsPage extends BasePage {
*/
quantityChanged (finalize: boolean) {
const page = this.page
const order = this.parseOrder()
const order = this.currentOrder = this.parseOrder()
if (order.qty < 0) {
page.lotField.value = '0'
page.qtyField.value = ''
Expand All @@ -2614,9 +2701,11 @@ export default class MarketsPage extends BasePage {
}
const lotSize = this.market.cfg.lotsize
const lots = Math.floor(order.qty / lotSize)
const adjusted = lots * lotSize
const adjusted = order.qty = this.currentOrder.qty = lots * lotSize
page.lotField.value = String(lots)

if (!order.isLimit && !order.sell) return

// Conversion factor must be a multiple of 10.
if (finalize) page.qtyField.value = String(adjusted / this.market.baseUnitInfo.conventional.conversionFactor)
this.previewQuoteAmt(true)
Expand All @@ -2630,14 +2719,21 @@ export default class MarketsPage extends BasePage {
const page = this.page
const qty = convertToAtoms(page.mktBuyField.value || '', this.market.quoteUnitInfo.conventional.conversionFactor)
const gap = this.midGap()
if (qty > 0) {
const quoteWallet = app().assets[this.market.quote.id].wallet
this.setOrderBttnEnabled(qty <= quoteWallet.balance.available, intl.prep(intl.ID_ORDER_BUTTON_BUY_BALANCE_ERROR))
} else {
this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_ERROR))
}
if (!gap || !qty) {
page.mktBuyLots.textContent = '0'
page.mktBuyScore.textContent = '0'
return
}
const lotSize = this.market.cfg.lotsize
const received = qty / gap
page.mktBuyLots.textContent = (received / lotSize).toFixed(1)
const lots = (received / lotSize)
page.mktBuyLots.textContent = lots.toFixed(1)
page.mktBuyScore.textContent = Doc.formatCoinValue(received, this.market.baseUnitInfo)
}

Expand All @@ -2652,9 +2748,11 @@ export default class MarketsPage extends BasePage {
this.depthLines.input = []
this.drawChartLines()
this.page.rateField.value = '0'
this.previewQuoteAmt(true)
this.updateOrderBttnState()
return
}
const order = this.parseOrder()
const order = this.currentOrder = this.parseOrder()
const r = adjusted / this.market.rateConversionFactor
this.page.rateField.value = String(r)
this.depthLines.input = [{
Expand All @@ -2663,6 +2761,7 @@ export default class MarketsPage extends BasePage {
}]
this.drawChartLines()
this.previewQuoteAmt(true)
this.updateOrderBttnState()
}

/*
Expand Down

0 comments on commit 08c04a1

Please sign in to comment.