Skip to content

Commit

Permalink
feat(BuySell): indicate quote changes to user
Browse files Browse the repository at this point in the history
  • Loading branch information
dkremniov-bc committed Nov 16, 2022
1 parent 65da9a3 commit d56181a
Show file tree
Hide file tree
Showing 28 changed files with 1,117 additions and 120 deletions.
1 change: 1 addition & 0 deletions config/jest/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom'
4 changes: 4 additions & 0 deletions packages/blockchain-wallet-v4-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"<rootDir>/../../config/jest/jest.shim.js",
"<rootDir>/../../config/jest/jest.config.js"
],
"setupFilesAfterEnv": ["<rootDir>/../../config/jest/jest.setup.js"],
"snapshotSerializers": [
"<rootDir>/../../node_modules/enzyme-to-json/serializer"
],
Expand Down Expand Up @@ -164,8 +165,11 @@
},
"devDependencies": {
"jest-styled-components": "6.3.3",
"mockdate": "3.0.5",
"@types/applepayjs": "3.0.4",
"@types/enzyme": "3.10.11",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "6.0.0",
"@graphql-codegen/cli": "2.5.0",
"@graphql-codegen/introspection": "2.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,8 @@
"modals.simplebuy.paymentmethods": "Payment Methods",
"modals.simplebuy.payusing": "Pay Using",
"modals.simplebuy.processing": "Processing…",
"modals.simplebuy.quote.countdown": "New quote in:",
"modals.simplebuy.quote.soon": "soon",
"modals.simplebuy.recurringbuy.success": "We will buy {amount} of {coin} {frequency} at that moment’s market price. Cancel this recurring buy at anytime.",
"modals.simplebuy.refresh": "Refresh",
"modals.simplebuy.rejected.bank_failed": "Please try making your purchase again. If this keeps happening, please",
Expand Down Expand Up @@ -1871,6 +1873,7 @@
"modals.transaction_detail.payment_method": "Payment Method",
"modals.transaction_detail.sub_title": "Transaction Details",
"modals.transaction_detail.title": "Card Transaction",
"modals.transaction_list.title": "All Activity",
"modals.transactions.report.download": "Download Report",
"modals.transactions.report.enddate": "end date",
"modals.transactions.report.generate": "Generate Report",
Expand Down Expand Up @@ -2024,6 +2027,7 @@
"scenes.debit_card.dashboard.manage_card.lock_card_subtitle": "Temporarily lock your card",
"scenes.debit_card.dashboard.manage_card.lock_card_title": "Lock Card",
"scenes.debit_card.dashboard.new_card": "Add New Card",
"scenes.debit_card.dashboard.see_all_transactions": "See All",
"scenes.debit_card.dashboard.transactions.empty_state": "Your most recent purchases will show up here",
"scenes.debit_card.dashboard.transactions.title": "Recent Transactions",
"scenes.debit_card.dashboard.transactions.title.payment_refund": "Refund {symbol} at {place}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ export type Props = {
children?: ReactNode
percentage: number
strokeWidth?: number
styles?: Parameters<typeof buildStyles>[0]
}

const CircularProgressBar = ({
children,
percentage,
strokeWidth = 8,
styles = {},
theme
}: Props & { theme: DefaultTheme }) => {
return (
<CircularProgressbarWithChildren
styles={buildStyles({
pathColor: theme.blue600,
trailColor: theme.grey000
trailColor: theme.grey000,
...styles
})}
strokeWidth={strokeWidth}
value={percentage}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { createSelector } from '@reduxjs/toolkit'

import { Remote } from '@core'
import { getDomains } from '@core/redux/walletOptions/selectors'
import { WalletOptionsType } from '@core/types'
import { selectors } from 'data'
import { RootState } from 'data/rootReducer'

import { DEFAULT_BS_BALANCE } from '../buySell/model'

export const getBankCredentials = (state: RootState) => state.components.brokerage.bankCredentials

export const getBankTransferAccounts = (state: RootState) =>
Expand Down Expand Up @@ -34,7 +31,7 @@ export const getIsFlow = (state: RootState) => state.components.brokerage.isFlow
export const getReason = (state: RootState) => state.components.brokerage.reason

export const getPlaidWalletHelperLink = createSelector(
(state: RootState) => selectors.core.walletOptions.getDomains(state),
(state: RootState) => getDomains(state),
(domainsR) => {
const { walletHelper } = domainsR.getOrElse({
walletHelper: 'https://wallet-helper.blockchain.com'
Expand All @@ -46,17 +43,3 @@ export const getPlaidWalletHelperLink = createSelector(

export const getCrossBorderLimits = (state: RootState) =>
state.components.brokerage.crossBorderLimits

export const getWithdrawableBalance = createSelector(
(state: RootState) => selectors.components.buySell.getBSBalances(state),
(state: RootState) => selectors.modules.profile.getUserCurrencies(state),
(sbBalancesR, userCurrenciesR) => {
const { defaultWalletCurrency } = userCurrenciesR.getOrFail('could not get user currencies')

return Remote.of(
sbBalancesR.getOrElse({
[defaultWalletCurrency]: DEFAULT_BS_BALANCE
})[defaultWalletCurrency]?.withdrawable || '0'
)
}
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import BigNumber from 'bignumber.js'
import { getQuote } from 'blockchain-wallet-v4-frontend/src/modals/BuySell/EnterAmount/Checkout/validation'
import { addSeconds, differenceInMilliseconds } from 'date-fns'
import { defaultTo, filter, prop } from 'ramda'
import { call, cancel, delay, fork, put, race, retry, select, take } from 'redux-saga/effects'

Expand All @@ -15,7 +14,6 @@ import {
BSPaymentMethodType,
BSPaymentTypes,
BSQuoteType,
BuyQuoteStateType,
CardAcquirer,
CardSuccessRateResponse,
ExtraKYCContext,
Expand All @@ -38,6 +36,7 @@ import {
BankPartners,
BankTransferAccountType,
BrokerageModalOriginType,
BuyQuoteStateType,
CustodialSanctionsEnum,
ModalName,
ProductEligibilityForUser,
Expand Down Expand Up @@ -80,7 +79,7 @@ import {
import * as S from './selectors'
import { actions as A } from './slice'
import * as T from './types'
import { getDirection, reversePair } from './utils'
import { getDirection, getQuoteRefreshConfig, reversePair } from './utils'

export const logLocation = 'components/buySell/sagas'

Expand Down Expand Up @@ -1380,18 +1379,21 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
buyQuotePaymentMethodId
)

const refreshConfig = getQuoteRefreshConfig({
currentDate: new Date(),
expireDate: new Date(quote.quoteExpiresAt)
})
yield put(
A.fetchBuyQuoteSuccess({
fee: quote.feeDetails.fee.toString(),
pair,
quote,
rate: parseInt(quote.price)
rate: parseInt(quote.price),
refreshConfig
})
)
const refresh = Math.abs(
differenceInMilliseconds(new Date(quote.quoteExpiresAt), addSeconds(new Date(), 10))
)
yield delay(refresh)

yield delay(refreshConfig.totalMs)
} catch (e) {
if (isNabuError(e)) {
yield put(A.fetchBuyQuoteFailure(e))
Expand Down Expand Up @@ -1425,8 +1427,6 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
const fetchSellQuote = function* ({ payload }: ReturnType<typeof A.fetchSellQuote>) {
while (true) {
try {
yield put(A.fetchSellQuoteLoading())

const { pair } = payload
const direction = getDirection(payload.account)
const quote: ReturnType<typeof api.getSwapQuote> = yield call(
Expand All @@ -1441,11 +1441,12 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
true
)

yield put(A.fetchSellQuoteSuccess({ quote, rate }))
const refresh = Math.abs(
differenceInMilliseconds(new Date(quote.expiresAt), addSeconds(new Date(), 10))
)
yield delay(refresh)
const refreshConfig = getQuoteRefreshConfig({
currentDate: new Date(),
expireDate: new Date(quote.expiresAt)
})
yield put(A.fetchSellQuoteSuccess({ quote, rate, refreshConfig }))
yield delay(refreshConfig.totalMs)
} catch (e) {
const error = errorHandler(e)
yield put(A.fetchSellQuoteFailure(error))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Remote from '@core/remote'
import { getBuyQuoteMemoizedByOrder } from 'data/components/buySell/selectors'
import { RootState } from 'data/rootReducer'

describe('buySell selectors', () => {
describe('getBuyQuoteMemoizedByOrder', () => {
const OLD_QUOTE = Remote.Success({
quote: {
quoteId: 'old-quote'
}
})
const OLD_ORDER = Remote.Success({
id: 'old-order'
})

const NEW_QUOTE = Remote.Success({
quote: {
quoteId: 'new-quote'
}
})
const NEW_ORDER = Remote.Success({
id: 'new-order'
})
const makeState = (buyQuote: typeof OLD_QUOTE, order: typeof OLD_ORDER) =>
({
components: {
buySell: {
buyQuote,
order
}
}
} as RootState)

describe('when cache is empty', () => {
it('should return up to date quote', () => {
expect(getBuyQuoteMemoizedByOrder(makeState(OLD_QUOTE, OLD_ORDER))).toBe(OLD_QUOTE)
})
})

describe('when quote changed but order is the same', () => {
it('should return cached quote', () => {
expect(getBuyQuoteMemoizedByOrder(makeState(NEW_QUOTE, OLD_ORDER))).toBe(OLD_QUOTE)
})
})

describe('when both quote and order changed', () => {
it('should return new quote', () => {
expect(getBuyQuoteMemoizedByOrder(makeState(NEW_QUOTE, NEW_ORDER))).toBe(NEW_QUOTE)
})
})
})

it.todo('other selectors')
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BigNumber from 'bignumber.js'
import { getQuote } from 'blockchain-wallet-v4-frontend/src/modals/BuySell/EnterAmount/Checkout/validation'
import memoize from 'fast-memoize'
import { head, isEmpty, isNil, lift } from 'ramda'
import { createSelector } from 'reselect'

Expand All @@ -11,7 +12,9 @@ import {
FiatType,
FiatTypeEnum
} from '@core/types'
import { model, selectors } from 'data'
import { getBankTransferAccounts } from 'data/components/brokerage/selectors'
import { getFormValues } from 'data/form/selectors'
import { components } from 'data/model'
import { RootState } from 'data/rootReducer'

import { convertBaseToStandard, convertStandardToBase } from '../exchange/services'
Expand All @@ -20,7 +23,7 @@ import { getRate } from '../swap/utils'
import { LIMIT } from './model'
import { BSCardStateEnum, BSCheckoutFormValuesType } from './types'

const { FORM_BS_CHECKOUT } = model.components.buySell
const { FORM_BS_CHECKOUT } = components.buySell

const hasEligibleFiatCurrency = (currency) =>
currency === FiatTypeEnum.USD || currency === FiatTypeEnum.GBP || currency === FiatTypeEnum.EUR
Expand Down Expand Up @@ -59,7 +62,7 @@ export const getDefaultPaymentMethod = (state: RootState) => {
const sbMethodsR = getBSPaymentMethods(state)
const actionType = getOrderType(state)
const sbBalancesR = getBSBalances(state)
const bankAccounts = selectors.components.brokerage.getBankTransferAccounts(state).getOrElse([])
const bankAccounts = getBankTransferAccounts(state).getOrElse([])

const transform = (
sbMethods: ExtractSuccess<typeof sbMethodsR>,
Expand Down Expand Up @@ -205,6 +208,23 @@ export const getCancelableOrder = createSelector(getBSOrders, (ordersR) => {

export const getBuyQuote = (state: RootState) => state.components.buySell.buyQuote

const makeGetBuyQuoteMemoizedByOrder = memoize(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(_: ReturnType<typeof getBSOrder>, state: RootState) => {
return getBuyQuote(state)
},
{
serializer: (args) => JSON.stringify(args[0]) // Use only first argument as memoization key
}
)

/**
* @returns Up to date quote only if order was also updated. Otherwise, previous cached quote.
*/
export const getBuyQuoteMemoizedByOrder = (state: RootState) => {
return makeGetBuyQuoteMemoizedByOrder(getBSOrder(state), state)
}

export const getSellQuote = (state: RootState) => state.components.buySell.sellQuote

export const getSellOrder = (state: RootState) => state.components.buySell.sellOrder
Expand All @@ -225,9 +245,10 @@ export const getPayment = (state: RootState) => state.components.buySell.payment

export const getIncomingAmount = (state: RootState) => {
const quoteR = getSellQuote(state)
const values = (selectors.form.getFormValues(FORM_BS_CHECKOUT)(
state
) as BSCheckoutFormValuesType) || { amount: '0', fix: 'CRYPTO' }
const values = (getFormValues(FORM_BS_CHECKOUT)(state) as BSCheckoutFormValuesType) || {
amount: '0',
fix: 'CRYPTO'
}

return lift(({ quote, rate }: ExtractSuccess<typeof quoteR>) => {
const fromCoin = getInputFromPair(quote.pair)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
BSPaymentMethodType,
BSPaymentTypes,
BSQuoteType,
BuyQuoteStateType,
CoinType,
CrossBorderLimits,
CrossBorderLimitsPayload,
Expand All @@ -28,7 +27,6 @@ import {
ProviderDetailsType,
SDDEligibleType,
SDDVerifiedType,
SwapQuoteStateType,
SwapUserLimitsType,
TradeAccumulatedItem
} from '@core/types'
Expand All @@ -39,14 +37,16 @@ import {
BSCardStateEnum,
BSFixType,
BSShowModalOriginType,
BuyQuoteStateType,
InitializeCheckout,
ModalOriginType,
SellQuoteStateType,
StepActionsPayload,
SwapAccountType
} from 'data/types'

import { getCoinFromPair, getFiatFromPair } from './model'
import { BSCardSuccessRateType, BuySellState, BuySellStepType } from './types'
import { BSCardSuccessRateType, BuySellState } from './types'

const initialState: BuySellState = {
account: Remote.NotAsked,
Expand Down Expand Up @@ -367,10 +367,7 @@ const buySellSlice = createSlice({
fetchSellQuoteFailure: (state, action: PayloadAction<string>) => {
state.sellQuote = Remote.Failure(action.payload)
},
fetchSellQuoteLoading: (state) => {
state.sellQuote = Remote.Loading
},
fetchSellQuoteSuccess: (state, action: PayloadAction<SwapQuoteStateType>) => {
fetchSellQuoteSuccess: (state, action: PayloadAction<SellQuoteStateType>) => {
state.sellQuote = Remote.Success(action.payload)
},
handleBuyMaxAmountClick: (
Expand Down

0 comments on commit d56181a

Please sign in to comment.