diff --git a/CHANGELOG.md b/CHANGELOG.md index 6edb9e7ee06..da6733c7f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # edge-react-gui +## 3.1.0 (2023-01-13) + +- Add Bech32 address support +- Add support for non-segwit DGB and LTC +- Thorchain Savers: Show estimate fees and estimate break-even time +- Show current APR on Earn button +- Split swap providers into centralized and decentralized (DEX) groups +- Allow user to prefer provider type +- Show error when attempting to unstake small (lower than tx fee) amounts +- Show staked amounts to the wallet transaction history scene +- Add shortcut to Exchange Settings by tapping on Powered By logo +- Deprecate Wyre +- Upgrade edge-core-js to v0.19.37 + - added: Always-enabled tokens. The currency engine checks these for balances and transactions, but they do not appear in the per-wallet enabled token lists. + - EdgeCurrencyConfig.alwaysEnabledTokenIds + - EdgeCurrencyConfig.changeAlwaysEnabledTokenIds + - added: EdgeCurrencyTools.checkPublicKey, which provides a mechanism for currency plugins to refresh their cached public keys if necessary. + - added: EdgeSwapInfo.isDex and EdgeSwapRequestOptions.preferType, to always prefer DEX swaps over centralized swaps. + - changed: Always select the "transfer" plugin if it returns a quote, regardless of price. + - added: Accelerate Transaction API +- Upgrade edge-currency-plugins to v1.3.5 + - Fixed: Incorrect types path in package.json + - Added: Support bech32 addresses as segwitAddress for EdgeFreshAddress + - Added: RBF flags for Bitcoin and Litecoin +- Upgrade edge-exchange-plugins to v0.16.17 + - Add: isDex and swapPlugType to plugins and quotes + ## 3.0.0 (2023-01-06) - Updated dark mode theme: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 975d5261301..65b7f7950b0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -18,7 +18,7 @@ PODS: - disklet (0.5.2): - React - DoubleConversion (1.1.6) - - edge-core-js (0.19.35): + - edge-core-js (0.19.37): - React-Core - edge-login-ui-rn (0.10.17): - React @@ -1054,7 +1054,7 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 disklet: e7ed3e673ccad9d175a1675f9f3589ffbf69a5fd DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662 - edge-core-js: 09f9923bd19525fad89189786a1552f4e1cd4ed6 + edge-core-js: 64ce764c9466db018b22ce103998081bcb75537a edge-login-ui-rn: b61a9e79902bf149e274f7d27bf8c359e6d89072 FBLazyVector: d2db9d00883282819d03bbd401b2ad4360d47580 FBReactNativeSpec: 94da4d84ba3b1acf459103320882daa481a2b62d diff --git a/package.json b/package.json index bd838bb7020..de3739e5e6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edge-react-gui", - "version": "3.0.0", + "version": "3.1.0", "private": true, "description": "Edge Wallet React GUI", "homepage": "https://edge.app", @@ -106,11 +106,11 @@ "dateformat": "^3.0.3", "detect-bundler": "^1.1.0", "disklet": "^0.5.2", - "edge-core-js": "^0.19.35", + "edge-core-js": "^0.19.37", "edge-currency-accountbased": "^0.21.0", "edge-currency-monero": "^0.5.4", - "edge-currency-plugins": "^1.3.3", - "edge-exchange-plugins": "^0.16.16", + "edge-currency-plugins": "^1.3.5", + "edge-exchange-plugins": "^0.16.17", "edge-login-ui-rn": "^0.10.17", "edge-plugin-bity": "https://github.com/EdgeApp/edge-plugin-bity.git#2a52e6cb86512b98f69f8f5dd3105cdf6d0c2d8a", "edge-plugin-simplex": "https://github.com/EdgeApp/edge-plugin-simplex.git#b11def82d84f2735b91f4b8104a9e0d3e8c3600d", diff --git a/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap b/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap index d55814c06e7..789138d5d5a 100644 --- a/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap +++ b/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap @@ -181,25 +181,6 @@ Settlement: 10 - 30 minutes", "pluginId": "creditcard", "title": "Credit and Debit Card", }, - Object { - "cryptoCodes": Array [ - "BTC", - "ETH", - "DAI", - "USDC", - ], - "deepPath": "", - "deepQuery": Object {}, - "description": "Fee: 1-2% -Settlement: 1 - 5 days", - "partnerIconPath": "wyre-logo-square-small.png", - "paymentTypeLogoKey": "bank", - "paymentTypes": Array [ - "bank", - ], - "pluginId": "wyre", - "title": "ACH (deprecated)", - }, Object { "cryptoCodes": Array [ "BTC", @@ -254,25 +235,6 @@ Array [ "pluginId": "moonpay", "title": "ACH Bank Transfer", }, - Object { - "cryptoCodes": Array [ - "BTC", - "ETH", - "DAI", - "USDC", - ], - "deepPath": "", - "deepQuery": Object {}, - "description": "Fee: 1% / Settlement: 1 - 5 days -Monthly Limit: $150000", - "partnerIconPath": "wyre-logo-square-small.png", - "paymentTypeLogoKey": "bank", - "paymentTypes": Array [ - "bank", - ], - "pluginId": "wyre", - "title": "ACH (deprecated)", - }, Object { "cryptoCodes": Array [ "BTC", diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap index 9ff5329e9bd..a392da9164b 100644 --- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap +++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap @@ -140,10 +140,6 @@ Object { "fioAddressesLoading": false, "fioDomains": Array [], }, - "requestType": Object { - "uniqueLegacyAddress": false, - "useLegacyAddress": false, - }, "sendConfirmation": Object { "address": "", "authRequired": "none", @@ -228,6 +224,7 @@ Object { }, "pinLoginEnabled": false, "preferredSwapPluginId": "", + "preferredSwapPluginType": undefined, "spamFilterOn": true, "spendingLimits": Object { "transaction": Object { diff --git a/src/__tests__/reducers/requestTypeReducer.test.ts b/src/__tests__/reducers/requestTypeReducer.test.ts deleted file mode 100644 index 2b7a05eea5c..00000000000 --- a/src/__tests__/reducers/requestTypeReducer.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect, test } from '@jest/globals' - -import { requestType as requestTypeReducer } from '../../reducers/RequestTypeReducer' - -test('initialState', () => { - const expected = { - useLegacyAddress: false, - uniqueLegacyAddress: false - } - const actual = requestTypeReducer(undefined, { type: 'DUMMY_ACTION_PLEASE_IGNORE' }) - - expect(actual).toEqual(expected) -}) diff --git a/src/__tests__/reducers/scenesReducer.test.ts b/src/__tests__/reducers/scenesReducer.test.ts index f59dd34f7a9..faaba3434ea 100644 --- a/src/__tests__/reducers/scenesReducer.test.ts +++ b/src/__tests__/reducers/scenesReducer.test.ts @@ -26,10 +26,6 @@ test('initialState', () => { fioAddressesLoading: false, fioDomains: [] }, - requestType: { - useLegacyAddress: false, - uniqueLegacyAddress: false - }, sendConfirmation: SendConfirmationInitialState, transactionDetails: { subcategories: [] diff --git a/src/__tests__/scenes/CryptoExchangeQuoteScene.test.tsx b/src/__tests__/scenes/CryptoExchangeQuoteScene.test.tsx index 03de28b70ed..bf0cc85fbde 100644 --- a/src/__tests__/scenes/CryptoExchangeQuoteScene.test.tsx +++ b/src/__tests__/scenes/CryptoExchangeQuoteScene.test.tsx @@ -1,10 +1,18 @@ import { describe, expect, it } from '@jest/globals' +import { EdgeSwapInfo } from 'edge-core-js' import * as React from 'react' import { createRenderer } from 'react-test-renderer/shallow' import { CryptoExchangeQuoteScreenComponent } from '../../components/scenes/CryptoExchangeQuoteScene' import { getTheme } from '../../components/services/ThemeContext' import { GuiSwapInfo } from '../../types/types' +import { fakeNavigation } from '../../util/fake/fakeNavigation' + +const dummySwapInfo: EdgeSwapInfo = { + pluginId: '', + displayName: '', + supportEmail: '' +} describe('CryptoExchangeQuoteScreenComponent', () => { it('should render with loading props', () => { @@ -36,6 +44,7 @@ describe('CryptoExchangeQuoteScreenComponent', () => { const swapInfo: GuiSwapInfo = { quote: { + swapInfo: dummySwapInfo, request: fakeRequest, isEstimate: true, fromNativeAmount: '10000', @@ -80,6 +89,7 @@ describe('CryptoExchangeQuoteScreenComponent', () => { const actual = renderer.render( undefined } diff --git a/src/__tests__/scenes/Request.test.tsx b/src/__tests__/scenes/RequestScene.test.tsx similarity index 89% rename from src/__tests__/scenes/Request.test.tsx rename to src/__tests__/scenes/RequestScene.test.tsx index 220088fefc1..508189d9fdd 100644 --- a/src/__tests__/scenes/Request.test.tsx +++ b/src/__tests__/scenes/RequestScene.test.tsx @@ -2,7 +2,7 @@ import { describe, expect, it } from '@jest/globals' import * as React from 'react' import { createRenderer } from 'react-test-renderer/shallow' -import { RequestComponent } from '../../components/scenes/RequestScene' +import { RequestSceneComponent } from '../../components/scenes/RequestScene' import { getTheme } from '../../components/services/ThemeContext' import { fakeNavigation } from '../../util/fake/fakeNavigation' @@ -11,7 +11,7 @@ describe('Request', () => { const renderer = createRenderer() const actual = renderer.render( - { exchangeSecondaryToPrimaryRatio={null as any} primaryCurrencyInfo={null as any} secondaryCurrencyInfo={null as any} - useLegacyAddress={null as any} theme={getTheme()} refreshAllFioAddresses={() => undefined} onSelectWallet={(walletId, currencyCode) => undefined} @@ -38,7 +37,7 @@ describe('Request', () => { } const actual = renderer.render( - { exchangeSecondaryToPrimaryRatio={{} as any} primaryCurrencyInfo={{ displayDenomination: { multiplier: '100000000' }, exchangeDenomination: { multiplier: '100000000' } } as any} secondaryCurrencyInfo={{} as any} - useLegacyAddress={false} theme={getTheme()} refreshAllFioAddresses={() => undefined} onSelectWallet={(walletId, currencyCode) => undefined} diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap index e1c5861aef6..833bbd18569 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap @@ -46,7 +46,8 @@ exports[`CryptoExchangeQuoteScreenComponent should render with loading props 1`] fiatCurrencyCode="USD" walletName="" /> - ChangeNow - + + - - Your Receiving Wallet Address + Your Wallet Address - + Loading… diff --git a/src/actions/CryptoExchangeActions.tsx b/src/actions/CryptoExchangeActions.tsx index 86bbc952f3b..2a03fc17503 100644 --- a/src/actions/CryptoExchangeActions.tsx +++ b/src/actions/CryptoExchangeActions.tsx @@ -168,6 +168,7 @@ export function exchangeMax(): ThunkAction> { async function fetchSwapQuote(state: RootState, request: EdgeSwapRequest): Promise { const { account } = state.core + const { preferredSwapPluginType } = state.ui.settings // Find preferred swap provider: const activePlugins = bestOfPlugins(state.account.referralCache.accountPlugins, state.account.accountReferral, state.ui.settings.preferredSwapPluginId) @@ -181,6 +182,7 @@ async function fetchSwapQuote(state: RootState, request: EdgeSwapRequest): Promi // Get the quote: const quote: EdgeSwapQuote = await account.fetchSwapQuote(request, { preferPluginId, + preferType: preferredSwapPluginType, disabled: activePlugins.disabled, promoCodes: activePlugins.promoCodes }) diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 90bf88b902d..0e6d44e2256 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -145,6 +145,7 @@ export function initializeAccount(account: EdgeAccount, touchIdInfo: GuiTouchIdI passwordReminder: passwordReminderInitialState, pinLoginEnabled: false, preferredSwapPluginId: undefined, + preferredSwapPluginType: undefined, spendingLimits: { transaction: { isEnabled: false, amount: 0 } }, touchIdInfo, walletId: '', diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index f2310c1d673..3e6ce010405 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -1,4 +1,4 @@ -import { EdgeAccount, EdgeDenomination } from 'edge-core-js' +import { EdgeAccount, EdgeDenomination, EdgeSwapPluginType } from 'edge-core-js' import { disableTouchId, enableTouchId } from 'edge-login-ui-rn' import * as React from 'react' @@ -11,6 +11,7 @@ import { setDenominationKeyRequest as setDenominationKeyRequestAccountSettings, setDeveloperModeOn as setDeveloperModeOnAccountSettings, setPreferredSwapPluginId as setPreferredSwapPluginIdAccountSettings, + setPreferredSwapPluginType as setPreferredSwapPluginTypeAccountSettings, setSpamFilterOn as setSpamFilterOnAccountSettings, setSpendingLimits as setSpendingLimitsAccountSettings } from '../modules/Core/Account/settings' @@ -118,6 +119,21 @@ export function setPreferredSwapPluginId(pluginId: string | undefined): ThunkAct } } +export function setPreferredSwapPluginType(swapPluginType: EdgeSwapPluginType | undefined): ThunkAction { + return (dispatch, getState) => { + const state = getState() + const { account } = state.core + setPreferredSwapPluginTypeAccountSettings(account, swapPluginType) + .then(() => + dispatch({ + type: 'UI/SETTINGS/SET_PREFERRED_SWAP_PLUGIN_TYPE', + data: swapPluginType + }) + ) + .catch(showError) + } +} + // Denominations export function setDenominationKeyRequest(pluginId: string, currencyCode: string, denomination: EdgeDenomination): ThunkAction> { return async (dispatch, getState) => { diff --git a/src/actions/WalletActions.tsx b/src/actions/WalletActions.tsx index 8c26d7bc745..043992739de 100644 --- a/src/actions/WalletActions.tsx +++ b/src/actions/WalletActions.tsx @@ -41,9 +41,6 @@ export function selectWallet(walletId: string, currencyCode: string, alwaysActiv type: 'UI/WALLETS/SELECT_WALLET', data: { walletId, currencyCode } }) - const wallet: EdgeCurrencyWallet = currencyWallets[walletId] - const receiveAddress = await wallet.getReceiveAddress({ currencyCode }) - dispatch({ type: 'NEW_RECEIVE_ADDRESS', data: { receiveAddress } }) } } } diff --git a/src/components/Main.ui.tsx b/src/components/Main.ui.tsx index 7ffafd392e4..25b1b2d5b5f 100644 --- a/src/components/Main.ui.tsx +++ b/src/components/Main.ui.tsx @@ -80,7 +80,7 @@ import { NotificationScene } from './scenes/NotificationScene' import { OtpRepairScene } from './scenes/OtpRepairScene' import { OtpSettingsScene } from './scenes/OtpSettingsScene' import { ChangeRecoveryScene } from './scenes/PasswordRecoveryScene' -import { Request } from './scenes/RequestScene' +import { RequestScene } from './scenes/RequestScene' import { SecurityAlertsScene } from './scenes/SecurityAlertsScene' import { SendScene } from './scenes/SendScene' import { SendScene2 } from './scenes/SendScene2' @@ -484,7 +484,7 @@ export class MainComponent extends React.Component { } diff --git a/src/components/modals/WalletListModal.tsx b/src/components/modals/WalletListModal.tsx index f4c2094de7e..44fa1156416 100644 --- a/src/components/modals/WalletListModal.tsx +++ b/src/components/modals/WalletListModal.tsx @@ -5,7 +5,7 @@ import { FlatList } from 'react-native-gesture-handler' import { sprintf } from 'sprintf-js' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' -import { makeWyreClient, PaymentMethodsMap } from '../../controllers/action-queue/WyreClient' +import { PaymentMethodsMap } from '../../controllers/action-queue/WyreClient' import { useAsyncValue } from '../../hooks/useAsyncValue' import { useHandler } from '../../hooks/useHandler' import { useRowLayout } from '../../hooks/useRowLayout' @@ -33,7 +33,7 @@ export interface WalletListResult { // Wyre buy/sell isBankSignupRequest?: boolean - wyreAccountId?: string + fiatAccountId?: string } interface Props { @@ -99,10 +99,9 @@ export function WalletListModal(props: Props) { const [searching, setSearching] = React.useState(false) const [searchText, setSearchText] = React.useState('') - const [bankAccountsMap] = useAsyncValue(async (): Promise => { - const wyreClient = await makeWyreClient({ account }) - if (!wyreClient.isAccountSetup) return {} - return await wyreClient.getPaymentMethods() + const [bankAccountsMap] = useAsyncValue(async (): Promise => { + // TODO: Re-enable once new fiat ramp partner is re-integrated + return null }, [account]) // #endregion State @@ -133,8 +132,8 @@ export function WalletListModal(props: Props) { const handleCancel = useHandler(() => { bridge.resolve({}) }) - const handlePaymentMethodPress = useHandler((paymentMethodId: string) => () => { - bridge.resolve({ wyreAccountId: paymentMethodId }) + const handlePaymentMethodPress = useHandler((fiatAccountId: string) => () => { + bridge.resolve({ fiatAccountId }) }) const handleWalletListPress = useHandler((walletId: string, currencyCode: string) => { if (walletId === '') { @@ -183,8 +182,9 @@ export function WalletListModal(props: Props) { }) const renderBankSection = () => { + if (bankAccountsMap == null) return null if (!showBankOptions) return null - if (bankAccountsMap == null || Object.keys(bankAccountsMap).length === 0) return renderBankSignupButton() + if (Object.keys(bankAccountsMap).length === 0) return renderBankSignupButton() return ( <> diff --git a/src/components/scenes/CryptoExchangeQuoteScene.tsx b/src/components/scenes/CryptoExchangeQuoteScene.tsx index 15447746b12..eb5b5e49746 100644 --- a/src/components/scenes/CryptoExchangeQuoteScene.tsx +++ b/src/components/scenes/CryptoExchangeQuoteScene.tsx @@ -1,14 +1,15 @@ import { div, gte } from 'biggystring' import { EdgeAccount } from 'edge-core-js/types' import * as React from 'react' -import { ScrollView, View } from 'react-native' +import { ScrollView, TouchableOpacity, View } from 'react-native' import FastImage from 'react-native-fast-image' +import IonIcon from 'react-native-vector-icons/Ionicons' import { exchangeTimerExpired, shiftCryptoCurrency } from '../../actions/CryptoExchangeActions' import s from '../../locales/strings' import { Slider } from '../../modules/UI/components/Slider/Slider' import { connect } from '../../types/reactRedux' -import { RouteProp } from '../../types/routerTypes' +import { NavigationProp, RouteProp } from '../../types/routerTypes' import { GuiSwapInfo } from '../../types/types' import { getSwapPluginIconUri } from '../../util/CdnUris' import { getWalletName } from '../../util/CurrencyWalletHelpers' @@ -26,6 +27,7 @@ import { LineTextDivider } from '../themed/LineTextDivider' import { SceneHeader } from '../themed/SceneHeader' interface OwnProps { + navigation: NavigationProp<'exchangeQuote'> route: RouteProp<'exchangeQuote'> } interface StateProps { @@ -95,6 +97,10 @@ export class CryptoExchangeQuoteScreenComponent extends React.Component { + this.props.navigation.navigate('exchangeSettings', {}) + } + render() { const { account, fromDenomination, fromWalletCurrencyName, toDenomination, toWalletCurrencyName, pending, theme, route } = this.props const { swapInfo } = route.params @@ -135,11 +141,12 @@ export class CryptoExchangeQuoteScreenComponent extends React.Component - + {s.strings.plugin_powered_by_space + ' '} {' ' + exchangeName} - + + {quote.isEstimate && ( { const [bankAccountsMap, setBankAccountsMap] = React.useState<{ [paymentMethodId: string]: PaymentMethod } | undefined>(undefined) useAsyncEffect(async () => { - const wyreClient = await makeWyreClient({ account }) - if (wyreClient.isAccountSetup) { - setBankAccountsMap(await wyreClient.getPaymentMethods()) - } + // TODO: Re-enable when new fiat ramp partner is avialable: + setBankAccountsMap(undefined) }, [account]) const paymentMethod = destBankId == null || bankAccountsMap == null || Object.keys(bankAccountsMap).length === 0 ? undefined : bankAccountsMap[destBankId] @@ -260,7 +258,7 @@ export const LoanCreateScene = (props: Props) => { filterActivation /> )) - .then(async ({ walletId, currencyCode, isBankSignupRequest, wyreAccountId }) => { + .then(async ({ walletId, currencyCode, isBankSignupRequest, fiatAccountId }) => { if (isBankSignupRequest) { // Open bank plugin for new user signup navigation.navigate('pluginView', { @@ -268,10 +266,10 @@ export const LoanCreateScene = (props: Props) => { deepPath: '', deepQuery: {} }) - } else if (wyreAccountId != null) { + } else if (fiatAccountId != null) { // Set a hard-coded intermediate AAVE loan destination asset (USDC) to // use for the bank sell step that comes after the initial loan - setDestBankId(wyreAccountId) + setDestBankId(fiatAccountId) setDestWallet(borrowEngineWallet) setDestTokenId(hardDestTokenAddr) } else if (walletId != null && currencyCode != null) { diff --git a/src/components/scenes/Loans/LoanManageScene.tsx b/src/components/scenes/Loans/LoanManageScene.tsx index b471f53b91a..96822df43f1 100644 --- a/src/components/scenes/Loans/LoanManageScene.tsx +++ b/src/components/scenes/Loans/LoanManageScene.tsx @@ -12,7 +12,7 @@ import { makeActionProgram } from '../../../controllers/action-queue/ActionProgr import { dryrunActionProgram } from '../../../controllers/action-queue/runtime/dryrunActionProgram' import { ActionOp, ActionProgram } from '../../../controllers/action-queue/types' import { makeInitialProgramState } from '../../../controllers/action-queue/util/makeInitialProgramState' -import { makeWyreClient, PaymentMethodsMap } from '../../../controllers/action-queue/WyreClient' +import { PaymentMethodsMap } from '../../../controllers/action-queue/WyreClient' import { runLoanActionProgram } from '../../../controllers/loan-manager/redux/actions' import { LoanProgramType } from '../../../controllers/loan-manager/store' import { LoanAccount } from '../../../controllers/loan-manager/types' @@ -166,11 +166,8 @@ export const LoanManageSceneComponent = (props: Props) => { }) const [bankAccountsMap] = useAsyncValue(async (): Promise => { - if (account == null) return {} - const wyreClient = await makeWyreClient({ account }) - if (!wyreClient.isAccountSetup) return {} - return await wyreClient.getPaymentMethods() - }, [account]) + return {} + }, []) // New debt/collateral amount const amountChange = loanManageType === 'loan-manage-borrow' || loanManageType === 'loan-manage-deposit' ? 'increase' : 'decrease' @@ -351,7 +348,7 @@ export const LoanManageSceneComponent = (props: Props) => { filterActivation /> )) - .then(async ({ walletId, currencyCode, isBankSignupRequest, wyreAccountId }) => { + .then(async ({ walletId, currencyCode, isBankSignupRequest, fiatAccountId: wyreAccountId }) => { if (isBankSignupRequest) { // Open bank plugin for new user signup navigation.navigate('pluginView', { diff --git a/src/components/scenes/RequestScene.tsx b/src/components/scenes/RequestScene.tsx index 700694de12f..7423ccc5452 100644 --- a/src/components/scenes/RequestScene.tsx +++ b/src/components/scenes/RequestScene.tsx @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard' -import { gt, lt, lte } from 'biggystring' +import { lt, lte } from 'biggystring' import { EdgeCurrencyWallet, EdgeEncodeUri } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator, InputAccessoryView, Linking, Platform, Text, TouchableOpacity, View } from 'react-native' @@ -30,11 +30,12 @@ import { WalletListModal, WalletListResult } from '../modals/WalletListModal' import { Airship, showError, showToast } from '../services/AirshipInstance' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' import { FiatText } from '../text/FiatText' +import { AddressQr } from '../themed/AddressQr' +import { Carousel } from '../themed/Carousel' import { EdgeText } from '../themed/EdgeText' import { ExchangedFlipInput, ExchangedFlipInputAmounts } from '../themed/ExchangedFlipInput' import { FlipInput } from '../themed/FlipInput' import { MainButton } from '../themed/MainButton' -import { QrCode } from '../themed/QrCode' import { SceneHeader } from '../themed/SceneHeader' import { ShareButtons } from '../themed/ShareButtons' @@ -49,7 +50,6 @@ interface StateProps { isConnected: boolean primaryCurrencyInfo?: GuiCurrencyInfo secondaryCurrencyInfo?: GuiCurrencyInfo - useLegacyAddress?: boolean } interface DispatchProps { @@ -64,19 +64,22 @@ interface CurrencyMinimumPopupState { type Props = StateProps & DispatchProps & OwnProps & ThemeProps interface State { - publicAddress: string - legacyAddress: string - encodedURI: string | undefined + addresses: AddressInfo[] + selectedAddress?: AddressInfo + amounts?: ExchangedFlipInputAmounts minimumPopupModalState: CurrencyMinimumPopupState isFioMode: boolean errorMessage?: string } +interface AddressInfo { + addressString: string + label: string +} + const inputAccessoryViewID: string = 'cancelHeaderId' -export class RequestComponent extends React.Component { - // @ts-expect-error - amounts: ExchangedFlipInputAmounts +export class RequestSceneComponent extends React.Component { flipInput: React.ElementRef | null = null unsubscribeAddressChanged: (() => void) | undefined @@ -89,9 +92,7 @@ export class RequestComponent extends React.Component { } }) this.state = { - publicAddress: '', - legacyAddress: '', - encodedURI: undefined, + addresses: [], minimumPopupModalState, isFioMode: false } @@ -105,10 +106,10 @@ export class RequestComponent extends React.Component { } componentDidMount() { - this.generateEncodedUri() + this.getAddressItems() this.props.refreshAllFioAddresses() if (this.props.wallet != null) { - this.unsubscribeAddressChanged = this.props.wallet.on('addressChanged', async () => this.generateEncodedUri()) + this.unsubscribeAddressChanged = this.props.wallet.on('addressChanged', async () => this.getAddressItems()) } } @@ -130,73 +131,59 @@ export class RequestComponent extends React.Component { return !!diffElement || !!diffElement2 } - async generateEncodedUri() { - const { wallet, useLegacyAddress, currencyCode } = this.props - let legacyAddress = '' - let publicAddress = '' - if (wallet != null) { - const receiveAddress = await wallet.getReceiveAddress() - // @ts-expect-error - legacyAddress = receiveAddress.legacyAddress - publicAddress = receiveAddress.publicAddress + async getAddressItems() { + const { wallet, currencyCode } = this.props + if (currencyCode == null) return + if (wallet == null) return + + const receiveAddress = await wallet.getReceiveAddress() + const addresses: AddressInfo[] = [] + + // Handle segwitAddress + if (receiveAddress.segwitAddress != null) { + addresses.push({ + addressString: receiveAddress.segwitAddress, + label: s.strings.request_qr_your_segwit_address + }) } - this.setState({ - publicAddress, - legacyAddress + // Handle publicAddress + addresses.push({ + addressString: receiveAddress.publicAddress, + label: receiveAddress.segwitAddress != null ? s.strings.request_qr_your_wrapped_segwit_address : s.strings.request_qr_your_wallet_address }) - if (!currencyCode) return - const abcEncodeUri = { - publicAddress: useLegacyAddress && legacyAddress != null ? legacyAddress : publicAddress, - currencyCode - } - let encodedURI - try { - encodedURI = wallet ? await wallet.encodeUri(abcEncodeUri) : undefined - this.setState({ - encodedURI - }) - } catch (e: any) { - console.log(e) - publicAddress = s.strings.loading - legacyAddress = s.strings.loading - this.setState({ - publicAddress, - legacyAddress + // Handle legacyAddress + if (receiveAddress.legacyAddress != null) { + addresses.push({ + addressString: receiveAddress.legacyAddress, + label: s.strings.request_qr_your_legacy_address }) } + + this.setState({ addresses, selectedAddress: addresses[0] }) + } + + async getEncodedUri(): Promise { + const { wallet, currencyCode } = this.props + const { amounts, selectedAddress } = this.state + + if (wallet == null || currencyCode == null || selectedAddress == null) return + + return await wallet.encodeUri({ currencyCode, publicAddress: selectedAddress.addressString, nativeAmount: amounts?.nativeAmount }) } - async componentDidUpdate(prevProps: Props) { - const { currencyCode, wallet, useLegacyAddress } = this.props + async componentDidUpdate(prevProps: Props, prevState: State) { + const { currencyCode, wallet } = this.props if (wallet == null || currencyCode == null) return + const { pluginId } = wallet.currencyInfo - const receiveAddress = await wallet.getReceiveAddress() - const didAddressChange = this.state.publicAddress !== receiveAddress.publicAddress - const changeLegacyPublic = useLegacyAddress !== prevProps.useLegacyAddress + const didAddressChange = prevState.selectedAddress !== this.state.selectedAddress const didWalletChange = prevProps.wallet && wallet.id !== prevProps.wallet.id - if (didAddressChange || changeLegacyPublic || didWalletChange) { - let { publicAddress, legacyAddress } = receiveAddress - - const abcEncodeUri = useLegacyAddress ? { publicAddress, legacyAddress, currencyCode } : { publicAddress, currencyCode } - let encodedURI - try { - encodedURI = await wallet.encodeUri(abcEncodeUri) - } catch (err: any) { - console.log(err) - publicAddress = s.strings.loading - legacyAddress = s.strings.loading - } - - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - encodedURI, - publicAddress: publicAddress, - // @ts-expect-error - legacyAddress: legacyAddress - }) + if (didWalletChange) { + this.getAddressItems() } + // old blank address to new // include 'didAddressChange' because didWalletChange returns false upon initial request scene load if (didWalletChange || didAddressChange) { @@ -251,9 +238,10 @@ export class RequestComponent extends React.Component { } handleAddressBlockExplorer = () => { - const { wallet, useLegacyAddress } = this.props + const { wallet } = this.props const addressExplorer = wallet != null ? wallet.currencyInfo.addressExplorer : null - const requestAddress = useLegacyAddress ? this.state.legacyAddress : this.state.publicAddress + if (this.state.selectedAddress == null) return + const requestAddress = this.state.selectedAddress.addressString Airship.show<'confirm' | 'cancel' | undefined>(bridge => ( { /> )) .then((result?: string) => { - // @ts-expect-error - return result === 'confirm' ? Linking.openURL(sprintf(addressExplorer, requestAddress)) : null + return result === 'confirm' && addressExplorer != null ? Linking.openURL(sprintf(addressExplorer, requestAddress)) : null }) .catch(error => console.log(error)) } @@ -283,10 +270,6 @@ export class RequestComponent extends React.Component { ) } - handleQrCodePress = () => { - Airship.show(bridge => ) - } - onError = (errorMessage?: string) => this.setState({ errorMessage }) handleKeysOnlyModePress = () => showWebViewModal(config.supportSite, s.strings.help_support) @@ -303,15 +286,24 @@ export class RequestComponent extends React.Component { ) } + handleChangeAddressItem = (item: AddressInfo) => { + this.setState({ selectedAddress: item }) + } + + handlePressAddressItem = async (encodedUri?: string) => { + Airship.show(bridge => ) + } + render() { - const { exchangeSecondaryToPrimaryRatio, wallet, primaryCurrencyInfo, secondaryCurrencyInfo, theme } = this.props + const { currencyCode, exchangeSecondaryToPrimaryRatio, wallet, primaryCurrencyInfo, secondaryCurrencyInfo, theme } = this.props const styles = getStyles(theme) - if (primaryCurrencyInfo == null || secondaryCurrencyInfo == null || exchangeSecondaryToPrimaryRatio == null || wallet == null) { + if (currencyCode == null || primaryCurrencyInfo == null || secondaryCurrencyInfo == null || exchangeSecondaryToPrimaryRatio == null || wallet == null) { return } - const requestAddress = this.props.useLegacyAddress ? this.state.legacyAddress : this.state.publicAddress + const selectedAddress = this.state.selectedAddress + const requestAddress = selectedAddress?.addressString ?? s.strings.loading const flipInputHeaderText = sprintf(s.strings.send_to_wallet, getWalletName(wallet)) const { keysOnlyMode = false } = getSpecialCurrencyInfo(wallet.currencyInfo.pluginId) @@ -379,10 +371,23 @@ export class RequestComponent extends React.Component { ) : null} - + item.addressString} + onChangeItem={this.handleChangeAddressItem} + renderItem={item => ( + + )} + /> - {s.strings.request_qr_your_receiving_wallet_address} + {selectedAddress?.label ?? s.strings.request_qr_your_wallet_address} {requestAddress} @@ -395,30 +400,19 @@ export class RequestComponent extends React.Component { } onExchangeAmountChanged = async (amounts: ExchangedFlipInputAmounts) => { - const { publicAddress, legacyAddress } = this.state - const { currencyCode } = this.props - this.amounts = amounts - if (!currencyCode) return - const edgeEncodeUri: EdgeEncodeUri = - // @ts-expect-error - this.props.useLegacyAddress && legacyAddress ? { publicAddress, legacyAddress, currencyCode } : { publicAddress, currencyCode } - if (gt(amounts.nativeAmount, '0')) { - edgeEncodeUri.nativeAmount = amounts.nativeAmount - } - let encodedURI - try { - encodedURI = this.props.wallet ? await this.props.wallet.encodeUri(edgeEncodeUri) : undefined - } catch (e: any) { - console.log(e) - } - - this.setState({ encodedURI }) + this.setState({ amounts }) } - copyToClipboard = () => { - const requestAddress = this.props.useLegacyAddress ? this.state.legacyAddress : this.state.publicAddress - Clipboard.setString(requestAddress) - showToast(s.strings.fragment_request_address_copied) + copyToClipboard = async (uri?: string) => { + try { + const encodedUri = uri ?? (await this.getEncodedUri()) + if (encodedUri != null) { + Clipboard.setString(encodedUri) + showToast(s.strings.fragment_request_address_uri_copied) + } + } catch (error) { + showError(error) + } } shouldShowMinimumModal = (props: Props): boolean => { @@ -439,12 +433,13 @@ export class RequestComponent extends React.Component { } shareMessage = async () => { - const { currencyCode, wallet, useLegacyAddress } = this.props - const { legacyAddress, publicAddress } = this.state - if (!currencyCode || !wallet) { + const { currencyCode, wallet } = this.props + const { selectedAddress } = this.state + if (currencyCode == null || wallet == null || selectedAddress == null) { throw new Error('Wallet still loading. Please wait and try again.') } - let sharedAddress = this.state.encodedURI ?? '' + const publicAddress = selectedAddress.addressString + let sharedAddress = (await this.getEncodedUri()) ?? publicAddress let edgePayUri = 'https://deep.edge.app/' let addOnMessage = '' // if encoded (like XTZ), only share the public address @@ -453,11 +448,7 @@ export class RequestComponent extends React.Component { } else { // Rebuild uri to preserve uriPrefix if amount is 0 if (sharedAddress != null && !sharedAddress.includes('amount')) { - const edgeEncodeUri: EdgeEncodeUri = - useLegacyAddress && legacyAddress - ? // @ts-expect-error - { publicAddress, legacyAddress, currencyCode, nativeAmount: '0' } - : { publicAddress, currencyCode, nativeAmount: '0' } + const edgeEncodeUri: EdgeEncodeUri = { publicAddress, currencyCode, nativeAmount: '0' } const newUri = await wallet.encodeUri(edgeEncodeUri) sharedAddress = newUri.substring(0, newUri.indexOf('?')) } @@ -492,7 +483,7 @@ export class RequestComponent extends React.Component { showError(`${s.strings.title_register_fio_address}. ${s.strings.fio_request_by_fio_address_error_no_address}`) return } - if (!this.amounts || lte(this.amounts.nativeAmount, '0')) { + if (this.state.amounts == null || lte(this.state.amounts.nativeAmount, '0')) { if (Platform.OS === 'android') { showError(`${s.strings.fio_request_by_fio_address_error_invalid_amount_header}. ${s.strings.fio_request_by_fio_address_error_invalid_amount}`) return @@ -502,7 +493,7 @@ export class RequestComponent extends React.Component { } } navigation.navigate('fioRequestConfirmation', { - amounts: this.amounts + amounts: this.state.amounts }) } @@ -522,7 +513,7 @@ export class RequestComponent extends React.Component { } nextFioMode = () => { - if (this.state.isFioMode && (!this.amounts || lte(this.amounts.nativeAmount, '0'))) { + if (this.state.isFioMode && (!this.state.amounts || lte(this.state.amounts.nativeAmount, '0'))) { showError(`${s.strings.fio_request_by_fio_address_error_invalid_amount_header}. ${s.strings.fio_request_by_fio_address_error_invalid_amount}`) } else { if (this.flipInput) { @@ -595,7 +586,7 @@ const getStyles = cacheStyles((theme: Theme) => ({ } })) -export const Request = connect( +export const RequestScene = connect( state => { const { account } = state.core const { currencyWallets } = account @@ -607,7 +598,6 @@ export const Request = connect( return { account, publicAddress: '', - legacyAddress: '', fioAddressesExist: false, isConnected: state.network.isConnected } @@ -648,7 +638,6 @@ export const Request = connect( exchangeSecondaryToPrimaryRatio, primaryCurrencyInfo, secondaryCurrencyInfo, - useLegacyAddress: state.ui.scenes.requestType.useLegacyAddress, fioAddressesExist, isConnected: state.network.isConnected } @@ -661,4 +650,4 @@ export const Request = connect( dispatch(selectWalletFromModal(walletId, currencyCode)) } }) -)(withTheme(RequestComponent)) +)(withTheme(RequestSceneComponent)) diff --git a/src/components/scenes/SwapSettingsScene.tsx b/src/components/scenes/SwapSettingsScene.tsx index 0ea811bd1b5..f916226bfcd 100644 --- a/src/components/scenes/SwapSettingsScene.tsx +++ b/src/components/scenes/SwapSettingsScene.tsx @@ -1,11 +1,13 @@ -import { EdgePluginMap, EdgeSwapConfig } from 'edge-core-js/types' +import { EdgePluginMap, EdgeSwapConfig, EdgeSwapPluginType } from 'edge-core-js/types' import * as React from 'react' import { ScrollView, Text, View } from 'react-native' import FastImage from 'react-native-fast-image' import AntDesignIcon from 'react-native-vector-icons/AntDesign' +import Feather from 'react-native-vector-icons/Feather' +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import { ignoreAccountSwap, removePromotion } from '../../actions/AccountReferralActions' -import { setPreferredSwapPluginId } from '../../actions/SettingsActions' +import { setPreferredSwapPluginId, setPreferredSwapPluginType } from '../../actions/SettingsActions' import s from '../../locales/strings' import { connect } from '../../types/reactRedux' import { AccountReferral } from '../../types/ReferralTypes' @@ -22,6 +24,7 @@ import { SettingsTappableRow } from '../themed/SettingsTappableRow' interface DispatchProps { changePreferredSwapPlugin: (pluginId: string | undefined) => void + changePreferredSwapPluginType: (swapPluginType: EdgeSwapPluginType | undefined) => void ignoreAccountSwap: () => void removePromotion: (installerId: string) => Promise } @@ -31,6 +34,7 @@ interface StateProps { accountReferral: AccountReferral exchanges: EdgePluginMap settingsPreferredSwap: string | undefined + settingsPreferredSwapType: EdgeSwapPluginType | undefined } type Props = StateProps & DispatchProps & ThemeProps @@ -40,7 +44,8 @@ interface State { } export class SwapSettings extends React.Component { - sortedIds: string[] + sortedCexIds: string[] + sortedDexIds: string[] constructor(props: Props) { super(props) @@ -53,7 +58,10 @@ export class SwapSettings extends React.Component { } const exchangeIds = Object.keys(exchanges).filter(id => id !== 'transfer') - this.sortedIds = exchangeIds.sort((a, b) => exchanges[a].swapInfo.displayName.localeCompare(exchanges[b].swapInfo.displayName)) + const cexIds = exchangeIds.filter(id => exchanges[id].swapInfo.isDex !== true) + const dexIds = exchangeIds.filter(id => exchanges[id].swapInfo.isDex === true) + this.sortedCexIds = cexIds.sort((a, b) => exchanges[a].swapInfo.displayName.localeCompare(exchanges[b].swapInfo.displayName)) + this.sortedDexIds = dexIds.sort((a, b) => exchanges[a].swapInfo.displayName.localeCompare(exchanges[b].swapInfo.displayName)) } async componentWillUnmount() { @@ -67,36 +75,86 @@ export class SwapSettings extends React.Component { } handlePreferredModal = () => { - const { accountPlugins, changePreferredSwapPlugin, exchanges, ignoreAccountSwap, accountReferral, settingsPreferredSwap, theme } = this.props + const { + accountPlugins, + changePreferredSwapPlugin, + changePreferredSwapPluginType, + exchanges, + ignoreAccountSwap, + accountReferral, + settingsPreferredSwap, + settingsPreferredSwapType, + theme + } = this.props const styles = getStyles(this.props.theme) const activePlugins = bestOfPlugins(accountPlugins, accountReferral, settingsPreferredSwap) // Selected Exchange const selectedPluginId = activePlugins.preferredSwapPluginId ?? '' - const selected = exchanges[selectedPluginId] != null ? exchanges[selectedPluginId].swapInfo.displayName : s.strings.swap_preferred_cheapest + + let selected: string + if (settingsPreferredSwapType === 'DEX') { + selected = s.strings.swap_preferred_dex + } else if (settingsPreferredSwapType === 'CEX') { + selected = s.strings.swap_preferred_cex + } else { + selected = exchanges[selectedPluginId] != null ? exchanges[selectedPluginId].swapInfo.displayName : s.strings.swap_preferred_cheapest + } // Process Items - const exchangeItems = Object.keys(exchanges) + const cexItems = Object.keys(exchanges) + .filter(pluginId => exchanges[pluginId].swapInfo.isDex !== true) + .sort((a, b) => exchanges[a].swapInfo.displayName.localeCompare(exchanges[b].swapInfo.displayName)) + + const dexItems = Object.keys(exchanges) + .filter(pluginId => exchanges[pluginId].swapInfo.isDex === true) .sort((a, b) => exchanges[a].swapInfo.displayName.localeCompare(exchanges[b].swapInfo.displayName)) + + const exchangeItems = [...dexItems, ...cexItems] + // const exchangeItems = [...cexItems, ...dexItems] .filter(pluginId => exchanges[pluginId].enabled && pluginId !== 'transfer') .map(pluginId => ({ name: exchanges[pluginId].swapInfo.displayName, icon: getSwapPluginIconUri(pluginId, theme) })) - const exchangeDefaultItem = { + const preferCheapest = { name: s.strings.swap_preferred_cheapest, icon: } + const preferDex = { + name: s.strings.swap_preferred_dex, + icon: + } + + const preferCex = { + name: s.strings.swap_preferred_cex, + icon: + } + // Render Airship.show(bridge => ( - + )).then(result => { if (result == null) return if (activePlugins.swapSource.type === 'account') ignoreAccountSwap() - changePreferredSwapPlugin(Object.keys(exchanges).find(pluginId => exchanges[pluginId].swapInfo.displayName === result)) + if (result === preferDex.name) { + changePreferredSwapPluginType('DEX') + changePreferredSwapPlugin(undefined) + } else if (result === preferCex.name) { + changePreferredSwapPluginType('CEX') + changePreferredSwapPlugin(undefined) + } else { + changePreferredSwapPluginType(undefined) + changePreferredSwapPlugin(Object.keys(exchanges).find(pluginId => exchanges[pluginId].swapInfo.displayName === result)) + } }) } @@ -108,7 +166,10 @@ export class SwapSettings extends React.Component { {s.strings.settings_exchange_instruction} - {this.sortedIds.map(pluginId => this.renderPlugin(pluginId))} + + {this.sortedDexIds.map(pluginId => this.renderPlugin(pluginId))} + + {this.sortedCexIds.map(pluginId => this.renderPlugin(pluginId))} {this.renderPreferredArea()} @@ -143,7 +204,7 @@ export class SwapSettings extends React.Component { } renderPreferredArea() { - const { accountPlugins, exchanges, accountReferral, settingsPreferredSwap, theme } = this.props + const { accountPlugins, exchanges, accountReferral, settingsPreferredSwap, settingsPreferredSwapType, theme } = this.props const styles = getStyles(theme) const iconSize = theme.rem(1.25) @@ -153,7 +214,7 @@ export class SwapSettings extends React.Component { const { swapSource } = activePlugins // Pick the plugin description: - const { label, icon } = + let { label, icon } = pluginId != null && exchanges[pluginId] != null ? { label: exchanges[pluginId].swapInfo.displayName, @@ -164,6 +225,16 @@ export class SwapSettings extends React.Component { icon: } + if (settingsPreferredSwapType != null) { + if (settingsPreferredSwapType === 'DEX') { + label = s.strings.swap_preferred_dex + icon = + } else if (settingsPreferredSwapType === 'CEX') { + label = s.strings.swap_preferred_cex + icon = + } + } + // If a promo controls the swap plugin, provide a disable option: if (swapSource.type === 'promotion') { return ( @@ -217,12 +288,16 @@ export const SwapSettingsScene = connect( accountPlugins: state.account.referralCache.accountPlugins, accountReferral: state.account.accountReferral, exchanges: state.core.account.swapConfig, - settingsPreferredSwap: state.ui.settings.preferredSwapPluginId + settingsPreferredSwap: state.ui.settings.preferredSwapPluginId, + settingsPreferredSwapType: state.ui.settings.preferredSwapPluginType }), dispatch => ({ changePreferredSwapPlugin(pluginId) { dispatch(setPreferredSwapPluginId(pluginId)) }, + changePreferredSwapPluginType(swapPluginType) { + dispatch(setPreferredSwapPluginType(swapPluginType)) + }, ignoreAccountSwap() { dispatch(ignoreAccountSwap()) }, diff --git a/src/components/themed/AddressQr.tsx b/src/components/themed/AddressQr.tsx new file mode 100644 index 00000000000..64f4af13841 --- /dev/null +++ b/src/components/themed/AddressQr.tsx @@ -0,0 +1,42 @@ +import { EdgeCurrencyWallet } from 'edge-core-js' +import * as React from 'react' +import { View } from 'react-native' + +import { useAsyncValue } from '../../hooks/useAsyncValue' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' +import { QrCode } from '../themed/QrCode' + +interface Props { + address: string + currencyCode: string + wallet: EdgeCurrencyWallet + + nativeAmount?: string + onPress?: (encodedUri?: string) => void +} + +export const AddressQr = (props: Props) => { + const theme = useTheme() + const styles = getStyles(theme) + const { address, currencyCode, wallet, nativeAmount, onPress = () => {} } = props + + const [encodedUri] = useAsyncValue( + async () => await wallet.encodeUri({ publicAddress: address, currencyCode, nativeAmount }), + [address, currencyCode, nativeAmount, wallet] + ) + + return ( + + onPress(encodedUri)} marginRem={0} /> + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + container: { + aspectRatio: 1, + height: '100%', + width: '100%', + alignSelf: 'center' + } +})) diff --git a/src/components/themed/Carousel.tsx b/src/components/themed/Carousel.tsx new file mode 100644 index 00000000000..ea9ff8e2629 --- /dev/null +++ b/src/components/themed/Carousel.tsx @@ -0,0 +1,202 @@ +import * as React from 'react' +import { useEffect } from 'react' +import { LayoutChangeEvent, Pressable, View } from 'react-native' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import Animated, { runOnJS, SharedValue, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated' + +import { useWindowSize } from '../../hooks/useWindowSize' +import { useState } from '../../types/reactHooks' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' + +interface Props { + items: T[] + renderItem: (item: T, index: number) => React.ReactNode + keyExtractor?: (item: T, index: number) => string + onChangeItem?: (item: T, index: number) => void +} + +interface ItemDisplayProps { + children: React.ReactNode + currentOffset: SharedValue + itemIndex: number + onLayout: (event: LayoutChangeEvent) => void +} + +interface PaginationDotProps { + currentOffset: SharedValue + itemIndex: number + onPress?: () => void +} + +export function Carousel(props: Props) { + const theme = useTheme() + const styles = getStyles(theme) + const { items, keyExtractor = defaultKeyExtractor, renderItem, onChangeItem } = props + + const handleChangeItem = (index: number) => { + if (onChangeItem != null) { + onChangeItem(items[index], index) + } + } + + const handlePressPaginationDot = (index: number) => { + offset.value = withTiming(index) + handleChangeItem(index) + } + + // Respond to device orientation changes: + const { width: windowWidth } = useWindowSize() + useEffect(() => { + setItemWidth(itemWidth => itemWidth) + }, [windowWidth]) + + const itemCount = items.length + const [itemWidth, setItemWidth] = useState(1) // 1 avoids division by zero + const trackWidth = itemWidth * itemCount + const handleLayout = (event: LayoutChangeEvent) => { + // The check prevents layout jittering + if (Math.abs(event.nativeEvent.layout.width - itemWidth) >= 1) { + setItemWidth(event.nativeEvent.layout.width) + } + } + + const offset = useSharedValue(0) + const offsetStart = useSharedValue(0) + const panGesture = Gesture.Pan() + .activeOffsetX([-theme.rem(1.5), theme.rem(1.5)]) + .onBegin(_ => { + offsetStart.value = offset.value + }) + .onUpdate(e => { + // Subtract to make the value positive and to make calculations easier + offset.value = offsetStart.value - e.translationX / itemWidth + }) + .onEnd(_ => { + const maxOffset = itemCount - 1 + let destValue: number + if (offset.value < 0) { + // Snap to left edge: + destValue = 0 + } else if (offset.value > maxOffset) { + // Snap to right edge: + destValue = maxOffset + } else { + // Snap to the nearest item: + destValue = Math.round(offset.value) + } + offset.value = withSpring(destValue, { damping: 15 }) + + // Handle change event + runOnJS(handleChangeItem)(destValue) + }) + + const trackStyle = useAnimatedStyle(() => ({ + // We must negate the value because it is a positive number + maxWidth: itemWidth, + transform: [{ translateX: itemCount < 1 ? 0 : -((offset.value / itemCount) * trackWidth) }] + })) + + // Init/Reset: + useEffect(() => { + offset.value = 0 + offsetStart.value = 0 + }, [items, offset, offsetStart]) + + return ( + <> + + + {items.map((item, index) => ( + + {renderItem(item, index)} + + ))} + + + + {items.map((item, index) => ( + handlePressPaginationDot(index)} /> + ))} + + + ) +} + +const Item = (props: ItemDisplayProps) => { + const theme = useTheme() + const styles = getStyles(theme) + const { children, currentOffset, itemIndex, onLayout } = props + + const scaleDiff = 1 - 0.8 + const opacityDiff = 1 - 0.2 + + const animatedStyles = useAnimatedStyle(() => { + const delta = Math.min(1, Math.abs(itemIndex - currentOffset.value)) + const scale = 1 - scaleDiff * delta + const opacity = 1 - opacityDiff * delta + return { + transform: [{ scale }], + opacity: opacity + } + }) + + return ( + + {children} + + ) +} + +const PaginationDot = (props: PaginationDotProps) => { + const theme = useTheme() + const styles = getStyles(theme) + const { currentOffset, itemIndex, onPress } = props + + const animatedStyles = useAnimatedStyle(() => { + const delta = Math.min(1, Math.abs(itemIndex - currentOffset.value)) + const opacity = 1 - 0.5 * delta + return { + opacity + } + }) + + return ( + + + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + container: { + paddingTop: theme.rem(1), + alignSelf: 'center', + flex: 1, + flexDirection: 'row' + }, + itemContainer: { + // height: '100%', + // width: '100%' + }, + paginationContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: theme.rem(1) + }, + paginationDot: { + borderRadius: 10, + backgroundColor: theme.icon, + marginLeft: theme.rem(0.2), + marginRight: theme.rem(0.2), + width: theme.rem(0.5), + height: theme.rem(0.5) + } +})) + +function defaultKeyExtractor(item: any, index: number): string { + if (typeof item !== 'object' || item == null) return index.toString() + if (item.key != null) return item.key + if (item.id != null) return item.id + return index.toString() +} diff --git a/src/components/themed/ShareButtons.tsx b/src/components/themed/ShareButtons.tsx index b19971d80c1..81da01bfda2 100644 --- a/src/components/themed/ShareButtons.tsx +++ b/src/components/themed/ShareButtons.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { TouchableOpacity, View } from 'react-native' import { Fontello } from '../../assets/vector/index' +import { useHandler } from '../../hooks/useHandler' import s from '../../locales/strings' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' @@ -30,9 +31,10 @@ function ShareButton(props: { text: string; onPress: () => void; icon: string }) const { icon, text, onPress } = props const theme = useTheme() const styles = getStyles(theme) + const handlePress = useHandler(() => onPress()) return ( - + {text} diff --git a/src/components/themed/TransactionListTop.tsx b/src/components/themed/TransactionListTop.tsx index f8944c60df6..168f877b233 100644 --- a/src/components/themed/TransactionListTop.tsx +++ b/src/components/themed/TransactionListTop.tsx @@ -1,4 +1,4 @@ -import { add, gt, mul } from 'biggystring' +import { add, gt, mul, round } from 'biggystring' import { EdgeBalances, EdgeCurrencyWallet, EdgeDenomination } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator, TouchableOpacity, View } from 'react-native' @@ -16,19 +16,19 @@ import { useWatch } from '../../hooks/useWatch' import { formatNumber } from '../../locales/intl' import s from '../../locales/strings' import { makeStakePlugins } from '../../plugins/stake-plugins/stakePlugins' -import { StakePlugin, StakePolicy } from '../../plugins/stake-plugins/types' +import { PositionAllocation, StakePlugin, StakePolicy } from '../../plugins/stake-plugins/types' import { getDisplayDenomination, getExchangeDenomination } from '../../selectors/DenominationSelectors' import { getExchangeRate } from '../../selectors/WalletSelectors' import { useDispatch, useSelector } from '../../types/reactRedux' import { Actions, NavigationProp } from '../../types/routerTypes' import { triggerHaptic } from '../../util/haptic' -import { getPluginFromPolicy } from '../../util/stakeUtils' +import { getPluginFromPolicy, getPositionAllocations } from '../../util/stakeUtils' import { convertNativeToDenomination } from '../../util/utils' import { EarnCryptoCard } from '../cards/EarnCryptoCard' import { CryptoIcon } from '../icons/CryptoIcon' import { WalletListMenuModal } from '../modals/WalletListMenuModal' import { WalletListModal, WalletListResult } from '../modals/WalletListModal' -import { Airship } from '../services/AirshipInstance' +import { Airship, showWarning } from '../services/AirshipInstance' import { cacheStyles, Theme, ThemeProps, useTheme } from '../services/ThemeContext' import { EdgeText } from './EdgeText' import { OutlinedTextInput, OutlinedTextInputRef } from './OutlinedTextInput' @@ -67,6 +67,7 @@ interface State { input: string stakePolicies: StakePolicy[] | null stakePlugins: StakePlugin[] | null + lockedNativeAmount: string } type Props = OwnProps & StateProps & DispatchProps & ThemeProps @@ -78,6 +79,7 @@ export class TransactionListTopComponent extends React.PureComponent { + const { pluginId } = this.props.wallet.currencyInfo + const amount = positions.filter(p => p.currencyCode === currencyCode && p.pluginId === pluginId).reduce((prev, curr) => add(prev, curr.nativeAmount), '0') + return amount + } + + updatePositions = async ({ stakePlugins = [], stakePolicies = [] }: { stakePlugins?: StakePlugin[]; stakePolicies?: StakePolicy[] }) => { + let lockedNativeAmount = '0' + for (const stakePolicy of stakePolicies) { + const { stakePolicyId } = stakePolicy + const stakePlugin = getPluginFromPolicy(stakePlugins, stakePolicy) + if (stakePlugin == null) continue + const amount = await stakePlugin + .fetchStakePosition({ stakePolicyId, wallet: this.props.wallet }) + .then(async stakePosition => { + const { staked, earned } = getPositionAllocations(stakePosition) + return this.getTotalPosition(this.props.currencyCode, [...staked, ...earned]) + }) + .catch(err => { + console.error(err) + showWarning(s.strings.stake_unable_to_query_locked) + }) + if (amount == null) return + + lockedNativeAmount = add(lockedNativeAmount, amount) + } + this.setState({ lockedNativeAmount }) + } + handleOpenWalletListModal = () => { triggerHaptic('impactLight') const { navigation } = this.props @@ -198,7 +233,8 @@ export class TransactionListTopComponent extends React.PureComponent { + const { stakePolicies } = this.state + if (stakePolicies == null || stakePolicies.length === 0) return + const bestApy = stakePolicies.reduce((prev, curr) => Math.max(prev, curr.apy ?? 0), 0) + if (bestApy === 0) return + return round(bestApy.toString(), -1) + '%' + } + handleOnChangeText = (input: string) => { this.setState({ input }) } @@ -304,6 +348,7 @@ export class TransactionListTopComponent extends React.PureComponent - {s.strings.fragment_stake_label} + {s.strings.stake_earn_button_label} + {bestApy != null ? {bestApy} : null} ) )} @@ -435,6 +481,13 @@ const getStyles = cacheStyles((theme: Theme) => ({ fontFamily: theme.fontFaceMedium, marginLeft: theme.rem(0.25) }, + apyText: { + fontSize: theme.rem(0.75), + color: theme.textLink, + fontFamily: theme.fontFaceMedium, + marginTop: theme.rem(-0.5), + marginLeft: theme.rem(0.25) + }, // Transactions Divider transactionsDividerText: { diff --git a/src/components/themed/WalletListSortable.tsx b/src/components/themed/WalletListSortable.tsx index 82275012a3e..6a64158028f 100644 --- a/src/components/themed/WalletListSortable.tsx +++ b/src/components/themed/WalletListSortable.tsx @@ -38,12 +38,12 @@ export function WalletListSortable(props: Props) { return ( } + disableAnimatedScrolling /> ) } diff --git a/src/constants/plugins/GuiPlugins.ts b/src/constants/plugins/GuiPlugins.ts index 7daa0b0de17..c61619e91bc 100644 --- a/src/constants/plugins/GuiPlugins.ts +++ b/src/constants/plugins/GuiPlugins.ts @@ -93,21 +93,6 @@ export const guiPlugins: { [pluginId: string]: GuiPlugin } = { lockUriPath: true, displayName: 'Simplex' }, - wyre: { - pluginId: 'wyre', - storeId: 'co.edgesecure.wyre', - baseUri: hostedUri + 'co.edgesecure.wyre/index.html', - lockUriPath: true, - displayName: 'Wyre', - permissions: ['camera'], - fixCurrencyCodes: { - ETH: { pluginId: 'ethereum' }, - BTC: { pluginId: 'bitcoin' }, - DAI: { pluginId: 'ethereum', tokenId: '6b175474e89094c44da98b954eedeac495271d0f' }, - USDC: { pluginId: 'ethereum', tokenId: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' } - } - // supportEmail: 'support@sendwyre.com' - }, bity: { pluginId: 'bity', storeId: 'com.bity', diff --git a/src/constants/plugins/buyPluginList.json b/src/constants/plugins/buyPluginList.json index deef0d5cda2..0a591b05e4a 100644 --- a/src/constants/plugins/buyPluginList.json +++ b/src/constants/plugins/buyPluginList.json @@ -263,26 +263,6 @@ ], "paymentTypeLogoKey": "credit" }, - { - "id": "wyre", - "pluginId": "wyre", - "paymentTypes": [ - "bank" - ], - "description": "Fee: 1-2%\nSettlement: 1 - 5 days", - "title": "ACH (deprecated)", - "partnerIconPath": "wyre-logo-square-small.png", - "forCountries": [ - "US" - ], - "cryptoCodes": [ - "BTC", - "ETH", - "DAI", - "USDC" - ], - "paymentTypeLogoKey": "bank" - }, { "id": "bity", "pluginId": "bity", @@ -557,7 +537,7 @@ "BCH", "DASH", "DOGE", - "LTC" + "LTC" ], "paymentTypeLogoKey": "giftcard" }, @@ -582,10 +562,6 @@ "id": "bits of gold", "sortIndex": 1 }, - { - "id": "wyre", - "sortIndex": 3 - }, { "id": "bity", "sortIndex": 6 diff --git a/src/constants/plugins/sellPluginList.json b/src/constants/plugins/sellPluginList.json index c2b2bf2b97e..7aba7a6ba9a 100644 --- a/src/constants/plugins/sellPluginList.json +++ b/src/constants/plugins/sellPluginList.json @@ -13,26 +13,6 @@ ], "paymentTypeLogoKey": "bank" }, - { - "id": "wyre", - "pluginId": "wyre", - "paymentTypes": [ - "bank" - ], - "description": "Fee: 1% / Settlement: 1 - 5 days\nMonthly Limit: $150000", - "title": "ACH (deprecated)", - "partnerIconPath": "wyre-logo-square-small.png", - "forCountries": [ - "US" - ], - "cryptoCodes": [ - "BTC", - "ETH", - "DAI", - "USDC" - ], - "paymentTypeLogoKey": "bank" - }, { "id": "bits of gold", "pluginId": "bitsofgold", diff --git a/src/controllers/action-queue/WyreClient.ts b/src/controllers/action-queue/WyreClient.ts index 6a65b8298ec..91ee89ecf6a 100644 --- a/src/controllers/action-queue/WyreClient.ts +++ b/src/controllers/action-queue/WyreClient.ts @@ -29,6 +29,7 @@ interface WyreClient { const { baseUri } = ENV.WYRE_CLIENT_INIT +// Deprecated: Create generic "fiat ramp API" for future fiat on/off-ramp partner export const makeWyreClient = async (opt: WyreClientOptions): Promise => { const { account } = opt const dataStore = account.dataStore diff --git a/src/controllers/action-queue/runtime/evaluateAction.ts b/src/controllers/action-queue/runtime/evaluateAction.ts index 854cbc5a696..026546f383a 100644 --- a/src/controllers/action-queue/runtime/evaluateAction.ts +++ b/src/controllers/action-queue/runtime/evaluateAction.ts @@ -1,14 +1,11 @@ import { add, mul } from 'biggystring' -import { sprintf } from 'sprintf-js' -import s from '../../../locales/strings' import { ApprovableAction, BorrowEngine, BorrowPlugin } from '../../../plugins/borrow-plugins/types' import { queryBorrowPlugins } from '../../../plugins/helpers/borrowPluginHelpers' import { getCurrencyCode } from '../../../util/CurrencyInfoHelpers' import { getOrCreateLoanAccount } from '../../loan-manager/redux/actions' import { waitForBorrowEngineSync } from '../../loan-manager/util/waitForLoanAccountSync' import { ActionEffect, ActionProgram, ActionProgramState, BroadcastTx, ExecutableAction, ExecutionContext, ExecutionOutput, PendingTxMap } from '../types' -import { makeWyreClient } from '../WyreClient' /** * Evaluates an ActionProgram against an ActionProgramState and returns the @@ -145,61 +142,27 @@ export async function evaluateAction(context: ExecutionContext, program: ActionP } } - case 'wyre-sell': { - const { wyreAccountId, nativeAmount, tokenId, walletId } = actionOp - const wallet = account.currencyWallets[walletId] - const parentCurrencyCode = wallet.currencyInfo.currencyCode - const currencyCode = getCurrencyCode(wallet, tokenId) - - const wyreClient = await makeWyreClient({ - account - }) + // TODO: Remove once we implement action-queue disk data deletion + case 'wyre-buy': { + const makeExecutionOutput = async (): Promise => { + console.error(`Using obsolete wyre-buy action`) - const paymentAddress = await wyreClient.getCryptoPaymentAddress(wyreAccountId, walletId) - - const makeExecutionOutput = async (_dryrun: boolean, pendingTxMap: Readonly): Promise => { - // Get any pending txs for this wallet - const pendingTxs = pendingTxMap[walletId] - - const unsignedTx = await wallet.makeSpend({ - currencyCode, - // Always skip checks instead of inheriting from dryrun param because - // USDC balance isn't available when using this action op specifically - // in the case of being contained in a par, preceeded by a borrow, and - // with this par being the first op of the ActionProgram. - skipChecks: true, - spendTargets: [ - { - nativeAmount, - publicAddress: paymentAddress - } - ], - metadata: { - name: 'Wyre', - category: 'Exchange:Sell', - notes: sprintf(s.strings.wyre_metadata_sell_notes_4s, currencyCode, parentCurrencyCode, wallet.name, paymentAddress) - }, - pendingTxs - }) - const signedTx = await wallet.signTx(unsignedTx) - const networkFee = { - currencyCode: wallet.currencyInfo.currencyCode, - nativeAmount: signedTx.parentNetworkFee ?? signedTx.networkFee ?? '0' + return { + effect: { type: 'done' }, + broadcastTxs: [] } + } + + return makeExecutableAction(context, makeExecutionOutput) + } + // TODO: Remove once we implement action-queue disk data deletion + case 'wyre-sell': { + const makeExecutionOutput = async (): Promise => { + console.error(`Using obsolete wyre-sell action`) + return { - effect: { - type: 'tx-confs', - txId: signedTx.txid, - walletId, - confirmations: 1 - }, - broadcastTxs: [ - { - walletId: walletId, - networkFee, - tx: signedTx - } - ] + effect: { type: 'done' }, + broadcastTxs: [] } } @@ -362,9 +325,6 @@ export async function evaluateAction(context: ExecutionContext, program: ActionP case 'broadcast-tx': { throw new Error(`No implementation for action type ${actionOp.type}`) } - case 'wyre-buy': { - throw new Error(`No implementation for action type ${actionOp.type}`) - } } } diff --git a/src/controllers/action-queue/runtime/mock/evaluateAction.ts b/src/controllers/action-queue/runtime/mock/evaluateAction.ts index 4df63e55325..465830afcfa 100644 --- a/src/controllers/action-queue/runtime/mock/evaluateAction.ts +++ b/src/controllers/action-queue/runtime/mock/evaluateAction.ts @@ -96,21 +96,11 @@ export async function evaluateAction(context: ExecutionContext, program: ActionP } } + case 'wyre-buy': { + throw new Error(`Using obsolete wyre-buy action`) + } case 'wyre-sell': { - const { walletId } = actionOp - const wallet = await account.waitForCurrencyWallet(walletId) - - return mockExecutableAction(context, (): ExecutionOutput => { - return { - effect: { - type: 'tx-confs', - txId: mockDelayTimestamp(3000, 5000), - walletId: walletId, - confirmations: 1 - }, - broadcastTxs: mockBroadcastTxs(wallet) - } - }) + throw new Error(`Using obsolete wyre-sell action`) } case 'loan-borrow': { @@ -197,9 +187,6 @@ export async function evaluateAction(context: ExecutionContext, program: ActionP case 'broadcast-tx': { throw new Error(`No implementation for action type ${actionOp.type}`) } - case 'wyre-buy': { - throw new Error(`No implementation for action type ${actionOp.type}`) - } } } diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index ee5da7d44db..88a17f067c3 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -162,7 +162,7 @@ const strings = { fragment_create_wallet_select_valid: 'Please select valid data', fragment_request_copy_title: 'Copy', fragment_request_subtitle: 'Request', - fragment_request_address_copied: 'Request address successfully copied to clipboard', + fragment_request_address_uri_copied: 'Request address URI copied to clipboard', fragment_copied: 'Successfully copied to clipboard', request_minimum_notification_title: 'Minimum Balance Required', request_xrp_minimum_notification_body: @@ -316,7 +316,10 @@ const strings = { 'Please extract the private keys and import into a %1$s supporting wallet. If you need assistance please submit a support ticket below.', request_qr_email_title: 'Pay with %1$s:', request_email_subject: '%1$s %2$s Request', - request_qr_your_receiving_wallet_address: 'Your Receiving Wallet Address', + request_qr_your_wallet_address: 'Your Wallet Address', + request_qr_your_wrapped_segwit_address: 'Your Wrapped-Segwit Address', + request_qr_your_legacy_address: 'Your Legacy Address', + request_qr_your_segwit_address: 'Your Segwit Address', request_review_question_title: 'Enjoying %1$s?', request_review_question_subtitle: 'Please give us a review', request_review_answer_no: 'No Thanks', @@ -358,6 +361,10 @@ const strings = { settings_hide_spam_transactions: 'Hide spam transactions', swap_preferred_header: 'Preferred Exchange', swap_preferred_cheapest: 'Pick best price', + swap_preferred_dex: 'Prefer Decentralized', + swap_preferred_cex: 'Prefer Centralized', + swap_options_header_decentralized: 'Decentralized\nNo personal info required', + swap_options_header_centralized: 'Centralized\nMay require personal info', swap_preferred_instructions: 'When multiple exchanges can fill an order, prefer:', swap_preferred_promo_instructions: 'When multiple exchanges can fill an order, the current promotion always prefers:', settings_button_clear_logs: 'Clear Logs', @@ -961,9 +968,6 @@ const strings = { wallet_list_sort_highest: 'Sort by highest value', wallet_list_sort_lowest: 'Sort by lowest value', - // Wyre Metadata - wyre_metadata_sell_notes_4s: `Sell %1$s on chain %2$s from %3$s to Wyre at address: %4$s. For assistance, please contact support@sendwyre.com.`, - // Select Fio Address select_fio_address_address_from: 'Send from FIO Crypto Handle', select_fio_address_address_memo: 'FIO Memo', @@ -1150,6 +1154,8 @@ const strings = { 'Based on the total fees incurred to stake and unstake your requested amount, this is the estimated amount of time you need to keep funds staked to earn enough rewards to pay for the fees incurred. This time frame is only an estimate as is subject to change based on change in rewards APY and the total amount of funds in the staking pool.', stake_break_even_days_s: '%1$s days', stake_break_even_days_months_s: '%1$s days (%2$s months)', + stake_earn_button_label: 'Earn', + stake_unable_to_query_locked: 'Unable to query locked balance. Please try again later.', fiat_plugin_select_asset_to_purchase: 'Select Asset to Purchase', fiat_plugin_buy_currencycode: 'Buy %s', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 07c4abfcdc2..78ecd31a213 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -114,7 +114,7 @@ "fragment_create_wallet_select_valid": "Please select valid data", "fragment_request_copy_title": "Copy", "fragment_request_subtitle": "Request", - "fragment_request_address_copied": "Request address successfully copied to clipboard", + "fragment_request_address_uri_copied": "Request address URI copied to clipboard", "fragment_copied": "Successfully copied to clipboard", "request_minimum_notification_title": "Minimum Balance Required", "request_xrp_minimum_notification_body": "Ripple (XRP) wallets require a 10 XRP minimum balance. You must deposit at least 10 XRP to this address before this wallet will show a balance or transactions. 10 XRP will be unspendable for the lifetime of this wallet address.", @@ -258,7 +258,10 @@ "request_deprecated_currency_code": "Please extract the private keys and import into a %1$s supporting wallet. If you need assistance please submit a support ticket below.", "request_qr_email_title": "Pay with %1$s:", "request_email_subject": "%1$s %2$s Request", - "request_qr_your_receiving_wallet_address": "Your Receiving Wallet Address", + "request_qr_your_wallet_address": "Your Wallet Address", + "request_qr_your_wrapped_segwit_address": "Your Wrapped-Segwit Address", + "request_qr_your_legacy_address": "Your Legacy Address", + "request_qr_your_segwit_address": "Your Segwit Address", "request_review_question_title": "Enjoying %1$s?", "request_review_question_subtitle": "Please give us a review", "request_review_answer_no": "No Thanks", @@ -299,6 +302,10 @@ "settings_hide_spam_transactions": "Hide spam transactions", "swap_preferred_header": "Preferred Exchange", "swap_preferred_cheapest": "Pick best price", + "swap_preferred_dex": "Prefer Decentralized", + "swap_preferred_cex": "Prefer Centralized", + "swap_options_header_decentralized": "Decentralized\nNo personal info required", + "swap_options_header_centralized": "Centralized\nMay require personal info", "swap_preferred_instructions": "When multiple exchanges can fill an order, prefer:", "swap_preferred_promo_instructions": "When multiple exchanges can fill an order, the current promotion always prefers:", "settings_button_clear_logs": "Clear Logs", @@ -848,7 +855,6 @@ "wallet_list_sort_currencyName": "Sort by currency name", "wallet_list_sort_highest": "Sort by highest value", "wallet_list_sort_lowest": "Sort by lowest value", - "wyre_metadata_sell_notes_4s": "Sell %1$s on chain %2$s from %3$s to Wyre at address: %4$s. For assistance, please contact support@sendwyre.com.", "select_fio_address_address_from": "Send from FIO Crypto Handle", "select_fio_address_address_memo": "FIO Memo", "select_fio_address_address_memo_error": "FIO Memo Error", @@ -1000,6 +1006,8 @@ "stake_break_even_time_message": "Based on the total fees incurred to stake and unstake your requested amount, this is the estimated amount of time you need to keep funds staked to earn enough rewards to pay for the fees incurred. This time frame is only an estimate as is subject to change based on change in rewards APY and the total amount of funds in the staking pool.", "stake_break_even_days_s": "%1$s days", "stake_break_even_days_months_s": "%1$s days (%2$s months)", + "stake_earn_button_label": "Earn", + "stake_unable_to_query_locked": "Unable to query locked balance. Please try again later.", "fiat_plugin_select_asset_to_purchase": "Select Asset to Purchase", "fiat_plugin_buy_currencycode": "Buy %s", "fiat_plugin_amount_currencycode": "Amount %s", diff --git a/src/modules/Core/Account/settings.ts b/src/modules/Core/Account/settings.ts index 539eed10186..9bfdafafffb 100644 --- a/src/modules/Core/Account/settings.ts +++ b/src/modules/Core/Account/settings.ts @@ -1,7 +1,7 @@ /* eslint-disable quote-props */ -import { asArray, asBoolean, asMap, asMaybe, asNumber, asObject, asOptional, asString } from 'cleaners' -import { EdgeAccount, EdgeDenomination } from 'edge-core-js' +import { asArray, asBoolean, asMap, asMaybe, asNumber, asObject, asOptional, asString, asValue, Cleaner } from 'cleaners' +import { EdgeAccount, EdgeDenomination, EdgeSwapPluginType } from 'edge-core-js' import { asSortOption, SortOption } from '../../../components/modals/WalletListSortModal' import { showError } from '../../../components/services/AirshipInstance' @@ -28,12 +28,14 @@ export const asCurrencyCodeDenom = asObject({ const asDenominationSettings = asMap(asOptional(asObject(asMaybe(asCurrencyCodeDenom)))) export type DenominationSettings = ReturnType +export const asSwapPluginType: Cleaner<'CEX' | 'DEX'> = asValue('CEX', 'DEX') export const asSyncedAccountSettings = asObject({ autoLogoutTimeInSeconds: asOptional(asNumber, 3600), defaultFiat: asOptional(asString, 'USD'), defaultIsoFiat: asOptional(asString, 'iso:USD'), preferredSwapPluginId: asOptional(asString, ''), + preferredSwapPluginType: asOptional(asSwapPluginType), countryCode: asOptional(asString, ''), mostRecentWallets: asOptional(asArray(asMostRecentWallet), []), passwordRecoveryRemindersShown: asOptional( @@ -105,6 +107,13 @@ export const setPreferredSwapPluginId = async (account: EdgeAccount, pluginId: s }) } +export const setPreferredSwapPluginType = async (account: EdgeAccount, swapPluginType: EdgeSwapPluginType | undefined) => { + return getSyncedSettings(account).then(async settings => { + const updatedSettings = updateSettings(settings, { preferredSwapPluginType: swapPluginType }) + return setSyncedSettings(account, updatedSettings) + }) +} + export const setMostRecentWalletsSelected = async (account: EdgeAccount, mostRecentWallets: MostRecentWallet[]) => getSyncedSettings(account).then(async settings => { const updatedSettings = updateSettings(settings, { mostRecentWallets }) diff --git a/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts b/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts index cc76305bc7e..708cea69d19 100644 --- a/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts +++ b/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts @@ -802,8 +802,8 @@ const updateInboundAddresses = async (opts: EdgeGuiPluginOptions): Promise } const getPrimaryAddress = async (wallet: EdgeCurrencyWallet, currencyCode: string): Promise<{ primaryAddress: string; addressBalance: string }> => { - const { publicAddress, nativeBalance, segwitAddress, segwitNativeBalance } = await wallet.getReceiveAddress({ forceIndex: 0, currencyCode }) - const primaryAddress = segwitAddress ?? publicAddress + const { publicAddress, nativeBalance } = await wallet.getReceiveAddress({ forceIndex: 0, currencyCode }) + const primaryAddress = publicAddress let addressBalance = '0' if (wallet.displayPublicSeed?.toLowerCase() === primaryAddress.toLowerCase()) { @@ -811,7 +811,7 @@ const getPrimaryAddress = async (wallet: EdgeCurrencyWallet, currencyCode: strin // the wallet balance addressBalance = await wallet.balances[currencyCode] } else { - addressBalance = segwitAddress != null ? segwitNativeBalance ?? '0' : nativeBalance ?? '0' + addressBalance = nativeBalance ?? '0' } return { primaryAddress, addressBalance } diff --git a/src/reducers/RequestTypeReducer.ts b/src/reducers/RequestTypeReducer.ts deleted file mode 100644 index 4a7e64816df..00000000000 --- a/src/reducers/RequestTypeReducer.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Reducer } from 'redux' - -import { Action } from '../types/reduxTypes' - -export interface RequestTypeState { - useLegacyAddress: boolean - uniqueLegacyAddress: boolean -} - -const initialState: RequestTypeState = { - useLegacyAddress: false, - uniqueLegacyAddress: false -} - -export const requestType: Reducer = (state = initialState, action: Action) => { - switch (action.type) { - case 'NEW_RECEIVE_ADDRESS': { - let uniqueLegacy = true - if (action.data.receiveAddress.legacyAddress) { - uniqueLegacy = action.data.receiveAddress.publicAddress === action.data.receiveAddress.legacyAddress - } - return { - ...state, - useLegacyAddress: false, - uniqueLegacyAddress: !uniqueLegacy - } - } - - case 'USE_REGULAR_REQUEST_ADDRESS': { - return { - ...state, - useLegacyAddress: false - } - } - - case 'USE_LEGACY_REQUEST_ADDRESS': { - return { - ...state, - useLegacyAddress: true - } - } - - default: - return state - } -} diff --git a/src/reducers/scenes/ScenesReducer.ts b/src/reducers/scenes/ScenesReducer.ts index f145073cd68..3817371fc03 100644 --- a/src/reducers/scenes/ScenesReducer.ts +++ b/src/reducers/scenes/ScenesReducer.ts @@ -2,7 +2,6 @@ import { combineReducers } from 'redux' import { fioAddress, FioAddressSceneState } from '../../modules/FioAddress/reducer' import { Action } from '../../types/reduxTypes' -import { requestType, RequestTypeState } from '../RequestTypeReducer' import { createWallet, CreateWalletState } from './CreateWalletReducer' import { sendConfirmation, SendConfirmationState } from './SendConfirmationReducer' import { transactionDetails, TransactionDetailsState } from './TransactionDetailsReducer' @@ -11,7 +10,6 @@ import { transactionList, TransactionListState } from './TransactionListReducer' export interface ScenesState { readonly createWallet: CreateWalletState readonly fioAddress: FioAddressSceneState - readonly requestType: RequestTypeState readonly sendConfirmation: SendConfirmationState readonly transactionDetails: TransactionDetailsState readonly transactionList: TransactionListState @@ -20,7 +18,6 @@ export interface ScenesState { export const scenes = combineReducers({ createWallet, fioAddress, - requestType, sendConfirmation, transactionDetails, transactionList diff --git a/src/reducers/scenes/SettingsReducer.ts b/src/reducers/scenes/SettingsReducer.ts index bf8dd830b09..04d0b186b89 100644 --- a/src/reducers/scenes/SettingsReducer.ts +++ b/src/reducers/scenes/SettingsReducer.ts @@ -1,4 +1,4 @@ -import { EdgeAccount } from 'edge-core-js' +import { EdgeAccount, EdgeSwapPluginType } from 'edge-core-js' import ENV from '../../../env.json' import { SortOption } from '../../components/modals/WalletListSortModal' @@ -32,6 +32,7 @@ export interface AccountInitPayload { passwordReminder: PasswordReminderState pinLoginEnabled: boolean preferredSwapPluginId: string | undefined + preferredSwapPluginType: EdgeSwapPluginType | undefined spamFilterOn: boolean spendingLimits: SpendingLimits touchIdInfo: GuiTouchIdInfo @@ -78,6 +79,7 @@ export interface SettingsState { isTouchSupported: boolean loginStatus: boolean | null preferredSwapPluginId: string | undefined + preferredSwapPluginType: EdgeSwapPluginType | undefined pinLoginEnabled: boolean isAccountBalanceVisible: boolean walletsSort: SortOption @@ -126,6 +128,7 @@ export const settingsLegacy = (state: SettingsState = initialState, action: Acti defaultFiat, defaultIsoFiat, preferredSwapPluginId, + preferredSwapPluginType, countryCode, pinLoginEnabled, denominationSettings, @@ -145,6 +148,7 @@ export const settingsLegacy = (state: SettingsState = initialState, action: Acti defaultFiat, defaultIsoFiat, preferredSwapPluginId: preferredSwapPluginId === '' ? undefined : preferredSwapPluginId, + preferredSwapPluginType, countryCode, pinLoginEnabled, denominationSettings, @@ -220,6 +224,11 @@ export const settingsLegacy = (state: SettingsState = initialState, action: Acti return { ...state, preferredSwapPluginId: pluginId } } + case 'UI/SETTINGS/SET_PREFERRED_SWAP_PLUGIN_TYPE': { + const swapPluginType = action.data + return { ...state, preferredSwapPluginType: swapPluginType } + } + case 'UI/SETTINGS/SET_SETTINGS_LOCK': { return { ...state, diff --git a/src/types/reduxActions.ts b/src/types/reduxActions.ts index 6a0a5a4a363..8e30b72e1e1 100644 --- a/src/types/reduxActions.ts +++ b/src/types/reduxActions.ts @@ -1,5 +1,5 @@ import { Disklet } from 'disklet' -import { EdgeAccount, EdgeContext, EdgeCurrencyWallet, EdgeDenomination, EdgeLobby, EdgeReceiveAddress, EdgeSpendInfo, EdgeTransaction } from 'edge-core-js' +import { EdgeAccount, EdgeContext, EdgeCurrencyWallet, EdgeDenomination, EdgeLobby, EdgeSpendInfo, EdgeSwapPluginType, EdgeTransaction } from 'edge-core-js' import { PriceChangeNotificationSettings } from '../actions/NotificationActions' import { SortOption } from '../components/modals/WalletListSortModal' @@ -54,8 +54,6 @@ type NoDataActionName = | 'START_SHIFT_TRANSACTION' | 'UI/SEND_CONFIRMATION/RESET' | 'UI/SEND_CONFIRMATION/TOGGLE_CRYPTO_ON_TOP' - | 'USE_LEGACY_REQUEST_ADDRESS' - | 'USE_REGULAR_REQUEST_ADDRESS' | 'FIO/EXPIRED_REMINDER_SHOWN' export type Action = @@ -104,7 +102,6 @@ export type Action = } | { type: 'CONTACTS/LOAD_CONTACTS_SUCCESS'; data: { contacts: GuiContact[] } } | { type: 'GENERIC_SHAPE_SHIFT_ERROR'; data: string } - | { type: 'NEW_RECEIVE_ADDRESS'; data: { receiveAddress: EdgeReceiveAddress } } | { type: 'RESET_WALLET_LOADING_PROGRESS'; data: { walletId: string } } | { type: 'SAVE_EDGE_LOBBY'; data: EdgeLobby } | { type: 'SET_LOBBY_ERROR'; data: string } @@ -150,6 +147,7 @@ export type Action = | { type: 'UI/SETTINGS/SET_DENOMINATION_KEY'; data: { pluginId: string; currencyCode: string; denomination: EdgeDenomination } } | { type: 'UI/SETTINGS/SET_MOST_RECENT_WALLETS'; data: { mostRecentWallets: MostRecentWallet[] } } | { type: 'UI/SETTINGS/SET_PREFERRED_SWAP_PLUGIN'; data: string | undefined } + | { type: 'UI/SETTINGS/SET_PREFERRED_SWAP_PLUGIN_TYPE'; data: EdgeSwapPluginType | undefined } | { type: 'UI/SETTINGS/SET_SETTINGS_LOCK'; data: boolean } | { type: 'UI/SETTINGS/SET_WALLETS_SORT'; data: { walletsSort: SortOption } } | { type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED'; data: { pinLoginEnabled: boolean } } diff --git a/src/util/ActionProgramUtils.ts b/src/util/ActionProgramUtils.ts index ba26f9db2c7..cf4fd9ed7a3 100644 --- a/src/util/ActionProgramUtils.ts +++ b/src/util/ActionProgramUtils.ts @@ -162,13 +162,7 @@ export const makeAaveBorrowAction = async (params: AaveBorrowActionParams): Prom // Construct the Withdraw to Bank action if (destination.paymentMethodId != null) { - loanParallelActions.push({ - type: 'wyre-sell', - wyreAccountId: destination.paymentMethodId, - nativeAmount: destination.nativeAmount, - tokenId: destination.tokenId ?? defaultTokenId, - walletId: borrowEngineWallet.id - }) + throw new Error('fiat-sell not implemented yet.') } return actionOp diff --git a/src/util/CurrencyInfoHelpers.ts b/src/util/CurrencyInfoHelpers.ts index 82485ec598e..6213c1fd4d3 100644 --- a/src/util/CurrencyInfoHelpers.ts +++ b/src/util/CurrencyInfoHelpers.ts @@ -61,22 +61,22 @@ export function getCreateWalletTypes(account: EdgeAccount, filterActivation: boo const out: CreateWalletType[] = [] for (const currencyInfo of infos) { - const { currencyCode, pluginId } = currencyInfo + const { currencyCode, displayName, pluginId, walletType } = currencyInfo // Prevent plugins that are "watch only" from being allowed to create new wallets if (keysOnlyModePlugins.includes(pluginId)) continue // Prevent currencies that needs activation from being created from a modal if (filterActivation && activationRequiredCurrencyCodes.includes(currencyCode.toUpperCase())) continue // FIO disable changes - if (pluginId === 'bitcoin') { + if (['bitcoin', 'litecoin', 'digibyte'].includes(pluginId)) { out.push({ - currencyName: 'Bitcoin (Segwit)', - walletType: 'wallet:bitcoin-bip49', + currencyName: `${displayName} (Segwit)`, + walletType: `${walletType}-bip49`, pluginId, currencyCode }) out.push({ - currencyName: 'Bitcoin (no Segwit)', - walletType: 'wallet:bitcoin-bip44', + currencyName: `${displayName} (no Segwit)`, + walletType: `${walletType}-bip44`, pluginId, currencyCode }) diff --git a/yarn.lock b/yarn.lock index 59e226a062d..5a64e8621c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3749,9 +3749,9 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -"altcoin-js@https://github.com/EdgeApp/altcoin-js.git#master": +"altcoin-js@git+https://github.com/EdgeApp/altcoin-js.git#master": version "5.1.10" - resolved "https://github.com/EdgeApp/altcoin-js.git#02c16e9686c87a3fee7ae4144e89bcf43d47c8d1" + resolved "git+https://github.com/EdgeApp/altcoin-js.git#02c16e9686c87a3fee7ae4144e89bcf43d47c8d1" dependencies: bech32 "^1.1.4" big-integer "^1.6.44" @@ -6955,37 +6955,10 @@ ed25519@0.0.4: bindings "^1.2.1" nan "^2.0.9" -edge-core-js@^0.19.33: - version "0.19.34" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-0.19.34.tgz#f8074a4ea40aae194b05f8a9448ca7e352fe1c73" - integrity sha512-+MXfuO96c59pFdl1cTDjdIZLplBwFWkBOsOdSEwG6+zJwtgpw+155fMpqsry9nGbxmabGxGFVdjCkhpGXArSNg== - dependencies: - aes-js "^3.1.0" - base-x "^1.0.4" - biggystring "^4.0.0" - cleaners "^0.3.11" - currency-codes "^1.1.2" - disklet "^0.5.2" - edge-sync-client "^0.2.7" - elliptic "^6.4.0" - ethereumjs-tx "^1.3.7" - ethereumjs-util "^5.2.0" - hash.js "^1.1.7" - hmac-drbg "^1.0.1" - node-fetch "^2.6.1" - redux "^4.2.0" - redux-keto "^0.3.5" - redux-pixies "^0.3.6" - rfc4648 "^1.4.0" - scrypt-js "^2.0.3" - serverlet "^0.1.0" - yaob "^0.3.8" - yavent "^0.1.3" - -edge-core-js@^0.19.35: - version "0.19.35" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-0.19.35.tgz#4578f15ef6f533e57b034caf130842f0001f3d85" - integrity sha512-2AFZXLRcWZXn6Fx7Pxz2eYLuHQRLHtOeEK5nXHBm5pTwbMnDzHNX+mvjtG54cdtKEy7Wm2KBwo4WP0Am1kZzRA== +edge-core-js@^0.19.37: + version "0.19.37" + resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-0.19.37.tgz#f10ed23353458f9acda1b831d74c6a7cb24b88ed" + integrity sha512-zGfuL+b+C7qXAO1K3jL8czMoM/8D8Otd4fnFMjI8a2cbKY3OsmJWqciAPTER6HH9smwR5zCr7WRv0vTyB1fVmQ== dependencies: aes-js "^3.1.0" base-x "^1.0.4" @@ -7059,10 +7032,10 @@ edge-currency-monero@^0.5.4: uri-js "^3.0.2" yaob "^0.3.7" -edge-currency-plugins@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/edge-currency-plugins/-/edge-currency-plugins-1.3.3.tgz#bb5e58e4169e9f620c875baa2ccdea0fd899e7bb" - integrity sha512-GYBBaWAN5fu2bcngKfxzCmBIxn4NOvAgRqqXQCHlnn4U7xND+EXDPat9OL3Je00hKbcBOBSrS6wzA8ordLh2eA== +edge-currency-plugins@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/edge-currency-plugins/-/edge-currency-plugins-1.3.5.tgz#b307445ffcbcaa122f22b9f1cb134e1322f3e678" + integrity sha512-+D6SMXOdvm+458feOq3mItDCNXB2N2TxxuAyWD7TyIzzmgkmnkYhJY+hbzjuAnlzgUxavbUV7w/yyxpLPAcp0w== dependencies: altcoin-js "https://github.com/EdgeApp/altcoin-js.git#master" async-mutex "^0.2.6" @@ -7079,7 +7052,7 @@ edge-currency-plugins@^1.3.3: bs58smartcheck "^2.0.4" cleaners "^0.3.12" disklet "^0.4.5" - edge-core-js "^0.19.33" + edge-core-js "^0.19.37" edge-sync-client "^0.2.7" memlet "^0.1.7" uri-js "^4.4.0" @@ -7088,10 +7061,10 @@ edge-currency-plugins@^1.3.3: wifgrs "^2.0.6" ws "^7.4.6" -edge-exchange-plugins@^0.16.16: - version "0.16.16" - resolved "https://registry.yarnpkg.com/edge-exchange-plugins/-/edge-exchange-plugins-0.16.16.tgz#8c8530e47e12757efc51d00e16d00939db1e5e45" - integrity sha512-YGnvNzpGnh2b+LQHqPly0IJEN/1FKnQ9byPuNCKRjK647sqJYmXIVvxa8s5OJw5C/HmGmxuuKamjNT/y26IzkA== +edge-exchange-plugins@^0.16.17: + version "0.16.17" + resolved "https://registry.yarnpkg.com/edge-exchange-plugins/-/edge-exchange-plugins-0.16.17.tgz#405d80eaada8dcb3494a451bf74943a6ea791d81" + integrity sha512-vDGM2iVkGAxXV7YSyTZuCkEb1JPCGo8shAT3+YQj/66MpPaRMcovaCI6gMuWW4wMRxqFYBps4cq0ofV52wM9dg== dependencies: "@ethersproject/address" "^5.5.0" "@ethersproject/contracts" "^5.5.0"