Skip to content

Commit da424e3

Browse files
committed
Add new ramp plugin scenes
1 parent 74a5087 commit da424e3

17 files changed

+1167
-25
lines changed

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
## Build/Test/Lint Commands
2323

24-
- `yarn lint` - Run ESLint on entire codebase
24+
- `yarn lint` - Run ESLint on entire codebase (only use when working on warning cleanup)
25+
- `yarn lint --quiet` - Run ESLint on entire codebase and only get error (Prefer this usage always)
2526
- `yarn fix` - Auto-fix linting issues and deduplicate yarn
2627
- `yarn test` - Run Jest tests (single run)
2728
- `yarn watch` - Run Jest tests in watch mode

design.png

541 KB
Loading

simulator_screenshot.png

527 KB
Loading

src/actions/ExchangeRateActions.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,117 @@ export const closestRateForTimestamp = (
359359
}
360360
return bestRate
361361
}
362+
363+
/**
364+
* Fetches exchange rates for a specific fiat currency on demand.
365+
* This is used when the user wants to view rates in a currency other than their default.
366+
*/
367+
export function fetchExchangeRatesForFiat(
368+
fiatCurrencyCode: string
369+
): ThunkAction<Promise<void>> {
370+
return async (dispatch, getState) => {
371+
const state = getState()
372+
const { account } = state.core
373+
const { currencyWallets } = account
374+
const now = Date.now()
375+
const yesterday = getYesterdayDateRoundDownHour(now).toISOString()
376+
377+
// Build asset pairs for the requested fiat currency
378+
const assetPairs: AssetPair[] = []
379+
const pairExpiration = now + ONE_MONTH
380+
const rateExpiration = now + ONE_DAY
381+
382+
// If the fiat isn't dollars, get its price relative to USD
383+
if (fiatCurrencyCode !== 'iso:USD') {
384+
assetPairs.push({
385+
currency_pair: `iso:USD_${fiatCurrencyCode}`,
386+
date: undefined,
387+
expiration: pairExpiration
388+
})
389+
}
390+
391+
// Get rates for all wallet assets
392+
for (const walletId of Object.keys(currencyWallets)) {
393+
const wallet = currencyWallets[walletId]
394+
const { currencyCode } = wallet.currencyInfo
395+
396+
// Get the primary asset's prices for today
397+
assetPairs.push({
398+
currency_pair: `${currencyCode}_${fiatCurrencyCode}`,
399+
date: undefined,
400+
expiration: pairExpiration
401+
})
402+
403+
// Do the same for any tokens
404+
for (const tokenId of wallet.enabledTokenIds) {
405+
const token = wallet.currencyConfig.allTokens[tokenId]
406+
if (token == null) continue
407+
if (token.currencyCode === currencyCode) continue
408+
assetPairs.push({
409+
currency_pair: `${token.currencyCode}_${fiatCurrencyCode}`,
410+
date: undefined,
411+
expiration: pairExpiration
412+
})
413+
}
414+
}
415+
416+
// Fetch rates from server in batches
417+
const newRates: ExchangeRateCache = {}
418+
for (let i = 0; i < assetPairs.length; i += RATES_SERVER_MAX_QUERY_SIZE) {
419+
const query = assetPairs.slice(i, i + RATES_SERVER_MAX_QUERY_SIZE)
420+
421+
const options = {
422+
method: 'POST',
423+
headers: { 'Content-Type': 'application/json' },
424+
body: JSON.stringify({ data: query })
425+
}
426+
427+
try {
428+
const response = await fetchRates('v2/exchangeRates', options)
429+
if (response.ok) {
430+
const json = await response.json()
431+
const cleanedRates = asRatesResponse(json)
432+
for (const rate of cleanedRates.data) {
433+
const { currency_pair: currencyPair, exchangeRate, date } = rate
434+
const isHistorical = now - new Date(date).valueOf() > HOUR_MS
435+
const key = isHistorical ? `${currencyPair}_${date}` : currencyPair
436+
437+
if (exchangeRate != null) {
438+
newRates[key] = {
439+
expiration: rateExpiration,
440+
rate: parseFloat(exchangeRate)
441+
}
442+
}
443+
}
444+
}
445+
} catch (error: unknown) {
446+
console.log(
447+
`fetchExchangeRatesForFiat error querying rates server ${String(
448+
error
449+
)}`
450+
)
451+
}
452+
}
453+
454+
// Merge with existing rates
455+
const mergedRates = { ...exchangeRateCache?.rates, ...newRates }
456+
const { exchangeRates, exchangeRatesMap } = buildGuiRates(
457+
mergedRates,
458+
yesterday
459+
)
460+
461+
// Update Redux state
462+
dispatch({
463+
type: 'EXCHANGE_RATES/UPDATE_EXCHANGE_RATES',
464+
data: {
465+
exchangeRates,
466+
exchangeRatesMap
467+
}
468+
})
469+
470+
// Update the in-memory cache if it exists
471+
if (exchangeRateCache != null) {
472+
exchangeRateCache.rates = mergedRates
473+
}
474+
}
475+
}

src/components/Main.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,7 @@ import { FioSentRequestDetailsScene as FioSentRequestDetailsSceneComponent } fro
9696
import { FioStakingChangeScene as FioStakingChangeSceneComponent } from './scenes/Fio/FioStakingChangeScene'
9797
import { FioStakingOverviewScene as FioStakingOverviewSceneComponent } from './scenes/Fio/FioStakingOverviewScene'
9898
import { GettingStartedScene } from './scenes/GettingStartedScene'
99-
import {
100-
BuyScene as BuySceneComponent,
101-
SellScene as SellSceneComponent
102-
} from './scenes/GuiPluginListScene'
99+
import { SellScene as SellSceneComponent } from './scenes/GuiPluginListScene'
103100
import { GuiPluginViewScene as GuiPluginViewSceneComponent } from './scenes/GuiPluginViewScene'
104101
import { HomeScene as HomeSceneComponent } from './scenes/HomeScene'
105102
import { LoanCloseScene as LoanCloseSceneComponent } from './scenes/Loans/LoanCloseScene'
@@ -138,6 +135,8 @@ import { SweepPrivateKeyCalculateFeeScene as SweepPrivateKeyCalculateFeeSceneCom
138135
import { SweepPrivateKeyCompletionScene as SweepPrivateKeyCompletionSceneComponent } from './scenes/SweepPrivateKeyCompletionScene'
139136
import { SweepPrivateKeyProcessingScene as SweepPrivateKeyProcessingSceneComponent } from './scenes/SweepPrivateKeyProcessingScene'
140137
import { SweepPrivateKeySelectCryptoScene as SweepPrivateKeySelectCryptoSceneComponent } from './scenes/SweepPrivateKeySelectCryptoScene'
138+
import { TradeCreateScene as TradeCreateSceneComponent } from './scenes/TradeCreateScene'
139+
import { TradeOptionSelectScene as TradeOptionSelectSceneComponent } from './scenes/TradeOptionSelectScene'
141140
import { TransactionDetailsScene as TransactionDetailsSceneComponent } from './scenes/TransactionDetailsScene'
142141
import {
143142
TransactionList as TransactionListComponent,
@@ -161,7 +160,6 @@ import { MenuTabs } from './themed/MenuTabs'
161160
import { SideMenu } from './themed/SideMenu'
162161

163162
const AssetSettingsScene = ifLoggedIn(AssetSettingsSceneComponent)
164-
const BuyScene = ifLoggedIn(BuySceneComponent)
165163
const ChangeMiningFeeScene = ifLoggedIn(ChangeMiningFeeSceneComponent)
166164
const ChangePasswordScene = ifLoggedIn(ChangePasswordSceneComponent)
167165
const ChangePinScene = ifLoggedIn(ChangePinSceneComponent)
@@ -284,6 +282,8 @@ const SweepPrivateKeySelectCryptoScene = ifLoggedIn(
284282
const TransactionDetailsScene = ifLoggedIn(TransactionDetailsSceneComponent)
285283
const TransactionList = ifLoggedIn(TransactionListComponent)
286284
const TransactionsExportScene = ifLoggedIn(TransactionsExportSceneComponent)
285+
const TradeCreateScene = ifLoggedIn(TradeCreateSceneComponent)
286+
const TradeOptionSelectScene = ifLoggedIn(TradeOptionSelectSceneComponent)
287287
const UpgradeUsernameScene = ifLoggedIn(UpgradeUsernameSceneComponent)
288288
const WalletDetails = ifLoggedIn(WalletDetailsComponent)
289289
const WalletListScene = ifLoggedIn(WalletListSceneComponent)
@@ -374,9 +374,13 @@ const EdgeBuyTabScreen = () => {
374374
>
375375
<BuyStack.Screen
376376
name="pluginListBuy"
377-
component={BuyScene}
377+
component={TradeCreateScene}
378378
options={firstSceneScreenOptions}
379379
/>
380+
<BuyStack.Screen
381+
name="rampSelectOption"
382+
component={TradeOptionSelectScene}
383+
/>
380384
<BuyStack.Screen
381385
name="pluginViewBuy"
382386
component={GuiPluginViewScene}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as React from 'react'
2+
import Ionicon from 'react-native-vector-icons/Ionicons'
3+
4+
import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity'
5+
import { styled } from '../hoc/styled'
6+
import { useTheme } from '../services/ThemeContext'
7+
8+
export interface DropDownInputButtonProps {
9+
children: React.ReactNode
10+
onPress: () => void | Promise<void>
11+
testID?: string
12+
}
13+
14+
export const DropDownInputButton = (
15+
props: DropDownInputButtonProps
16+
): React.ReactElement => {
17+
const { children, onPress, testID } = props
18+
const theme = useTheme()
19+
20+
return (
21+
<Container onPress={onPress} testID={testID}>
22+
{children}
23+
<Ionicon
24+
name="chevron-down"
25+
size={theme.rem(1)}
26+
color={theme.iconTappable}
27+
/>
28+
</Container>
29+
)
30+
}
31+
32+
const Container = styled(EdgeTouchableOpacity)(theme => ({
33+
backgroundColor: theme.textInputBackgroundColor,
34+
borderRadius: theme.rem(0.5),
35+
padding: theme.rem(1),
36+
flexDirection: 'row',
37+
alignItems: 'center',
38+
gap: theme.rem(0.25),
39+
minWidth: theme.rem(4),
40+
height: theme.rem(3.25)
41+
}))

src/components/buttons/PillButton.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { EdgeText } from '../themed/EdgeText'
88

99
export interface PillButtonProps {
1010
label: string
11-
onPress: () => void
12-
icon?: () => React.ReactElement
11+
onPress: () => void | Promise<void>
12+
icon?: () => React.ReactElement | null
1313
}
1414

1515
export const PillButton = (props: PillButtonProps): React.ReactElement => {
@@ -36,7 +36,8 @@ const Gradient = styled(LinearGradient)(theme => ({
3636
borderRadius: theme.rem(100),
3737
flexDirection: 'row',
3838
paddingHorizontal: theme.rem(0.75),
39-
paddingVertical: theme.rem(0.25)
39+
paddingVertical: theme.rem(0.25),
40+
gap: theme.rem(0.5)
4041
}))
4142

4243
const Label = styled(EdgeText)(theme => ({

src/components/cards/PaymentOptionCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface Props {
2626

2727
// Events:
2828
onPress: () => Promise<void> | void
29-
onLongPress: () => Promise<void> | void
29+
onLongPress?: () => Promise<void> | void
3030
onProviderPress: () => Promise<void> | void
3131
}
3232

src/components/modals/WalletListModal.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,36 @@ import { ButtonsModal } from './ButtonsModal'
3535
import { EdgeModal } from './EdgeModal'
3636

3737
export const ErrorNoMatchingWallets = 'ErrorNoMatchingWallets'
38+
39+
// User cancelled.
40+
// This is consistent with other modals that return `T | undefined`:
3841
export type WalletListResult =
39-
| {
40-
type: 'wallet'
41-
walletId: string
42-
tokenId: EdgeTokenId
43-
}
44-
| { type: 'wyre'; fiatAccountId: string }
45-
| { type: 'bankSignupRequest' }
46-
| { type: 'custom'; customAsset?: CustomAsset }
47-
// User cancelled.
48-
// This is consistent with other modals that return `T | undefined`:
42+
| WalletListWalletResult
43+
| WalletListWyreResult
44+
| WalletListBankSignupRequestResult
45+
| WalletListCustomResult
4946
| undefined
5047

48+
export interface WalletListWalletResult {
49+
type: 'wallet'
50+
walletId: string
51+
tokenId: EdgeTokenId
52+
}
53+
54+
export interface WalletListWyreResult {
55+
type: 'wyre'
56+
fiatAccountId: string
57+
}
58+
59+
export interface WalletListBankSignupRequestResult {
60+
type: 'bankSignupRequest'
61+
}
62+
63+
export interface WalletListCustomResult {
64+
type: 'custom'
65+
customAsset?: CustomAsset
66+
}
67+
5168
interface Props {
5269
bridge: AirshipBridge<WalletListResult>
5370
navigation: NavigationBase

src/components/scenes/GuiPluginListScene.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,12 @@ const pluginPartnerLogos: Record<string, 'guiPluginLogoMoonpay'> = {
133133
moonpay: 'guiPluginLogoMoonpay'
134134
}
135135

136-
type BuyProps = BuyTabSceneProps<'pluginListBuy'>
136+
type BuyProps = BuyTabSceneProps<'pluginListBuyOld'>
137137
type SellProps = SellTabSceneProps<'pluginListSell'>
138138
type OwnProps = BuyProps | SellProps
139139

140140
function isBuyProps(props: OwnProps): props is BuyProps {
141-
return props.route.name === 'pluginListBuy'
141+
return props.route.name === 'pluginListBuyOld'
142142
}
143143

144144
interface StateProps {

0 commit comments

Comments
 (0)