Skip to content

Commit

Permalink
Merge pull request #4186 from EdgeApp/sam/fiat-plugin-refactors
Browse files Browse the repository at this point in the history
- Refactor Fiat Plugin `enterAmount` API
  • Loading branch information
samholmes committed May 18, 2023
2 parents d20894b + 0e803e0 commit adbae34
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 182 deletions.
147 changes: 80 additions & 67 deletions src/plugins/gui/amountQuotePlugin.ts
Expand Up @@ -12,7 +12,7 @@ import { getTokenId } from '../../util/CurrencyInfoHelpers'
import { fetchInfo } from '../../util/network'
import { logEvent } from '../../util/tracking'
import { fuzzyTimeout } from '../../util/utils'
import { FiatPlugin, FiatPluginFactory, FiatPluginFactoryArgs, FiatPluginGetMethodsResponse, FiatPluginStartParams } from './fiatPluginTypes'
import { FiatPlugin, FiatPluginFactory, FiatPluginFactoryArgs, FiatPluginStartParams } from './fiatPluginTypes'
import { FiatProvider, FiatProviderAssetMap, FiatProviderGetQuoteParams, FiatProviderQuote } from './fiatProviderTypes'
import { createStore, getBestError, getRateFromQuote } from './pluginUtils'
import { banxaProvider } from './providers/banxaProvider'
Expand Down Expand Up @@ -139,6 +139,7 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi
let counter = 0
let bestQuote: FiatProviderQuote | undefined
let goodQuotes: FiatProviderQuote[] = []
let lastSourceFieldNum: number

// HACK: Force EUR for sepa Bity, since Bity doesn't accept USD, a common
// wallet fiat selection regardless of region.
Expand All @@ -147,26 +148,37 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi
const displayFiatCurrencyCode = fiatCurrencyCode.replace('iso:', '')
const isBuy = direction === 'buy'

let enterAmountMethods: FiatPluginGetMethodsResponse
logEvent(isBuy ? 'Buy_Quote' : 'Sell_Quote')

// Navigate to scene to have user enter amount
showUi.enterAmount({
headerTitle: isBuy ? sprintf(lstrings.fiat_plugin_buy_currencycode, currencyCode) : sprintf(lstrings.fiat_plugin_sell_currencycode_s, currencyCode),
isBuy,
initState: {
value1: '500'
},
label1: sprintf(lstrings.fiat_plugin_amount_currencycode, displayFiatCurrencyCode),
label2: sprintf(lstrings.fiat_plugin_amount_currencycode, currencyCode),
initialAmount1: '500',
getMethods: (methods: FiatPluginGetMethodsResponse) => {
enterAmountMethods = methods
},
convertValue: async (sourceFieldNum: number, value: string): Promise<string | undefined> => {
async onChangeText() {},
async onFieldChange(event) {
const { stateManager } = event
const { sourceFieldNum, value } = event.value
if (!isValidInput(value)) {
if (enterAmountMethods != null)
enterAmountMethods.setStatusText({ statusText: lstrings.create_wallet_invalid_input, options: { textType: 'error' } })
stateManager.update({ statusText: { content: lstrings.create_wallet_invalid_input, textType: 'error' } })
return
}
bestQuote = undefined
goodQuotes = []
if (eq(value, '0')) return ''
lastSourceFieldNum = sourceFieldNum

const otherFieldKey = sourceFieldNum === 1 ? 'value2' : 'value1'
const spinnerKey = sourceFieldNum === 1 ? 'spinner2' : 'spinner1'

stateManager.update({ [spinnerKey]: true })

if (eq(value, '0')) {
stateManager.update({ [otherFieldKey]: '', [spinnerKey]: false })
return
}
const myCounter = ++counter
let quoteParams: FiatProviderGetQuoteParams
let sourceFieldCurrencyCode
Expand Down Expand Up @@ -220,76 +232,77 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi
if (goodQuotes.length === 0) {
// Find the best error to surface
const bestErrorText = getBestError(errors as any, sourceFieldCurrencyCode) ?? lstrings.fiat_plugin_buy_no_quote
if (enterAmountMethods != null) enterAmountMethods.setStatusText({ statusText: bestErrorText, options: { textType: 'error' } })
stateManager.update({ statusText: { content: bestErrorText, textType: 'error' }, [spinnerKey]: false })
return
}

// Find best quote factoring in pluginPriorities
bestQuote = getBestQuote(goodQuotes, priorityArray ?? [{}])
if (bestQuote == null) {
if (enterAmountMethods != null) enterAmountMethods.setStatusText({ statusText: lstrings.fiat_plugin_buy_no_quote, options: { textType: 'error' } })
stateManager.update({ statusText: { content: lstrings.fiat_plugin_buy_no_quote, textType: 'error' }, [spinnerKey]: false })
return
}

const exchangeRateText = getRateFromQuote(bestQuote, displayFiatCurrencyCode)
if (enterAmountMethods != null) {
const poweredByOnClick = async () => {
// 1. Show modal with all the valid quotes
const items = goodQuotes.map(quote => {
let text
if (sourceFieldNum === 1) {
// User entered a fiat value. Show the crypto value per partner
const localeAmount = formatNumber(toFixed(quote.cryptoAmount, 0, 6))
text = `(${localeAmount} ${quote.displayCurrencyCode})`
} else {
// User entered a crypto value. Show the fiat value per partner
const localeAmount = formatNumber(toFixed(quote.fiatAmount, 0, 2))
text = `(${localeAmount} ${quote.fiatCurrencyCode.replace('iso:', '')})`
}
const out = {
text,
name: quote.pluginDisplayName,
icon: getPartnerIconUri(quote.partnerIcon)
}
return out
})
const rowName = await showUi.listModal({
title: 'Providers',
selected: bestQuote?.pluginDisplayName ?? '',
items
})
if (bestQuote == null) return

// 2. Set the best quote to the one chosen by user (if any is chosen)
if (rowName != null && rowName !== bestQuote.pluginDisplayName) {
bestQuote = goodQuotes.find(quote => quote.pluginDisplayName === rowName)
if (bestQuote == null) return

// 3. Set the status text and powered by
const statusText = getRateFromQuote(bestQuote, displayFiatCurrencyCode)
enterAmountMethods.setStatusText({ statusText })
enterAmountMethods.setPoweredBy({ poweredByText: bestQuote.pluginDisplayName, poweredByIcon: bestQuote.partnerIcon, poweredByOnClick })

logEvent(isBuy ? 'Buy_Quote_Change_Provider' : 'Sell_Quote_Change_Provider')

if (sourceFieldNum === 1) {
enterAmountMethods.setValue2(bestQuote.cryptoAmount)
} else {
enterAmountMethods.setValue1(bestQuote.fiatAmount)
}
}
stateManager.update({
statusText: { content: exchangeRateText },
poweredBy: { poweredByText: bestQuote.pluginDisplayName, poweredByIcon: bestQuote.partnerIcon },
[otherFieldKey]: sourceFieldNum === 1 ? toFixed(bestQuote.cryptoAmount, 0, 6) : toFixed(bestQuote.fiatAmount, 0, 2),
[spinnerKey]: false
})
},
async onPoweredByClick(event) {
const { stateManager } = event
// 1. Show modal with all the valid quotes
const items = goodQuotes.map(quote => {
let text
if (lastSourceFieldNum === 1) {
// User entered a fiat value. Show the crypto value per partner
const localeAmount = formatNumber(toFixed(quote.cryptoAmount, 0, 6))
text = `(${localeAmount} ${quote.displayCurrencyCode})`
} else {
// User entered a crypto value. Show the fiat value per partner
const localeAmount = formatNumber(toFixed(quote.fiatAmount, 0, 2))
text = `(${localeAmount} ${quote.fiatCurrencyCode.replace('iso:', '')})`
}
const out = {
text,
name: quote.pluginDisplayName,
icon: getPartnerIconUri(quote.partnerIcon)
}
return out
})
const rowName = await showUi.listModal({
title: 'Providers',
selected: bestQuote?.pluginDisplayName ?? '',
items
})
if (bestQuote == null) return

// 2. Set the best quote to the one chosen by user (if any is chosen)
if (rowName != null && rowName !== bestQuote.pluginDisplayName) {
bestQuote = goodQuotes.find(quote => quote.pluginDisplayName === rowName)
if (bestQuote == null) return

// 3. Set the status text and powered by
const statusText = getRateFromQuote(bestQuote, displayFiatCurrencyCode)
stateManager.update({
statusText: { content: statusText },
poweredBy: { poweredByText: bestQuote.pluginDisplayName, poweredByIcon: bestQuote.partnerIcon }
})

logEvent(isBuy ? 'Buy_Quote_Change_Provider' : 'Sell_Quote_Change_Provider')

if (lastSourceFieldNum === 1) {
stateManager.update({ value2: bestQuote.cryptoAmount })
} else {
stateManager.update({ value1: bestQuote.fiatAmount })
}

enterAmountMethods.setStatusText({ statusText: exchangeRateText })
enterAmountMethods.setPoweredBy({ poweredByText: bestQuote.pluginDisplayName, poweredByIcon: bestQuote.partnerIcon, poweredByOnClick })
}
if (sourceFieldNum === 1) {
return toFixed(bestQuote.cryptoAmount, 0, 6)
} else {
return toFixed(bestQuote.fiatAmount, 0, 2)
}
},
async onSubmit() {
logEvent(isBuy ? 'Buy_Quote_Next' : 'Sell_Quote_Next')

if (bestQuote == null) {
return
}
Expand Down
27 changes: 4 additions & 23 deletions src/plugins/gui/fiatPlugin.tsx
Expand Up @@ -12,13 +12,10 @@ import { SendScene2Params } from '../../components/scenes/SendScene2'
import { Airship, showError, showToastSpinner } from '../../components/services/AirshipInstance'
import { HomeAddress, SepaInfo } from '../../types/FormTypes'
import { GuiPlugin } from '../../types/GuiPluginTypes'
import { NavigationBase } from '../../types/routerTypes'
import { logEvent } from '../../util/tracking'
import { AppParamList, NavigationBase } from '../../types/routerTypes'
import {
FiatPaymentType,
FiatPluginAddressFormParams,
FiatPluginEnterAmountParams,
FiatPluginEnterAmountResponse,
FiatPluginListModalParams,
FiatPluginRegionCode,
FiatPluginSepaFormParams,
Expand All @@ -39,7 +36,6 @@ export const executePlugin = async (params: {
}): Promise<void> => {
const { disablePlugins = {}, account, direction, guiPlugin, navigation, paymentType, providerId, regionCode } = params
const { pluginId } = guiPlugin
const isBuy = direction === 'buy'

const showUi: FiatPluginUi = {
showToastSpinner,
Expand All @@ -62,23 +58,8 @@ export const executePlugin = async (params: {
))
return result
},
enterAmount(params: FiatPluginEnterAmountParams) {
const { headerTitle, label1, label2, initialAmount1, convertValue, getMethods, onSubmit } = params
logEvent(isBuy ? 'Buy_Quote' : 'Sell_Quote')

navigation.navigate('guiPluginEnterAmount', {
headerTitle,
label1,
label2,
initialAmount1,
getMethods,
convertValue,
onChangeText: async () => undefined,
onSubmit: async (value: FiatPluginEnterAmountResponse) => {
logEvent(isBuy ? 'Buy_Quote_Next' : 'Sell_Quote_Next')
onSubmit(value)
}
})
enterAmount(params: AppParamList['guiPluginEnterAmount']) {
navigation.navigate('guiPluginEnterAmount', params)
},
addressForm: async (params: FiatPluginAddressFormParams) => {
const { countryCode, headerTitle, headerIconUri, onSubmit } = params
Expand Down Expand Up @@ -132,7 +113,7 @@ export const executePlugin = async (params: {
})
})
},
popScene: async () => {
exitScene: async () => {
navigation.pop()
}
}
Expand Down
31 changes: 9 additions & 22 deletions src/plugins/gui/fiatPluginTypes.ts
Expand Up @@ -5,8 +5,9 @@ import { DisablePluginMap } from '../../actions/ExchangeInfoActions'
import { SendScene2Params } from '../../components/scenes/SendScene2'
import { HomeAddress, SepaInfo } from '../../types/FormTypes'
import { GuiPlugin } from '../../types/GuiPluginTypes'
import { AppParamList } from '../../types/routerTypes'
import { EdgeTokenId } from '../../types/types'
import { EnterAmountPoweredBy } from './scenes/FiatPluginEnterAmountScene'
import { StateManager } from './hooks/useStateManager'

export const asFiatDirection = asValue('buy', 'sell')
export type FiatDirection = ReturnType<typeof asFiatDirection>
Expand All @@ -21,25 +22,6 @@ export interface FiatPluginAddressFormParams {
onSubmit: (homeAddress: HomeAddress) => Promise<void>
}

export interface FiatPluginGetMethodsResponse {
setStatusText: (params: { statusText: string; options?: { textType?: 'warning' | 'error' } }) => void
setPoweredBy: (params: EnterAmountPoweredBy) => void
setValue1: (value: string) => void
setValue2: (value: string) => void
}

export interface FiatPluginEnterAmountParams {
headerTitle: string
isBuy: boolean
label1: string
label2: string
convertValue: (sourceFieldNum: number, value: string) => Promise<string | undefined>
onSubmit: (value: FiatPluginEnterAmountResponse) => Promise<void>
getMethods?: (methods: FiatPluginGetMethodsResponse) => void
initialAmount1?: string
headerIconUri?: string
}

export interface FiatPluginSepaFormParams {
headerTitle: string
headerIconUri?: string
Expand Down Expand Up @@ -97,11 +79,11 @@ export interface FiatPluginUi {
}>
showError: (error: Error) => Promise<void>
listModal: (params: FiatPluginListModalParams) => Promise<string | undefined>
enterAmount: (params: FiatPluginEnterAmountParams) => void
enterAmount: (params: AppParamList['guiPluginEnterAmount']) => void
addressForm: (params: FiatPluginAddressFormParams) => Promise<HomeAddress>
sepaForm: (params: FiatPluginSepaFormParams) => Promise<SepaInfo>
sepaTransferInfo: (params: FiatPluginSepaTransferParams) => Promise<void>
popScene: () => {}
exitScene: () => {}
send: (params: SendScene2Params) => Promise<void>
// showWebView: (params: { webviewUrl: string }) => Promise<void>
}
Expand Down Expand Up @@ -133,3 +115,8 @@ export interface FiatPlugin {
}

export type FiatPluginFactory = (params: FiatPluginFactoryArgs) => Promise<FiatPlugin>

export interface StatefulSceneEvent<EventValue, State extends object> {
value: EventValue
stateManager: StateManager<State>
}
31 changes: 31 additions & 0 deletions src/plugins/gui/hooks/useStateManager.ts
@@ -0,0 +1,31 @@
import { useRef } from 'react'

import { useHandler } from '../../../hooks/useHandler'
import { useState } from '../../../types/reactHooks'

export class StateManager<State extends object> {
private _state: State
private readonly onUpdate: (state: State) => void

constructor(state: State, onUpdate: (state: State) => void) {
this._state = state
this.onUpdate = onUpdate
}

get state(): State {
return this._state
}

update(state: Partial<State>): void {
this._state = { ...this._state, ...state }
this.onUpdate(this._state)
}
}

export const useStateManager = <T extends object>(defaultState: T): StateManager<T> => {
const [state, setState] = useState<T>(defaultState)
const handleUpdate: StateManager<T>['update'] = useHandler((state: Partial<T>) => setState({ ...stateManagerRef.current.state, ...state }))
const stateManagerRef = useRef<StateManager<T>>(new StateManager(state, handleUpdate))

return stateManagerRef.current
}
4 changes: 2 additions & 2 deletions src/plugins/gui/providers/bityProvider.ts
Expand Up @@ -441,7 +441,7 @@ export const bityProvider: FiatProviderFactory = {
await completeSellOrder(approveQuoteRes, coreWallet, showUi)
}

showUi.popScene()
showUi.exitScene()
}
})
},
Expand Down Expand Up @@ -522,7 +522,7 @@ const completeBuyOrder = async (approveQuoteRes: BityApproveQuoteResponse, showU
}
},
onDone: async () => {
showUi.popScene()
showUi.exitScene()
}
})
}
Expand Down

0 comments on commit adbae34

Please sign in to comment.