diff --git a/.eslintrc.json b/.eslintrc.json index d76af120a67..d4e17158a67 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -46,7 +46,7 @@ "react-native/no-raw-text": [ "error", { - "skip": ["B", "EdgeText", "FormattedText"] + "skip": ["B", "EdgeText"] } ], "react-native/sort-styles": "off", diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e409444bb5..00427cfd544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,35 @@ ## Unreleased +## 4.1.0 + +- added: FIO conversion analytics events. +- added: Referral information on all analytics events. +- added: Sell to debit card for UK and AU +- added: ACH buy/sell through Kado +- added: Arbitrum One network support +- added: Base network support +- added: Cosmos Hub (ATOM) support +- added: Axelar (AXL) support +- added: Add WalletConnect support for Cosmos-based chains +- changed: Replace A/B test 'legacyLanding' with A/B/C/D test 'landingType' +- changed: Animate buy/sell scenes +- changed: Require Android 9 or above. - changed: Reword of the CrashScene to help users to force close (not uninstall) Edge +- changed: Updated swipe-able underlay elements for wallet list rows to match UI4 - changed: Upgrade to Android NDK version 26.1.10909125. +- changed: Re-enable fake signup captcha experiment at 50% +- changed: Adjust Transaction List Scene spacings, remove "Transactions" header +- changed: Add a referral ID to share links +- changed: Use `EdgeAsset` for defaultWallets in app config +- fixed: Fix button placement on wallet activation scene +- fixed: Resolved levitating search bar bug on Android +- fixed: Show a useful, localized error message when device doesn't have an email account +- fixed: Make FilledTextInputs take up constant vertical space +- fixed: Send Recipient Address Modal styling when saved recipients are shown +- fixed: Minor Transaction List spacing adjustments +- fixed: Insufficient Funds error in Thorchain unstaking +- removed: swipeLastUsp experiment (always allow swipes on the last USP) ## 4.0.3 (2024-02-15) @@ -14,14 +41,14 @@ - fixed: CAPTCHA image was not draggable on Android. -## 4.0.1 +## 4.0.1 (2024-02-04) - fixed: Missing background blurred dots on most scenes - fixed: Missing promo cards when multiple are on info server - fixed: Hidden Next button on Change PIN scene - fixed: Disable auto-capitalization of username when creating account -## 4.0.0 +## 4.0.0 (2024-02-02) - added: `minerTip` to `feeRateUsed` processing - added: Make the alert drop-down swipeable. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 75e8fa73ded..ffda9679d53 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -48,7 +48,7 @@ android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:exported="true" android:launchMode="singleTask" - android:windowSoftInputMode="adjustPan"> + android:windowSoftInputMode="adjustResize"> diff --git a/android/build.gradle b/android/build.gradle index 9e856cbe4cd..cc187c1c7f4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,12 +3,12 @@ buildscript { ext { buildToolsVersion = "33.0.0" - minSdkVersion = 27 // Edge modified from 21 + minSdkVersion = 28 // Edge modified from 21 compileSdkVersion = 33 targetSdkVersion = 33 // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. - ndkVersion = "23.1.7779620" + ndkVersion = "26.1.10909125" // Edge addition: kotlinVersion = "1.8.22" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cfde64ceafe..62ac918ced2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -18,13 +18,13 @@ PODS: - disklet (0.5.2): - React - DoubleConversion (1.1.6) - - edge-core-js (2.1.1): + - edge-core-js (2.2.1): - React-Core - - edge-currency-accountbased (3.0.4-1): + - edge-currency-accountbased (3.1.2-3): - React-Core - - edge-exchange-plugins (2.0.2): + - edge-exchange-plugins (2.1.1): - React-Core - - edge-login-ui-rn (3.3.2): + - edge-login-ui-rn (3.3.4): - React-Core - EXApplication (5.1.1): - ExpoModulesCore @@ -498,6 +498,9 @@ PODS: - React - react-native-camera/RN (1.13.1): - React + - react-native-compat (2.11.0): + - RCT-Folly (= 2021.07.22.00) + - React-Core - react-native-contacts (7.0.4): - React-Core - react-native-flipper (0.143.0): @@ -521,7 +524,7 @@ PODS: - React-Core - react-native-performance (5.1.0): - React-Core - - react-native-piratechain (0.4.3): + - react-native-piratechain (0.4.4): - gRPC-Swift (~> 1.8) - MnemonicSwift (~> 2.2) - React-Core @@ -536,7 +539,7 @@ PODS: - ReactCommon/turbomodule/core - react-native-webview (13.2.2): - React-Core - - react-native-zcash (0.6.6): + - react-native-zcash (0.6.7): - gRPC-Swift (~> 1.8) - MnemonicSwift (~> 2.2) - React-Core @@ -943,6 +946,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-camera (from `../node_modules/react-native-camera`) + - "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)" - react-native-contacts (from `../node_modules/react-native-contacts`) - react-native-flipper (from `../node_modules/react-native-flipper`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) @@ -1147,6 +1151,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-camera: :path: "../node_modules/react-native-camera" + react-native-compat: + :path: "../node_modules/@walletconnect/react-native-compat" react-native-contacts: :path: "../node_modules/react-native-contacts" react-native-flipper: @@ -1276,10 +1282,10 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 disklet: e7ed3e673ccad9d175a1675f9f3589ffbf69a5fd DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 - edge-core-js: 53102fb1a760ad57dc526f38951bf8c3883e99fe - edge-currency-accountbased: 551291d782decc785437388f8568514ea63c7abe - edge-exchange-plugins: 4871921ac514b98ccd24c5d49b4bf86b6bdba477 - edge-login-ui-rn: 0392e4a33e39c8bf01c66a1a68e2026678a9287c + edge-core-js: 07ef6631aa971d99679098d10171c793e6b94a4a + edge-currency-accountbased: 4b2e848fd3a80276b8d54a867ce525d315c3edb8 + edge-exchange-plugins: a457232f9ac76ba4e6508dc670d37e2d48907dd2 + edge-login-ui-rn: 45158491c219e2820a6518f2343033390f86d409 EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 EXConstants: f348da07e21b23d2b085e270d7b74f282df1a7d9 EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d @@ -1342,6 +1348,7 @@ SPEC CHECKSUMS: React-jsinspector: 4ade58a6a355d97a53f847543b14f4cb5033cb70 React-logger: 56699550750c013096a11dce3bc996e7dd583835 react-native-camera: b322bc25f47536219473c6d00c666d221f7f2695 + react-native-compat: d411b454141009ceca781cd1ca44132cace77c45 react-native-contacts: 1bff4c47816d611f26b06fa8b3eaf2db4d1b2c86 react-native-flipper: df8e2a7e5dcc857033d925ad8c7e8c52760b8e06 react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb @@ -1353,11 +1360,11 @@ SPEC CHECKSUMS: react-native-mymonero-core: 780bf854f27a0a905bcef5e7757d2de0a15b4408 react-native-netinfo: 3671b091c4843fda5e153612866ef4024b8f5d62 react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 - react-native-piratechain: 7576ad2a4f0dd983da3d622f03db89d6a4b54bd7 + react-native-piratechain: 2d1baa918b48f0acde635672504296340b039141 react-native-safari-view: 955d7160d159241b8e9395d12d10ea0ef863dcdd react-native-safe-area-context: 68b07eabfb0d14547d36f6929c0e98d818064f02 react-native-webview: b8ec89966713985111a14d6e4bf98d8b54bced0d - react-native-zcash: 42dfb76a142f4c50b368594f22339110a68918b8 + react-native-zcash: 18d627d1b559638cfb9cac04207f5460a6010546 React-perflogger: 0cc42978a483a47f3696171dac2e7033936fc82d React-RCTActionSheet: ea922b476d24f6d40b8e02ac3228412bd3637468 React-RCTAnimation: 7be2c148398eaa5beac950b2b5ec7102389ec3ad @@ -1421,4 +1428,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a84a2c04da9e8c38a3ce0d31af51c1be7866ec59 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/jestSetup.js b/jestSetup.js index a41295b0a96..86d8c5c3089 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -47,7 +47,8 @@ jest.mock('react-native-keyboard-controller', () => { useReanimatedKeyboardAnimation: () => ({ height, progress - }) + }), + useKeyboardHandler: handlers => {} } }) @@ -86,8 +87,6 @@ jest.mock('rn-qr-generator', () => ({ } })) -jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock')) - // force timezone to UTC jest.mock('dateformat', () => (number, format) => require('dateformat')(number, format, true)) diff --git a/package.json b/package.json index 6c07b5adaff..99d96aaa5e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edge-react-gui", - "version": "4.0.3", + "version": "4.1.0", "private": true, "description": "Edge Wallet React GUI", "homepage": "https://edge.app", @@ -86,6 +86,7 @@ "bs58check": "2.1.2", "core-js": "2.5.3", "create-hash": "1.2.0", + "clipboardy": "3.0.0", "elliptic": "6.5.4", "eosjs-api": "https://github.com/EdgeApp/eosjs-api.git#prepare-script", "hash-base": "3.0.4", @@ -123,10 +124,8 @@ "@react-navigation/native": "^6.1.3", "@react-navigation/stack": "^6.3.12", "@shopify/flash-list": "^1.6.3", - "@walletconnect/react-native-compat": "^2.9.0", - "@walletconnect/types": "^2.9.0", - "@walletconnect/utils": "^2.9.0", - "@walletconnect/web3wallet": "^1.8.6", + "@walletconnect/react-native-compat": "^2.11.0", + "@walletconnect/web3wallet": "^1.10.1", "assert": "^2.0.0", "async-lock": "^1.1.2", "base-x": "^4.0.0", @@ -141,13 +140,13 @@ "deepmerge": "^4.3.1", "detect-bundler": "^1.1.0", "disklet": "^0.5.2", - "edge-core-js": "^2.1.1", - "edge-currency-accountbased": "^3.0.4-1", + "edge-core-js": "^2.2.1", + "edge-currency-accountbased": "^3.1.2-3", "edge-currency-monero": "^1.1.1", "edge-currency-plugins": "^2.5.4", - "edge-exchange-plugins": "^2.0.2", - "edge-info-server": "^1.4.0", - "edge-login-ui-rn": "^3.3.2", + "edge-exchange-plugins": "^2.1.1", + "edge-info-server": "^2.0.0", + "edge-login-ui-rn": "^3.3.4", "ethers": "^5.7.2", "expo": "^48.0.0", "paraswap": "^5.2.0", @@ -186,7 +185,7 @@ "react-native-mymonero-core": "^0.3.1", "react-native-patina": "^0.1.6", "react-native-permissions": "^3.8.3", - "react-native-piratechain": "^0.4.3", + "react-native-piratechain": "^0.4.4", "react-native-popup-menu": "^0.16.1", "react-native-reanimated": "^3.5.4", "react-native-safari-view": "^2.1.0", @@ -199,7 +198,7 @@ "react-native-svg": "^13.9.0", "react-native-vector-icons": "^7.1.0", "react-native-webview": "^13.2.2", - "react-native-zcash": "^0.6.6", + "react-native-zcash": "^0.6.7", "react-redux": "^8.1.1", "redux": "^4.2.1", "redux-flipper": "^2.0.1", @@ -250,6 +249,7 @@ "@types/uuid": "^7.0.0", "@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/parser": "^5.36.2", + "@welldone-software/why-did-you-render": "^8.0.1", "appcenter-cli": "^2.12.0", "babel-eslint": "^10.1.0", "babel-jest": "^29.2.1", diff --git a/patches/react-native-camera+1.13.1.patch b/patches/react-native-camera+1.13.1.patch index e09141f1e60..3f2a04f2190 100644 --- a/patches/react-native-camera+1.13.1.patch +++ b/patches/react-native-camera+1.13.1.patch @@ -37,3 +37,42 @@ index bb8f269..572d099 100644 } @SuppressLint("all") +diff --git a/node_modules/react-native-camera/src/Camera.js b/node_modules/react-native-camera/src/Camera.js +index 9ef36a8..fc4393c 100644 +--- a/node_modules/react-native-camera/src/Camera.js ++++ b/node_modules/react-native-camera/src/Camera.js +@@ -9,13 +9,13 @@ import { + StyleSheet, + findNodeHandle, + requireNativeComponent, +- ViewPropTypes, + ActivityIndicator, + View, + Text, + UIManager, + PermissionsAndroid, + } from 'react-native'; ++import { ViewPropTypes } from 'deprecated-react-native-prop-types'; + + const requestPermissions = async ( + hasVideoAndAudio, +diff --git a/node_modules/react-native-camera/src/RNCamera.js b/node_modules/react-native-camera/src/RNCamera.js +index b1a7d97..740b033 100644 +--- a/node_modules/react-native-camera/src/RNCamera.js ++++ b/node_modules/react-native-camera/src/RNCamera.js +@@ -5,7 +5,6 @@ import { + findNodeHandle, + Platform, + NativeModules, +- ViewPropTypes, + requireNativeComponent, + View, + ActivityIndicator, +@@ -13,6 +12,7 @@ import { + StyleSheet, + PermissionsAndroid, + } from 'react-native'; ++import { ViewPropTypes } from 'deprecated-react-native-prop-types'; + + import type { FaceFeature } from './FaceDetector'; + diff --git a/patches/react-native-snap-carousel+3.9.1.patch b/patches/react-native-snap-carousel+3.9.1.patch new file mode 100644 index 00000000000..749846ceb9c --- /dev/null +++ b/patches/react-native-snap-carousel+3.9.1.patch @@ -0,0 +1,50 @@ +diff --git a/node_modules/react-native-snap-carousel/src/carousel/Carousel.js b/node_modules/react-native-snap-carousel/src/carousel/Carousel.js +index dae71a3..4dd36a2 100644 +--- a/node_modules/react-native-snap-carousel/src/carousel/Carousel.js ++++ b/node_modules/react-native-snap-carousel/src/carousel/Carousel.js +@@ -1,5 +1,6 @@ + import React, { Component } from 'react'; +-import { Animated, Easing, FlatList, I18nManager, Platform, ScrollView, View, ViewPropTypes } from 'react-native'; ++import { Animated, Easing, FlatList, I18nManager, Platform, ScrollView, View } from 'react-native'; ++import { ViewPropTypes } from 'deprecated-react-native-prop-types'; + import PropTypes from 'prop-types'; + import shallowCompare from 'react-addons-shallow-compare'; + import { +diff --git a/node_modules/react-native-snap-carousel/src/pagination/Pagination.js b/node_modules/react-native-snap-carousel/src/pagination/Pagination.js +index 5c021cf..d300dce 100644 +--- a/node_modules/react-native-snap-carousel/src/pagination/Pagination.js ++++ b/node_modules/react-native-snap-carousel/src/pagination/Pagination.js +@@ -1,5 +1,6 @@ + import React, { PureComponent } from 'react'; +-import { I18nManager, Platform, View, ViewPropTypes } from 'react-native'; ++import { I18nManager, Platform, View } from 'react-native'; ++import { ViewPropTypes } from 'deprecated-react-native-prop-types'; + import PropTypes from 'prop-types'; + import PaginationDot from './PaginationDot'; + import styles from './Pagination.style'; +diff --git a/node_modules/react-native-snap-carousel/src/pagination/PaginationDot.js b/node_modules/react-native-snap-carousel/src/pagination/PaginationDot.js +index e59d196..d2c8dcc 100644 +--- a/node_modules/react-native-snap-carousel/src/pagination/PaginationDot.js ++++ b/node_modules/react-native-snap-carousel/src/pagination/PaginationDot.js +@@ -1,5 +1,6 @@ + import React, { PureComponent } from 'react'; +-import { View, Animated, Easing, TouchableOpacity, ViewPropTypes } from 'react-native'; ++import { View, Animated, Easing, TouchableOpacity } from 'react-native'; ++import { ViewPropTypes } from 'deprecated-react-native-prop-types'; + import PropTypes from 'prop-types'; + import styles from './Pagination.style'; + +diff --git a/node_modules/react-native-snap-carousel/src/parallaximage/ParallaxImage.js b/node_modules/react-native-snap-carousel/src/parallaximage/ParallaxImage.js +index 8bc774a..d6d9de3 100644 +--- a/node_modules/react-native-snap-carousel/src/parallaximage/ParallaxImage.js ++++ b/node_modules/react-native-snap-carousel/src/parallaximage/ParallaxImage.js +@@ -1,7 +1,8 @@ + // Parallax effect inspired by https://github.com/oblador/react-native-parallax/ + + import React, { Component } from 'react'; +-import { View, ViewPropTypes, Image, Animated, Easing, ActivityIndicator, findNodeHandle } from 'react-native'; ++import { View, Image, Animated, Easing, ActivityIndicator, findNodeHandle } from 'react-native'; ++import { ViewPropTypes } from 'deprecated-react-native-prop-types'; + import PropTypes from 'prop-types'; + import styles from './ParallaxImage.style'; + diff --git a/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap b/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap index fd9fa693fcc..5805e1355d0 100644 --- a/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap +++ b/src/__tests__/__snapshots__/GuiPlugins.test.ts.snap @@ -166,6 +166,20 @@ Settlement: 1 - 48 hours", exports[`Production plugin data Buy plugins match snapshot on iOS + US 1`] = ` [ + { + "cryptoCodes": [], + "deepPath": "", + "deepQuery": {}, + "description": "Fee: ~2% +Settlement: ~5 minutes", + "paymentType": "iach", + "paymentTypeLogoKey": "bank", + "paymentTypes": [ + "iach", + ], + "pluginId": "iach", + "title": "Instant ACH Bank Transfer", + }, { "cryptoCodes": [], "deepPath": "", @@ -220,7 +234,21 @@ exports[`Production plugin data Sell plugins match snapshot on iOS + US 1`] = ` "cryptoCodes": [], "deepPath": "", "deepQuery": {}, - "description": "Fee: 2.5% + "description": "Fee: 1.5% +Settlement: 2 - 3 days", + "paymentType": "ach", + "paymentTypeLogoKey": "bank", + "paymentTypes": [ + "ach", + ], + "pluginId": "ach", + "title": "ACH Bank Transfer", + }, + { + "cryptoCodes": [], + "deepPath": "", + "deepQuery": {}, + "description": "Fee: 4.0% Settlement: 5 min - 24 hours", "paymentType": "credit", "paymentTypeLogoKey": "bank", diff --git a/src/__tests__/components/BalanceCard.test.tsx b/src/__tests__/components/BalanceCard.test.tsx index c995c8ed596..7324a7c85d3 100644 --- a/src/__tests__/components/BalanceCard.test.tsx +++ b/src/__tests__/components/BalanceCard.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals' import * as React from 'react' -import renderer from 'react-test-renderer' +import TestRenderer from 'react-test-renderer' import { BalanceCardUi4 } from '../../components/ui4/BalanceCardUi4' import { FakeProviders } from '../../util/fake/FakeProviders' @@ -8,12 +8,13 @@ import { fakeNavigation } from '../../util/fake/fakeSceneProps' describe('BalanceCard', () => { it('should render with loading props', () => { - const actual = renderer.create( + const renderer = TestRenderer.create( ) - expect(actual).toMatchSnapshot() + expect(renderer.toJSON()).toMatchSnapshot() + renderer.unmount() }) }) diff --git a/src/__tests__/components/Card.test.tsx b/src/__tests__/components/Card.test.tsx deleted file mode 100644 index 53a44f26131..00000000000 --- a/src/__tests__/components/Card.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from '@jest/globals' -import * as React from 'react' -import { createRenderer } from 'react-test-renderer/shallow' - -import { CardComponent } from '../../components/cards/Card' -import { getTheme } from '../../components/services/ThemeContext' - -describe('Card', () => { - it('should render with loading props', () => { - const renderer = createRenderer() - - const fakeChild: React.ReactNode = 'string' - - const actual = renderer.render( - - {fakeChild} - - ) - - expect(actual).toMatchSnapshot() - }) -}) diff --git a/src/__tests__/components/ClickableRow.test.tsx b/src/__tests__/components/ClickableRow.test.tsx deleted file mode 100644 index a07f188fbba..00000000000 --- a/src/__tests__/components/ClickableRow.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from '@jest/globals' -import * as React from 'react' -import { createRenderer } from 'react-test-renderer/shallow' - -import { getTheme } from '../../components/services/ThemeContext' -import { ClickableRowComponent } from '../../components/themed/ClickableRow' - -describe('ClickableRow', () => { - it('should render with loading props', () => { - const renderer = createRenderer() - - const fakeChild: React.ReactNode = 'String' - - const actual = renderer.render( - undefined} onLongPress={() => undefined} autoHeight underline marginRem={11} paddingRem={11} theme={getTheme()}> - {fakeChild} - - ) - - expect(actual).toMatchSnapshot() - }) -}) diff --git a/src/__tests__/components/EdgeText.test.tsx b/src/__tests__/components/EdgeText.test.tsx index 1af288f2358..541f83534a4 100644 --- a/src/__tests__/components/EdgeText.test.tsx +++ b/src/__tests__/components/EdgeText.test.tsx @@ -1,22 +1,21 @@ import { describe, expect, it } from '@jest/globals' import * as React from 'react' -import { createRenderer } from 'react-test-renderer/shallow' +import TestRenderer from 'react-test-renderer' -import { getTheme } from '../../components/services/ThemeContext' -import { EdgeTextComponent } from '../../components/themed/EdgeText' +import { EdgeText } from '../../components/themed/EdgeText' +import { FakeProviders } from '../../util/fake/FakeProviders' describe('EdgeText', () => { it('should render with some props', () => { - const renderer = createRenderer() - - const fakeChild: React.ReactNode = 'Hello world' - - const actual = renderer.render( - - {fakeChild} - + const renderer = TestRenderer.create( + + + Hello world + + ) - expect(actual).toMatchSnapshot() + expect(renderer.toJSON()).toMatchSnapshot() + renderer.unmount() }) }) diff --git a/src/__tests__/components/SceneHeader.test.tsx b/src/__tests__/components/SceneHeader.test.tsx index b7eda6ef0e0..662a73ec14d 100644 --- a/src/__tests__/components/SceneHeader.test.tsx +++ b/src/__tests__/components/SceneHeader.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals' import * as React from 'react' -import renderer from 'react-test-renderer' +import TestRenderer from 'react-test-renderer' import { SceneHeader } from '../../components/themed/SceneHeader' import { FakeProviders } from '../../util/fake/FakeProviders' @@ -9,7 +9,7 @@ describe('SceneHeader', () => { it('should render with loading props', () => { const fakeChild: React.ReactNode = 'hello' - const actual = renderer.create( + const renderer = TestRenderer.create( {fakeChild} @@ -17,6 +17,7 @@ describe('SceneHeader', () => { ) - expect(actual).toMatchSnapshot() + expect(renderer.toJSON()).toMatchSnapshot() + renderer.unmount() }) }) diff --git a/src/__tests__/components/Tile.test.tsx b/src/__tests__/components/Tile.test.tsx deleted file mode 100644 index c7985c93d66..00000000000 --- a/src/__tests__/components/Tile.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from '@jest/globals' -import * as React from 'react' -import { createRenderer } from 'react-test-renderer/shallow' - -import { getTheme } from '../../components/services/ThemeContext' -import { TileComponent } from '../../components/tiles/Tile' - -describe('Tile', () => { - it('should render with loading props', () => { - const renderer = createRenderer() - - const fakeChild: React.ReactNode = 11 as any - - const actual = renderer.render( - undefined} title="string" type="copy" contentPadding maximumHeight="small" theme={getTheme()}> - {fakeChild} - - ) - - expect(actual).toMatchSnapshot() - }) -}) diff --git a/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap b/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap index 196a921350a..1e73d788c60 100644 --- a/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap @@ -115,21 +115,17 @@ exports[`BalanceCard should render with loading props 1`] = ` "color": "#00f1a2", "fontSize": 22, }, - [ - { - "alignSelf": "center", - "marginLeft": 6, - "marginRight": 0, - }, - { - "textShadowColor": "rgba(0, 0, 0, 0.514)", - "textShadowOffset": { - "height": 0, - "width": 0, - }, - "textShadowRadius": 3, + { + "alignSelf": "center", + "marginLeft": 6, + "marginRight": 0, + "textShadowColor": "rgba(0, 0, 0, 0.514)", + "textShadowOffset": { + "height": 0, + "width": 0, }, - ], + "textShadowRadius": 3, + }, { "fontFamily": "Ionicons", "fontStyle": "normal", @@ -163,21 +159,17 @@ exports[`BalanceCard should render with loading props 1`] = ` "fontSize": 22, "includeFontPadding": false, }, - [ - { - "height": 51, - "marginBottom": 11, - "marginTop": 6, - }, - { - "textShadowColor": "rgba(0, 0, 0, 0.514)", - "textShadowOffset": { - "height": 0, - "width": 0, - }, - "textShadowRadius": 3, + { + "height": 51, + "marginBottom": 11, + "marginTop": 6, + "textShadowColor": "rgba(0, 0, 0, 0.514)", + "textShadowOffset": { + "height": 0, + "width": 0, }, - ], + "textShadowRadius": 3, + }, null, ] } @@ -191,8 +183,9 @@ exports[`BalanceCard should render with loading props 1`] = ` layout="row" style={ { - "flex": 1, "flexDirection": "row-reverse", + "flexGrow": 1, + "flexShrink": 1, "justifyContent": "center", "margin": 11, } @@ -230,10 +223,11 @@ exports[`BalanceCard should render with loading props 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "borderRadius": 34, "flex": 1, "justifyContent": "center", + "margin": -11, "opacity": 1, + "padding": 11, } } > @@ -261,36 +255,22 @@ exports[`BalanceCard should render with loading props 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "stretch", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "flex": 1, - }, - ], - undefined, - undefined, ] } > @@ -371,10 +351,11 @@ exports[`BalanceCard should render with loading props 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "borderRadius": 34, "flex": 1, "justifyContent": "center", + "margin": -11, "opacity": 1, + "padding": 11, } } > @@ -402,36 +383,22 @@ exports[`BalanceCard should render with loading props 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "stretch", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "flex": 1, - }, - ], - undefined, - undefined, ] } > diff --git a/src/__tests__/components/__snapshots__/Card.test.tsx.snap b/src/__tests__/components/__snapshots__/Card.test.tsx.snap deleted file mode 100644 index c88aa04dc9b..00000000000 --- a/src/__tests__/components/__snapshots__/Card.test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Card should render with loading props 1`] = ` - - - string - - -`; diff --git a/src/__tests__/components/__snapshots__/ClickableRow.test.tsx.snap b/src/__tests__/components/__snapshots__/ClickableRow.test.tsx.snap deleted file mode 100644 index 86af128cd8b..00000000000 --- a/src/__tests__/components/__snapshots__/ClickableRow.test.tsx.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ClickableRow should render with loading props 1`] = ` - - - String - - -`; diff --git a/src/__tests__/components/__snapshots__/FilledTextInput.test.tsx.snap b/src/__tests__/components/__snapshots__/FilledTextInput.test.tsx.snap index 8804bbbfab2..c50f885aca7 100644 --- a/src/__tests__/components/__snapshots__/FilledTextInput.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/FilledTextInput.test.tsx.snap @@ -115,6 +115,7 @@ exports[`FilledTextInput should render with some props 1`] = ` "value": 0, } } + editable={true} focusAnimation={ { "value": 0, @@ -141,7 +142,7 @@ exports[`FilledTextInput should render with some props 1`] = ` value="string" /> - - + - + diff --git a/src/__tests__/components/__snapshots__/LineTextDivider.test.tsx.snap b/src/__tests__/components/__snapshots__/LineTextDivider.test.tsx.snap index 72cde2f8e42..0d0d28b9f38 100644 --- a/src/__tests__/components/__snapshots__/LineTextDivider.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/LineTextDivider.test.tsx.snap @@ -22,7 +22,7 @@ exports[`LineTextDivider should render with loading props 1`] = ` } } /> - string - + hello - title - + diff --git a/src/__tests__/components/__snapshots__/Tile.test.tsx.snap b/src/__tests__/components/__snapshots__/Tile.test.tsx.snap deleted file mode 100644 index ba6345b01c7..00000000000 --- a/src/__tests__/components/__snapshots__/Tile.test.tsx.snap +++ /dev/null @@ -1,90 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Tile should render with loading props 1`] = ` - - - - - - - string - - - string - - 11 - - - - - -`; diff --git a/src/__tests__/components/__snapshots__/TransactionListRow.test.tsx.snap b/src/__tests__/components/__snapshots__/TransactionListRow.test.tsx.snap index aabc76a2774..0fa10336d69 100644 --- a/src/__tests__/components/__snapshots__/TransactionListRow.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/TransactionListRow.test.tsx.snap @@ -63,8 +63,9 @@ exports[`TransactionListRow should render with loading props 1`] = ` style={ { "alignItems": "center", - "flex": 1, "flexDirection": "row", + "flexGrow": 1, + "flexShrink": 1, } } > @@ -142,8 +143,9 @@ exports[`TransactionListRow should render with loading props 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginBottom": 6, diff --git a/src/__tests__/components/__snapshots__/TransactionListTop.test.tsx.snap b/src/__tests__/components/__snapshots__/TransactionListTop.test.tsx.snap index 6c7b59d79e9..a37d5bd322d 100644 --- a/src/__tests__/components/__snapshots__/TransactionListTop.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/TransactionListTop.test.tsx.snap @@ -705,50 +705,6 @@ exports[`TransactionListTop should render (with ENABLE_VISA_PROGRAM) 1`] = ` } } > - - - - Transactions - - - - - - Transactions - - - `; diff --git a/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap b/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap index 23ae7ac7458..d6b3e91071c 100644 --- a/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap @@ -42,8 +42,9 @@ exports[`AdvancedDetailsCard should render with loading props 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, diff --git a/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap index 91a1342ea24..174880714d5 100644 --- a/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap @@ -175,9 +175,13 @@ exports[`CategoryModal should render with a subcategory 1`] = ` "alignSelf": "flex-start", "flexGrow": 1, "justifyContent": "flex-start", - "marginRight": 6, + "marginBottom": -17, + "marginRight": -22, + "marginTop": -22, "opacity": 1, - "paddingTop": 3, + "paddingBottom": 17, + "paddingRight": 28, + "paddingTop": 26, } } > @@ -657,6 +661,7 @@ exports[`CategoryModal should render with a subcategory 1`] = ` "value": 0, } } + editable={true} focusAnimation={ { "value": 0, @@ -732,7 +737,11 @@ exports[`CategoryModal should render with a subcategory 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "marginHorizontal": -22, + "marginVertical": -28, "opacity": 1, + "paddingHorizontal": 22, + "paddingVertical": 28, } } testID="undefined.clearIcon" @@ -1591,9 +1600,13 @@ exports[`CategoryModal should render with an empty subcategory 1`] = ` "alignSelf": "flex-start", "flexGrow": 1, "justifyContent": "flex-start", - "marginRight": 6, + "marginBottom": -17, + "marginRight": -22, + "marginTop": -22, "opacity": 1, - "paddingTop": 3, + "paddingBottom": 17, + "paddingRight": 28, + "paddingTop": 26, } } > @@ -2073,6 +2086,7 @@ exports[`CategoryModal should render with an empty subcategory 1`] = ` "value": 0, } } + editable={true} focusAnimation={ { "value": 0, @@ -2148,7 +2162,11 @@ exports[`CategoryModal should render with an empty subcategory 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "marginHorizontal": -22, + "marginVertical": -28, "opacity": 1, + "paddingHorizontal": 22, + "paddingVertical": 28, } } testID="undefined.clearIcon" diff --git a/src/__tests__/modals/__snapshots__/HelpModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/HelpModal.test.tsx.snap index 53f1f26dbf8..b29727da434 100644 --- a/src/__tests__/modals/__snapshots__/HelpModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/HelpModal.test.tsx.snap @@ -207,10 +207,17 @@ exports[`HelpModal should render with loading props 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "marginBottom": -17, + "marginRight": -22, + "marginTop": -22, "opacity": 1, + "paddingBottom": 17, + "paddingLeft": 22, + "paddingRight": 28, + "paddingTop": 22, "position": "absolute", - "right": 6, - "top": 3, + "right": 0, + "top": 0, } } > diff --git a/src/__tests__/modals/__snapshots__/WalletListModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/WalletListModal.test.tsx.snap index d43d071406e..6e42d911ffc 100644 --- a/src/__tests__/modals/__snapshots__/WalletListModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/WalletListModal.test.tsx.snap @@ -345,7 +345,9 @@ exports[`WalletListModal should render with loading props 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "margin": -22, "opacity": 1, + "padding": 22, } } testID="undefined.clearIcon" @@ -416,10 +418,17 @@ exports[`WalletListModal should render with loading props 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "marginBottom": -17, + "marginRight": -22, + "marginTop": -22, "opacity": 1, + "paddingBottom": 17, + "paddingLeft": 22, + "paddingRight": 28, + "paddingTop": 22, "position": "absolute", - "right": 6, - "top": 3, + "right": 0, + "top": 0, } } > diff --git a/src/__tests__/scenes/ChangePasswordScene.test.tsx b/src/__tests__/scenes/ChangePasswordScene.test.tsx index c130a3ecf30..f2886a6725a 100644 --- a/src/__tests__/scenes/ChangePasswordScene.test.tsx +++ b/src/__tests__/scenes/ChangePasswordScene.test.tsx @@ -2,18 +2,23 @@ import { describe, expect, it } from '@jest/globals' import * as React from 'react' import { createRenderer } from 'react-test-renderer/shallow' -import { ChangePasswordComponent } from '../../components/scenes/ChangePasswordScene' +import { ChangePasswordScene } from '../../components/scenes/ChangePasswordScene' +import { FakeProviders, FakeState } from '../../util/fake/FakeProviders' +import { fakeRootState } from '../../util/fake/fakeRootState' import { fakeSceneProps } from '../../util/fake/fakeSceneProps' -describe('ChangePasswordComponent', () => { +describe('ChangePasswordScene', () => { it('should render with loading props', () => { const renderer = createRenderer() + const rootState: FakeState = { ...fakeRootState, core: { account: {}, context: { appId: '' } } } - const fakeAccount: any = {} - const fakeContext: any = { apiKey: '', appId: '' } - - const actual = renderer.render() + const actual = renderer.render( + + + + ) expect(actual).toMatchSnapshot() + renderer.unmount() }) }) diff --git a/src/__tests__/scenes/ChangePinScene.test.tsx b/src/__tests__/scenes/ChangePinScene.test.tsx index dd1af0ff9c8..2d7c45b9fd6 100644 --- a/src/__tests__/scenes/ChangePinScene.test.tsx +++ b/src/__tests__/scenes/ChangePinScene.test.tsx @@ -2,17 +2,21 @@ import { describe, expect, it } from '@jest/globals' import * as React from 'react' import { createRenderer } from 'react-test-renderer/shallow' -import { ChangePinComponent } from '../../components/scenes/ChangePinScene' +import { ChangePinScene } from '../../components/scenes/ChangePinScene' +import { FakeProviders, FakeState } from '../../util/fake/FakeProviders' +import { fakeRootState } from '../../util/fake/fakeRootState' import { fakeSceneProps } from '../../util/fake/fakeSceneProps' describe('ChangePinComponent', () => { it('should render with loading props', () => { const renderer = createRenderer() + const rootState: FakeState = { ...fakeRootState, core: { account: {}, context: { appId: '' } } } - const fakeAccount: any = {} - const fakeContext: any = { apiKey: '', appId: '' } - - const actual = renderer.render() + const actual = renderer.render( + + + + ) expect(actual).toMatchSnapshot() }) diff --git a/src/__tests__/scenes/CreateWalletAccountSetupScene.test.tsx b/src/__tests__/scenes/CreateWalletAccountSetupScene.test.tsx index a61631382b6..bbf6f6b93a4 100644 --- a/src/__tests__/scenes/CreateWalletAccountSetupScene.test.tsx +++ b/src/__tests__/scenes/CreateWalletAccountSetupScene.test.tsx @@ -3,24 +3,33 @@ import * as React from 'react' import TestRenderer from 'react-test-renderer' import { CreateWalletAccountSetupScene } from '../../components/scenes/CreateWalletAccountSetupScene' -import { FakeProviders } from '../../util/fake/FakeProviders' +import { btcCurrencyInfo } from '../../util/fake/fakeBtcInfo' +import { FakeProviders, FakeState } from '../../util/fake/FakeProviders' import { fakeSceneProps } from '../../util/fake/fakeSceneProps' describe('CreateWalletAccountSelect', () => { it('renders', () => { + const mockState: FakeState = { + core: { + account: { + currencyConfig: { + bitcoin: { currencyInfo: btcCurrencyInfo } + }, + currencyWallets: { + '332s0ds39f': { currencyInfo: btcCurrencyInfo } + }, + watch: () => () => {} + } + } + } + const renderer = TestRenderer.create( - + diff --git a/src/__tests__/scenes/FioAddressSettingsScene.test.tsx b/src/__tests__/scenes/FioAddressSettingsScene.test.tsx index 33fa0e8fc07..f734af490ec 100644 --- a/src/__tests__/scenes/FioAddressSettingsScene.test.tsx +++ b/src/__tests__/scenes/FioAddressSettingsScene.test.tsx @@ -30,6 +30,7 @@ describe('FioAddressSettingsComponent', () => { isConnected refreshAllFioAddresses={async () => {}} theme={getTheme()} + onLogEvent={() => {}} /> ) diff --git a/src/__tests__/scenes/RequestScene.test.tsx b/src/__tests__/scenes/RequestScene.test.tsx index d6e73a3b14e..e9973a27770 100644 --- a/src/__tests__/scenes/RequestScene.test.tsx +++ b/src/__tests__/scenes/RequestScene.test.tsx @@ -12,7 +12,7 @@ describe('Request', () => { const actual = renderer.render( { const actual = renderer.render( - - + } +> + + + + + + + + + + + + `; diff --git a/src/__tests__/scenes/__snapshots__/ChangePinScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/ChangePinScene.test.tsx.snap index 0a959be9c09..78b7bb5e94d 100644 --- a/src/__tests__/scenes/__snapshots__/ChangePinScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/ChangePinScene.test.tsx.snap @@ -1,17 +1,95 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ChangePinComponent should render with loading props 1`] = ` - - - + } +> + + + + + + + + + + + + `; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap index 9e4d25a376d..48a7e4c24f5 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap @@ -228,6 +228,7 @@ exports[`CreateWalletAccountSelect renders 1`] = ` style={ { "height": 1334, + "maxHeight": 1334, "padding": 0, "width": 750, } @@ -552,6 +553,7 @@ exports[`CreateWalletAccountSelect renders 1`] = ` "value": 0, } } + editable={true} focusAnimation={ { "value": 0, @@ -628,7 +630,11 @@ exports[`CreateWalletAccountSelect renders 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "marginHorizontal": -22, + "marginVertical": -28, "opacity": 1, + "paddingHorizontal": 22, + "paddingVertical": 28, } } testID="undefined.clearIcon" @@ -669,12 +675,17 @@ exports[`CreateWalletAccountSelect renders 1`] = ` @@ -772,41 +785,22 @@ exports[`CreateWalletAccountSelect renders 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { "alignSelf": "center", - "opacity": 0.3, }, { "height": 67, - "paddingHorizontal": 45, - }, - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "paddingHorizontal": 34, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], { - "marginBottom": 11, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, + "opacity": 0.3, }, - undefined, ] } > diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap index 74f4ea1c791..9632e3ccc51 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap @@ -518,6 +518,7 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` "value": 0, } } + editable={true} focusAnimation={ { "value": 0, @@ -593,7 +594,11 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "marginHorizontal": -22, + "marginVertical": -28, "opacity": 1, + "paddingHorizontal": 22, + "paddingVertical": 28, } } testID="undefined.clearIcon" @@ -666,13 +671,15 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` { "alignItems": "center", "alignSelf": "center", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", - "marginBottom": 11, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, "opacity": 1, + "paddingBottom": 22, + "paddingLeft": 22, + "paddingRight": 22, + "paddingTop": 22, } } > @@ -700,41 +707,22 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { "alignSelf": "center", - "opacity": 1, }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], - { - "marginBottom": 11, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, + "opacity": 1, }, - undefined, ] } > diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap index 0b03cee40a9..a7bc071e2bb 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap @@ -481,7 +481,9 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` onStartShouldSetResponder={[Function]} style={ { + "margin": -22, "opacity": 1, + "padding": 22, } } testID="undefined.clearIcon" @@ -538,6 +540,7 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` "currencyCode": "ETH", "displayName": "Ethereum", "key": "create-undefined-ethereum", + "keyOptions": {}, "pluginId": "ethereum", "tokenId": null, "walletType": undefined, @@ -584,15 +587,7 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` }, ] } - extraData={ - { - "create-ethereum-1985365e9f78359a9b6ad760e32412f4a445e862": false, - "create-ethereum-221657776846890989a759ba2973e427dff5c9bb": false, - "create-ethereum-2e91e3e54c5788e9fdd6a181497fdcea1de1bcc1": false, - "create-ethereum-6b175474e89094c44da98b954eedeac495271d0f": false, - "create-undefined-ethereum": false, - } - } + extraData={Set {}} getItem={[Function]} getItemCount={[Function]} handlerTag={1} @@ -1829,13 +1824,15 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` { "alignItems": "center", "alignSelf": "center", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", - "marginBottom": 11, - "marginLeft": -11, - "marginRight": -11, - "marginTop": 0, "opacity": 1, + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, } } > @@ -1863,41 +1860,22 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { "alignSelf": "center", - "opacity": 1, }, { "height": 67, - "paddingHorizontal": 45, - }, - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "paddingHorizontal": 34, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], { - "marginBottom": 11, - "marginLeft": -11, - "marginRight": -11, - "marginTop": 0, + "opacity": 1, }, - undefined, ] } > diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap index 9ce6269ba80..d94adde195e 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap @@ -283,6 +283,7 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = style={ { "flex": 1, + "margin": 11, "paddingTop": 11, } } @@ -1185,13 +1186,15 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = { "alignItems": "center", "alignSelf": "center", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", - "marginBottom": 0, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, "opacity": 1, + "paddingBottom": 11, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 22, } } > @@ -1219,41 +1222,22 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { "alignSelf": "center", - "opacity": 1, }, { "height": 67, - "paddingHorizontal": 45, - }, - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "paddingHorizontal": 34, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], { - "marginBottom": 0, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, + "opacity": 1, }, - undefined, ] } > @@ -1317,16 +1301,15 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = { "alignItems": "center", "alignSelf": "center", - "borderRadius": 34, - "height": undefined, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", - "marginBottom": 22, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, "opacity": 1, - "paddingHorizontal": 0, - "paddingVertical": 0, + "paddingBottom": 22, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 11, } } > @@ -1354,47 +1337,25 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { "alignSelf": "center", - "opacity": 1, }, { - "height": 67, - "paddingHorizontal": 45, + "alignSelf": "center", }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "height": undefined, + "padding": 0, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - { - "alignSelf": "center", - "height": undefined, - "paddingHorizontal": 0, - "paddingVertical": 0, - }, - ], { - "marginBottom": 22, - "marginLeft": 11, - "marginRight": 11, - "marginTop": 11, + "opacity": 1, }, - undefined, ] } > diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap index 3b361b62d80..51f4352031c 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap @@ -228,6 +228,7 @@ exports[`CryptoExchangeQuoteScreenComponent should render with loading props 1`] style={ { "height": 1334, + "maxHeight": 1334, "padding": 0, "width": 750, } diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeScene.test.tsx.snap index 71d08daae8f..352dddd809c 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeScene.test.tsx.snap @@ -1,15 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CryptoExchangeComponent should render with loading props 1`] = ` - - + - - + - - + + - - + - - + - - + `; diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeSuccessScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeSuccessScene.test.tsx.snap index f7b82543f9f..53ba50ee48d 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeSuccessScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeSuccessScene.test.tsx.snap @@ -13,7 +13,7 @@ exports[`CryptoExchangeSuccessComponent should render with loading props 1`] = ` } } > - Congratulations! - - + Your exchange has been successfully completed! - - + Exchanges may take a few minutes and up to 24 hours to process. - + diff --git a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap index 48e61da4b06..8795d2cdb00 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap @@ -228,6 +228,7 @@ exports[`CurrencyNotificationComponent should render with loading props 1`] = ` style={ { "height": 1334, + "maxHeight": 1334, "padding": 0, "width": 750, } diff --git a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap index c437f6b2113..866aa328c38 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap @@ -228,6 +228,7 @@ exports[`CurrencySettings should render 1`] = ` style={ { "height": 1334, + "maxHeight": 1334, "padding": 0, "width": 750, } diff --git a/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap index 62aec20fbd9..0047eba1ad8 100644 --- a/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap @@ -428,13 +428,15 @@ exports[`EdgeLoginScene should render with loading props 1`] = ` { "alignItems": "center", "alignSelf": "center", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", - "marginBottom": 22, - "marginLeft": 22, - "marginRight": 22, - "marginTop": 22, "opacity": 1, + "paddingBottom": 22, + "paddingLeft": 22, + "paddingRight": 22, + "paddingTop": 22, } } > @@ -462,41 +464,22 @@ exports[`EdgeLoginScene should render with loading props 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "center", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], - { - "marginBottom": 22, - "marginLeft": 22, - "marginRight": 22, - "marginTop": 22, + "opacity": 1, }, - undefined, ] } > @@ -561,16 +544,15 @@ exports[`EdgeLoginScene should render with loading props 1`] = ` { "alignItems": "center", "alignSelf": "center", - "borderRadius": 34, - "height": undefined, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", - "marginBottom": 22, - "marginLeft": 22, - "marginRight": 22, - "marginTop": 0, "opacity": 1, - "paddingHorizontal": 0, - "paddingVertical": 0, + "paddingBottom": 22, + "paddingLeft": 22, + "paddingRight": 22, + "paddingTop": 0, } } > @@ -598,47 +580,25 @@ exports[`EdgeLoginScene should render with loading props 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "center", }, { - "height": 67, - "paddingHorizontal": 45, + "alignSelf": "center", }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "height": undefined, + "padding": 0, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - { - "alignSelf": "center", - "height": undefined, - "paddingHorizontal": 0, - "paddingVertical": 0, - }, - ], { - "marginBottom": 22, - "marginLeft": 22, - "marginRight": 22, - "marginTop": 0, + "opacity": 1, }, - undefined, ] } > diff --git a/src/__tests__/scenes/__snapshots__/FioAddressDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioAddressDetailsScene.test.tsx.snap index a63484f16ee..59f27848ede 100644 --- a/src/__tests__/scenes/__snapshots__/FioAddressDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioAddressDetailsScene.test.tsx.snap @@ -15,7 +15,7 @@ exports[`FioAddressDetails should render with loading props 1`] = ` } } > - Remaining bundled transactions: 100 - + diff --git a/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap index 758abf45391..1a5fe093371 100644 --- a/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap @@ -84,7 +84,7 @@ exports[`FioAddressList should render with loading props 1`] = ` onPress={[Function]} /> - - + - Send and Receive with an easy to remember FIO Crypto Handle! - + - + - Similar to website names, your FIO Crypto Handle can be used to send you tokens/coins without ever having to see or send your public key. - + - Your Crypto Handle name here - + - - + - Registered - - + MyFio@edge - + diff --git a/src/__tests__/scenes/__snapshots__/FioAddressRegisteredScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioAddressRegisteredScene.test.tsx.snap index b0391f73e22..e532af2c3e9 100644 --- a/src/__tests__/scenes/__snapshots__/FioAddressRegisteredScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioAddressRegisteredScene.test.tsx.snap @@ -39,17 +39,18 @@ exports[`FioAddressRegistered should render with loading props 1`] = ` } /> - Registered - - + myFio@edge - + diff --git a/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap index d0100cc7b47..ff94e52a3d1 100644 --- a/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap @@ -35,7 +35,7 @@ exports[`FioConnectWalletConfirm should render with loading props 1`] = ` onPress={[Function]} value={false} > - I acknowledge that one address from each wallet will become public. These addresses will remain public even after wallets are disconnected. - + - Note that you need to use FIO to register a Crypto Handle on a custom domain. - - + A FIO Crypto Handle consists of a username and a domain. If you purchase a custom domain, only you will able to register a Crypto Handle on it. Crypto Handle registrations will have to be paid for with FIO tokens from the same wallet which owns the domain. - + - - - + - + - - + `; diff --git a/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap index 4830a9671c9..80083cc67e3 100644 --- a/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap @@ -44,10 +44,10 @@ exports[`Request should render with loaded props 1`] = ` } } > - - Receive - - + 1 undefined - - - + + - + You have 0.000012 undefined - + - - - - + + - + - - + Your Wallet Address - + - Loading… - + - + - - + `; diff --git a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap index 8c96abf3344..868e08eb19f 100644 --- a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap @@ -470,8 +470,9 @@ exports[`SendScene2 1 spendTarget 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -997,8 +998,9 @@ exports[`SendScene2 1 spendTarget 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -1814,8 +1816,9 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -2341,8 +2344,9 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -3346,8 +3350,9 @@ exports[`SendScene2 2 spendTargets 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -4038,8 +4043,9 @@ exports[`SendScene2 2 spendTargets 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -4855,8 +4861,9 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -5248,8 +5255,9 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -6065,8 +6073,9 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -6434,8 +6443,9 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -7251,8 +7261,9 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -7459,8 +7470,9 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -8276,8 +8288,9 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -8789,8 +8802,9 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -9606,8 +9620,9 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -10054,8 +10069,9 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -10871,8 +10887,9 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -11278,8 +11295,9 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -12095,8 +12113,9 @@ exports[`SendScene2 Render SendScene 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, diff --git a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap index 5a715dae2ef..5d105c647c6 100644 --- a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap @@ -228,6 +228,7 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` style={ { "height": 1334, + "maxHeight": 1334, "padding": 0, "width": 750, } @@ -238,8 +239,9 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginBottom": 11, @@ -355,8 +357,9 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -1203,8 +1206,9 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -3327,9 +3331,13 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", + "margin": -11, "opacity": 1, + "padding": 11, } } > @@ -3357,36 +3365,22 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "stretch", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "stretch", - }, - ], - undefined, - undefined, ] } > @@ -3467,9 +3461,13 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", + "margin": -11, "opacity": 1, + "padding": 11, } } > @@ -3497,36 +3495,22 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "stretch", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "stretch", - }, - ], - undefined, - undefined, ] } > @@ -3792,6 +3776,7 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` style={ { "height": 1334, + "maxHeight": 1334, "padding": 0, "width": 750, } @@ -3802,8 +3787,9 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginBottom": 11, @@ -3919,8 +3905,9 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -4767,8 +4754,9 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -6891,9 +6879,13 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", + "margin": -11, "opacity": 1, + "padding": 11, } } > @@ -6921,36 +6913,22 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "stretch", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "stretch", - }, - ], - undefined, - undefined, ] } > @@ -7031,9 +7009,13 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "borderRadius": 34, + "flexBasis": "auto", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", + "margin": -11, "opacity": 1, + "padding": 11, } } > @@ -7061,36 +7043,22 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "stretch", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "stretch", - }, - ], - undefined, - undefined, ] } > diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index 7a6c6460bcd..368db8e11cc 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -183,6 +183,7 @@ exports[`TransactionDetailsScene should render 1`] = ` style={ { "height": 1334, + "maxHeight": 1334, "padding": 11, "width": 750, } @@ -451,8 +452,9 @@ exports[`TransactionDetailsScene should render 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -1069,8 +1071,9 @@ exports[`TransactionDetailsScene should render 1`] = ` style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -1389,6 +1392,10 @@ exports[`TransactionDetailsScene should render 1`] = ` entering={BaseAnimationMock {}} layout={BaseAnimationMock {}} /> + @@ -1767,36 +1780,22 @@ exports[`TransactionDetailsScene should render 1`] = ` [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "center", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], - undefined, - undefined, ] } > @@ -2046,6 +2045,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi style={ { "height": 1334, + "maxHeight": 1334, "padding": 11, "width": 750, } @@ -2314,8 +2314,9 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -2932,8 +2933,9 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi style={ [ { - "flex": 1, "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, }, { "marginVertical": 0, @@ -3252,6 +3254,10 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi entering={BaseAnimationMock {}} layout={BaseAnimationMock {}} /> + @@ -3630,36 +3642,22 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi [ { "alignItems": "center", + "borderRadius": 34, "flexDirection": "row", + "flexGrow": 0, + "flexShrink": 0, "justifyContent": "center", }, { - "alignSelf": "auto", - "opacity": 1, + "alignSelf": "center", }, { "height": 67, - "paddingHorizontal": 45, + "paddingHorizontal": 34, }, { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", + "opacity": 1, }, - [ - { - "alignItems": "center", - "alignSelf": "stretch", - "borderRadius": 34, - "justifyContent": "center", - }, - { - "alignSelf": "center", - }, - ], - undefined, - undefined, ] } > diff --git a/src/actions/AccountReferralActions.ts b/src/actions/AccountReferralActions.ts index 0f1030a5798..9f1da3cde87 100644 --- a/src/actions/AccountReferralActions.ts +++ b/src/actions/AccountReferralActions.ts @@ -84,10 +84,11 @@ function createAccountReferral(): ThunkAction> { accountPlugins: lockStartDates(plugins, creationDate) } - logEvent('Load_Install_Reason_Match', { installerId }) dispatch({ type: 'ACCOUNT_REFERRAL_LOADED', data: { cache, referral } }) await Promise.all([saveAccountReferral(getState()), saveReferralCache(getState())]) + dispatch(logEvent('Load_Install_Reason_Match')) + // Also try activating the same link as a promotion (with silent errors): if (installerId != null) { await activatePromotion(installerId)(dispatch, getState).catch(() => undefined) diff --git a/src/actions/CategoriesActions.ts b/src/actions/CategoriesActions.ts index 5614ec4796d..6d3ff336aa7 100644 --- a/src/actions/CategoriesActions.ts +++ b/src/actions/CategoriesActions.ts @@ -1,5 +1,5 @@ import { eq } from 'biggystring' -import { EdgeAccount, EdgeAssetAmount, EdgeCurrencyWallet, EdgeMetadata, EdgeTransaction } from 'edge-core-js' +import { EdgeAccount, EdgeAssetAction, EdgeAssetAmount, EdgeCurrencyWallet, EdgeMetadata, EdgeTransaction, EdgeTxAction } from 'edge-core-js' import { sprintf } from 'sprintf-js' import { showError } from '../components/services/AirshipInstance' @@ -273,6 +273,8 @@ export interface ActionDisplayInfo { userData: EdgeMetadata savedData: EdgeMetadata mergedData: EdgeMetadata + action?: EdgeTxAction + assetAction?: EdgeAssetAction } export const getTxActionDisplayInfo = (tx: EdgeTransaction, account: EdgeAccount, wallet: EdgeCurrencyWallet): ActionDisplayInfo => { @@ -498,6 +500,8 @@ export const getTxActionDisplayInfo = (tx: EdgeTransaction, account: EdgeAccount } return { + action, + assetAction, direction, iconPluginId, savedData, diff --git a/src/actions/CreateWalletActions.tsx b/src/actions/CreateWalletActions.tsx index 4516e918201..dac4f0b4763 100644 --- a/src/actions/CreateWalletActions.tsx +++ b/src/actions/CreateWalletActions.tsx @@ -1,16 +1,16 @@ import { mul, toFixed } from 'biggystring' -import { EdgeAccount, EdgeCurrencyConfig, EdgeCurrencyWallet, EdgeMetadata, EdgeTransaction, JsonObject } from 'edge-core-js' +import { EdgeAccount, EdgeCreateCurrencyWallet, EdgeCurrencyConfig, EdgeCurrencyWallet, EdgeMetadata, EdgeResult, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { Alert } from 'react-native' import { sprintf } from 'sprintf-js' import { ButtonsModal } from '../components/modals/ButtonsModal' -import { AccountPaymentParams } from '../components/scenes/CreateWalletAccountSelectScene' +import { ActivationPaymentInfo } from '../components/scenes/CreateWalletAccountSelectScene' import { Airship, showError } from '../components/services/AirshipInstance' -import { WalletCreateItem } from '../components/themed/WalletList' -import { getPluginId, SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' +import { SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' import { lstrings } from '../locales/strings' import { getExchangeDenomination } from '../selectors/DenominationSelectors' +import { TokenWalletCreateItem } from '../selectors/getCreateWalletList' import { config } from '../theme/appConfig' import { ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' @@ -18,52 +18,36 @@ import { EdgeAsset } from '../types/types' import { getWalletTokenId } from '../util/CurrencyInfoHelpers' import { logActivity } from '../util/logger' import { filterNull } from '../util/safeFilters' -import { logEvent, TrackingEventName } from '../util/tracking' +import { logEvent } from '../util/tracking' -export interface CreateWalletOptions { - walletType: string - fiatCurrencyCode?: string - importText?: string // for creating wallet from private seed / key - trackingEventFailed?: TrackingEventName - trackingEventSuccess?: TrackingEventName - walletName?: string - keyOptions?: JsonObject -} +export const createWallets = async (account: EdgeAccount, items: EdgeCreateCurrencyWallet[]): Promise>> => { + const out = await account.createCurrencyWallets(items) -export const createWallet = async (account: EdgeAccount, { walletType, walletName, fiatCurrencyCode, importText, keyOptions = {} }: CreateWalletOptions) => { - // Try and get the new format param from the legacy walletType if it's mentioned - const [type, format] = walletType.split('-') - const opts = { - name: walletName, - fiatCurrencyCode, - keyOptions: format != null ? { ...keyOptions, format } : { ...keyOptions }, - importText + // Log the results: + for (let i = 0; i < items.length; ++i) { + if (!out[i].ok) continue + const { fiatCurrencyCode, name = '', walletType } = items[i] + logActivity(`Create Wallet: ${account.username} -- ${walletType} -- ${fiatCurrencyCode ?? ''} -- ${name}`) } - const out = await account.createCurrencyWallet(type, opts) - logActivity(`Create Wallet: ${account.username} -- ${walletType} -- ${fiatCurrencyCode ?? ''} -- ${opts.name ?? ''}`) + return out } -export function createCurrencyWallet( - walletName: string, - walletType: string, - fiatCurrencyCode?: string, - importText?: string -): ThunkAction> { - return async (dispatch, getState) => { - const state = getState() - fiatCurrencyCode = fiatCurrencyCode ?? state.ui.settings.defaultIsoFiat - return await createWallet(state.core.account, { walletName, walletType, fiatCurrencyCode, importText }) - } +export const createWallet = async (account: EdgeAccount, opts: EdgeCreateCurrencyWallet): Promise => { + const { walletType, name, fiatCurrencyCode } = opts + const out = await account.createCurrencyWallet(walletType, opts) + + logActivity(`Create Wallet: ${account.username} -- ${walletType} -- ${fiatCurrencyCode ?? ''} -- ${name ?? ''}`) + + return out } // can move to component in the future, just account and currencyConfig, etc to component through connector -export function fetchAccountActivationInfo(walletType: string): ThunkAction> { +export function fetchAccountActivationInfo(pluginId: string): ThunkAction> { return async (dispatch, getState) => { const state = getState() const { account } = state.core - const currencyPluginName = getPluginId(walletType) - const currencyPlugin: EdgeCurrencyConfig = account.currencyConfig[currencyPluginName] + const currencyPlugin: EdgeCurrencyConfig = account.currencyConfig[pluginId] try { const [supportedCurrencies, activationCost] = await Promise.all([ currencyPlugin.otherMethods.getActivationSupportedCurrencies(), @@ -109,7 +93,7 @@ export function fetchAccountActivationInfo(walletType: string): ThunkAction { +export function fetchWalletAccountActivationPaymentInfo(paymentParams: ActivationPaymentInfo, createdCoreWallet: EdgeCurrencyWallet): ThunkAction { return (dispatch, getState) => { try { const networkTimeout = setTimeout(() => { @@ -181,9 +165,11 @@ export function createAccountTransaction( walletId: paymentWalletId, onBack: () => { // Hack. Keyboard pops up for some reason. Close it - logEvent('Activate_Wallet_Cancel', { - currencyCode: createdWalletCurrencyCode - }) + dispatch( + logEvent('Activate_Wallet_Cancel', { + currencyCode: createdWalletCurrencyCode + }) + ) }, onDone: (error: Error | null, edgeTransaction?: EdgeTransaction) => { if (error) { @@ -192,9 +178,11 @@ export function createAccountTransaction( Alert.alert(lstrings.create_wallet_account_error_sending_transaction) }, 750) } else if (edgeTransaction) { - logEvent('Activate_Wallet_Done', { - currencyCode: createdWalletCurrencyCode - }) + dispatch( + logEvent('Activate_Wallet_Done', { + currencyCode: createdWalletCurrencyCode + }) + ) const edgeMetadata: EdgeMetadata = { name: sprintf(lstrings.create_wallet_account_metadata_name, createdWalletCurrencyCode), category: 'Expense:' + sprintf(lstrings.create_wallet_account_metadata_category, createdWalletCurrencyCode), @@ -239,27 +227,6 @@ export function createHandleUnavailableModal(navigation: NavigationBase, newWall } export const PLACEHOLDER_WALLET_ID = 'NEW_WALLET_UNIQUE_STRING' -export interface MainWalletCreateItem extends WalletCreateItem { - walletType: string -} -interface TokenWalletCreateItem extends WalletCreateItem { - tokenId: string - createWalletIds: string[] -} - -export const splitCreateWalletItems = (createItems: WalletCreateItem[]): { newWalletItems: MainWalletCreateItem[]; newTokenItems: TokenWalletCreateItem[] } => { - const newWalletItems: MainWalletCreateItem[] = [] - const newTokenItems: TokenWalletCreateItem[] = [] - createItems.forEach(item => { - if (item.walletType != null) { - newWalletItems.push(item as MainWalletCreateItem) - } else if (item.tokenId != null) { - if (item.createWalletIds == null) item.createWalletIds = [] - newTokenItems.push(item as TokenWalletCreateItem) - } - }) - return { newWalletItems, newTokenItems } -} export function enableTokensAcrossWallets(newTokenItems: TokenWalletCreateItem[]): ThunkAction> { return async (dispatch, getState) => { diff --git a/src/actions/CryptoExchangeActions.tsx b/src/actions/CryptoExchangeActions.tsx index 3c76bc48347..6dd6db47a46 100644 --- a/src/actions/CryptoExchangeActions.tsx +++ b/src/actions/CryptoExchangeActions.tsx @@ -14,7 +14,6 @@ import * as React from 'react' import { Alert } from 'react-native' import { sprintf } from 'sprintf-js' -import { trackConversion } from '../actions/TrackingActions' import { InsufficientFeesModal } from '../components/modals/InsufficientFeesModal' import { Airship, showError } from '../components/services/AirshipInstance' import { formatNumber } from '../locales/intl' @@ -308,7 +307,7 @@ export function shiftCryptoCurrency(navigation: NavigationBase, quote: EdgeSwapQ const { toWallet, toTokenId } = request const toCurrencyCode = getCurrencyCode(toWallet, toTokenId) try { - logEvent('Exchange_Shift_Start') + dispatch(logEvent('Exchange_Shift_Start')) const result: EdgeSwapResult = await quote.approve() logActivity(`Swap Exchange Executed: ${account.username}`) @@ -340,16 +339,16 @@ export function shiftCryptoCurrency(navigation: NavigationBase, quote: EdgeSwapQ const exchangeAmount = await toWallet.nativeToDenomination(toNativeAmount, toCurrencyCode) dispatch( - trackConversion('Exchange_Shift_Success', { + logEvent('Exchange_Shift_Success', { pluginId, currencyCode: toCurrencyCode, - exchangeAmount: Number(exchangeAmount), + exchangeAmount, orderId: result.orderId }) ) } catch (error: any) { console.log(error) - logEvent('Exchange_Shift_Failed', { error: String(error) }) // TODO: Do we need to parse/clean all cases? + dispatch(logEvent('Exchange_Shift_Failed', { error: String(error) })) // TODO: Do we need to parse/clean all cases? dispatch({ type: 'DONE_SHIFT_TRANSACTION' }) setTimeout(() => { showError(`${lstrings.exchange_failed}. ${error.message}`) diff --git a/src/actions/DeepLinkingActions.ts b/src/actions/DeepLinkingActions.ts index 792ae666a83..3cbaab3b0ee 100644 --- a/src/actions/DeepLinkingActions.ts +++ b/src/actions/DeepLinkingActions.ts @@ -10,6 +10,7 @@ import { DeepLink } from '../types/DeepLinkTypes' import { Dispatch, RootState, ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' import { EdgeAsset } from '../types/types' +import { logEvent } from '../util/tracking' import { base58ToUuid } from '../util/utils' import { activatePromotion } from './AccountReferralActions' import { launchPaymentProto } from './PaymentProtoActions' @@ -62,7 +63,6 @@ export function retryPendingDeepLink(navigation: NavigationBase): ThunkAction { const { account, disklet } = state.core - const { accountReferral } = state.account const { activeWalletIds, currencyWallets } = account const deviceId = base58ToUuid(state.core.context.clientId) @@ -120,7 +120,6 @@ export async function handleLink(navigation: NavigationBase, dispatch: Dispatch, await executePlugin({ account, - accountReferral, deviceId, disablePlugins: disableProviders, disklet, @@ -129,7 +128,8 @@ export async function handleLink(navigation: NavigationBase, dispatch: Dispatch, regionCode: { countryCode: state.ui.settings.countryCode }, paymentType, providerId, - navigation + navigation, + onLogEvent: (event, values) => dispatch(logEvent(event, values)) }) return true } diff --git a/src/actions/DeviceReferralActions.ts b/src/actions/DeviceReferralActions.ts index fbe89557212..7334e0114da 100644 --- a/src/actions/DeviceReferralActions.ts +++ b/src/actions/DeviceReferralActions.ts @@ -41,7 +41,7 @@ export function loadDeviceReferral(): ThunkAction> { } catch (error: any) { // If all else fails, we just don't have a reason: console.log('Failed to load install reason', error) - logEvent('Load_Install_Reason_Fail', { error: String(error) }) + dispatch(logEvent('Load_Install_Reason_Fail', { error: String(error) })) } } } diff --git a/src/actions/DeviceSettingsActions.ts b/src/actions/DeviceSettingsActions.ts index a50878077ad..b58b6a64ada 100644 --- a/src/actions/DeviceSettingsActions.ts +++ b/src/actions/DeviceSettingsActions.ts @@ -11,6 +11,18 @@ export const initDeviceSettings = async () => { deviceSettings = await readDeviceSettings() } +export const writeDeveloperPluginUri = async (developerPluginUri: string) => { + try { + const raw = await disklet.getText(DEVICE_SETTINGS_FILENAME) + const json = JSON.parse(raw) + deviceSettings = asDeviceSettings(json) + } catch (e) { + console.log(e) + } + const updatedSettings = { ...deviceSettings, developerPluginUri } + return await writeDeviceSettings(updatedSettings) +} + export const writeDisableAnimations = async (disableAnimations: boolean) => { try { const raw = await disklet.getText(DEVICE_SETTINGS_FILENAME) diff --git a/src/actions/FioAddressActions.ts b/src/actions/FioAddressActions.ts index bc3c4de3284..52ea472e606 100644 --- a/src/actions/FioAddressActions.ts +++ b/src/actions/FioAddressActions.ts @@ -4,13 +4,18 @@ import { FIO_WALLET_TYPE } from '../constants/WalletAndCurrencyConstants' import { lstrings } from '../locales/strings' import { ThunkAction } from '../types/reduxTypes' import { refreshConnectedWalletsForFioAddress, refreshFioNames } from '../util/FioAddressUtils' -import { createCurrencyWallet } from './CreateWalletActions' +import { createWallet } from './CreateWalletActions' export function createFioWallet(): ThunkAction> { return async (dispatch, getState) => { const state = getState() const fiatCurrencyCode = state.ui.settings.defaultIsoFiat - return await dispatch(createCurrencyWallet(lstrings.fio_address_register_default_fio_wallet_name, FIO_WALLET_TYPE, fiatCurrencyCode)) + + return await createWallet(state.core.account, { + name: lstrings.fio_address_register_default_fio_wallet_name, + walletType: FIO_WALLET_TYPE, + fiatCurrencyCode + }) } } diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index c362b185d7c..a0b2e577316 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -1,4 +1,4 @@ -import { EdgeAccount } from 'edge-core-js/types' +import { EdgeAccount, EdgeCreateCurrencyWallet } from 'edge-core-js/types' import { hasSecurityAlerts } from 'edge-login-ui-rn' import * as React from 'react' import { Keyboard } from 'react-native' @@ -9,14 +9,16 @@ import { readSyncedSettings } from '../actions/SettingsActions' import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal' import { FioCreateHandleModal } from '../components/modals/FioCreateHandleModal' import { Airship, showError } from '../components/services/AirshipInstance' -import { WalletCreateItem } from '../components/themed/WalletList' import { ENV } from '../env' +import { getExperimentConfig } from '../experimentConfig' import { lstrings } from '../locales/strings' import { AccountInitPayload, initialState } from '../reducers/scenes/SettingsReducer' +import { WalletCreateItem } from '../selectors/getCreateWalletList' import { config } from '../theme/appConfig' import { Dispatch, ThunkAction } from '../types/reduxTypes' import { NavigationBase, NavigationProp } from '../types/routerTypes' -import { EdgeAsset, GuiTouchIdInfo } from '../types/types' +import { GuiTouchIdInfo } from '../types/types' +import { currencyCodesToEdgeAssets } from '../util/CurrencyInfoHelpers' import { logActivity } from '../util/logger' import { logEvent } from '../util/tracking' import { runWithTimeout } from '../util/utils' @@ -25,7 +27,6 @@ import { getUniqueWalletName } from './CreateWalletActions' import { expiredFioNamesCheckDates } from './FioActions' import { readLocalSettings } from './LocalSettingsActions' import { registerNotificationsV2 } from './NotificationActions' -import { trackAccountEvent } from './TrackingActions' function getFirstActiveWalletInfo(account: EdgeAccount): { walletId: string; currencyCode: string } { // Find the first wallet: @@ -62,8 +63,8 @@ export function initializeAccount(navigation: NavigationBase, account: EdgeAccou defaultFiat = phoneCurrency } // Ensure the creation reason is available before creating wallets: - const currencyCodes = getState().account.accountReferral.currencyCodes ?? config.defaultWallets - const defaultSelection = currencyCodesToEdgeTokenIds(account, currencyCodes) + const accountReferralCurrencyCodes = getState().account.accountReferral.currencyCodes + const defaultSelection = accountReferralCurrencyCodes != null ? currencyCodesToEdgeAssets(account, accountReferralCurrencyCodes) : config.defaultWallets const fiatCurrencyCode = 'iso:' + defaultFiat const newAccountFlow = async (navigation: NavigationProp<'createWalletSelectCrypto'>, items: WalletCreateItem[]) => { @@ -73,12 +74,10 @@ export function initializeAccount(navigation: NavigationBase, account: EdgeAccou screen: 'home' } }) - const selectedEdgetokenIds = items.map(item => ({ pluginId: item.pluginId, tokenId: item.tokenId })) + const createWalletsPromise = createCustomWallets(account, fiatCurrencyCode, items, dispatch).catch(error => showError(error)) // New user FIO handle registration flow (if env is properly configured) const { freeRegApiToken = '', freeRegRefCode = '' } = typeof ENV.FIO_INIT === 'object' ? ENV.FIO_INIT : {} - const createWalletsPromise = createCustomWallets(account, fiatCurrencyCode, selectedEdgetokenIds, dispatch) - if (freeRegApiToken !== '' && freeRegRefCode !== '') { const isCreateHandle = await Airship.show(bridge => ) if (isCreateHandle) { @@ -87,6 +86,7 @@ export function initializeAccount(navigation: NavigationBase, account: EdgeAccou } await createWalletsPromise + dispatch(logEvent('Signup_Complete')) } navigation.navigate('edgeApp', { @@ -220,105 +220,52 @@ export function logoutRequest(navigation: NavigationBase, nextLoginId?: string): Airship.clear() dispatch({ type: 'LOGOUT', data: { nextLoginId } }) if (typeof account.logout === 'function') await account.logout() - navigation.navigate('login', {}) + navigation.navigate('login', { experimentConfig: await getExperimentConfig() }) } } /** - * Creates a wallet, with timeout, and maybe also activates it. + * Creates wallets inside a new account. */ -async function safeCreateWallet(account: EdgeAccount, walletType: string, walletName: string, fiatCurrencyCode: string, dispatch: Dispatch) { - try { - const wallet = await runWithTimeout( - account.createCurrencyWallet(walletType, { - name: walletName, - fiatCurrencyCode - }), - 20000, - new Error(lstrings.error_creating_wallets) - ) - if (account.activeWalletIds.length <= 1) { - dispatch({ - type: 'UI/WALLETS/SELECT_WALLET', - data: { currencyCode: wallet.currencyInfo.currencyCode, walletId: wallet.id } - }) - } - dispatch(trackAccountEvent('Signup_Wallets_Created_Success')) - logActivity(`Create Wallet (login): ${account.username} -- ${walletType} -- ${fiatCurrencyCode ?? ''} -- ${walletName}`) - - return wallet - } catch (error) { - showError(error) - dispatch(trackAccountEvent('Signup_Wallets_Created_Failed', { error })) - throw error - } -} - -// The `currencyCodes` are in the format "ETH:DAI", -const currencyCodesToEdgeTokenIds = (account: EdgeAccount, currencyCodes: string[]): EdgeAsset[] => { - const chainCodePluginIdMap = Object.keys(account.currencyConfig).reduce( - (map: { [chainCode: string]: string }, pluginId) => { - const chainCode = account.currencyConfig[pluginId].currencyInfo.currencyCode - if (map[chainCode] == null) map[chainCode] = pluginId - return map - }, - { BNB: 'binancesmartchain' } // HACK: Prefer BNB Smart Chain over Beacon Chain if provided a BNB currency code) - ) - - const edgeTokenIds: EdgeAsset[] = [] - - for (const code of currencyCodes) { - const [parent, child] = code.split(':') - const pluginId = chainCodePluginIdMap[parent] - const currencyConfig = account.currencyConfig[pluginId] - if (currencyConfig == null) continue - - // Add the mainnet EdgeAsset if we haven't yet - if (edgeTokenIds.find(edgeTokenId => edgeTokenId.tokenId == null && edgeTokenId.pluginId === pluginId) == null) { - edgeTokenIds.push({ pluginId, tokenId: null }) +async function createCustomWallets(account: EdgeAccount, fiatCurrencyCode: string, items: WalletCreateItem[], dispatch: Dispatch): Promise { + // Maps pluginId's to core options: + const optionsMap = new Map() + for (const item of items) { + const { keyOptions, pluginId, tokenId } = item + + // Ensure we create the wallet: + let row = optionsMap.get(pluginId) + if (row == null) { + const { walletType } = account.currencyConfig[pluginId].currencyInfo + row = { + fiatCurrencyCode, + keyOptions, + name: getUniqueWalletName(account, pluginId), + walletType + } + optionsMap.set(pluginId, row) } - // Add tokens - if (child != null) { - const tokenId = Object.keys(currencyConfig.builtinTokens).find(tokenId => currencyConfig.builtinTokens[tokenId].currencyCode === child) - if (tokenId != null) edgeTokenIds.push({ pluginId, tokenId }) + // If this is a token, add it: + if (tokenId != null) { + row.enabledTokenIds ??= [] + row.enabledTokenIds.push(tokenId) } } - return edgeTokenIds -} - -/** - * Creates wallets inside a new account. - */ -async function createCustomWallets(account: EdgeAccount, fiatCurrencyCode: string, edgeTokenIds: EdgeAsset[], dispatch: Dispatch) { - if (edgeTokenIds.length === 0) return await createDefaultWallets(account, fiatCurrencyCode, dispatch) - - const pluginIdTokenIdMap: { [pluginId: string]: string[] } = {} - - for (const edgeTokenId of edgeTokenIds) { - const { pluginId, tokenId } = edgeTokenId - if (pluginIdTokenIdMap[pluginId] == null) pluginIdTokenIdMap[pluginId] = [] - if (tokenId != null) pluginIdTokenIdMap[pluginId].push(tokenId) - } - - for (const pluginId of Object.keys(pluginIdTokenIdMap)) { - const currencyConfig = account.currencyConfig[pluginId] - if (currencyConfig == null) continue + // Actually create the wallets: + const options = [...optionsMap.values()] + const results = await runWithTimeout(account.createCurrencyWallets(options), 20000, new Error(lstrings.error_creating_wallets)).catch(error => { + dispatch(logEvent('Signup_Wallets_Created_Failed', { error })) + throw error + }) - const walletName = getUniqueWalletName(account, pluginId) - const wallet = await safeCreateWallet(account, currencyConfig.currencyInfo.walletType, walletName, fiatCurrencyCode, dispatch) - if (pluginIdTokenIdMap[pluginId].length > 0) await wallet.changeEnabledTokenIds(pluginIdTokenIdMap[pluginId]) + for (let i = 0; i < results.length; ++i) { + const result = results[i] + if (!result.ok) continue + const { walletType, name } = options[i] + logActivity(`Create Wallet (login): ${account.username} -- ${walletType} -- ${fiatCurrencyCode ?? ''} -- ${name}`) } - logEvent('Signup_Complete') -} - -/** - * Creates the default wallets inside a new account. - */ -async function createDefaultWallets(account: EdgeAccount, fiatCurrencyCode: string, dispatch: Dispatch) { - const defaultEdgeTokenIds = currencyCodesToEdgeTokenIds(account, config.defaultWallets) - // TODO: Run these in parallel once the Core has safer locking: - await createCustomWallets(account, fiatCurrencyCode, defaultEdgeTokenIds, dispatch) + dispatch(logEvent('Signup_Wallets_Created_Success')) } diff --git a/src/actions/PluginActions.tsx b/src/actions/PluginActions.tsx index 504963897a1..b1cfe2ed5e4 100644 --- a/src/actions/PluginActions.tsx +++ b/src/actions/PluginActions.tsx @@ -2,24 +2,24 @@ import { guiPlugins } from '../constants/plugins/GuiPlugins' import { executePlugin } from '../plugins/gui/fiatPlugin' import { ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' +import { logEvent } from '../util/tracking' import { base58ToUuid } from '../util/utils' export function executePluginAction(navigation: NavigationBase, pluginId: string, direction: 'buy' | 'sell'): ThunkAction> { return async (dispatch, getState) => { const state = getState() const { account, context, disklet } = state.core - const { accountReferral } = state.account const deviceId = base58ToUuid(context.clientId) await executePlugin({ account, - accountReferral, deviceId, direction: 'sell', disklet, guiPlugin: guiPlugins[pluginId], navigation, - regionCode: { countryCode: 'US' } + regionCode: { countryCode: 'US' }, + onLogEvent: (event, values) => dispatch(logEvent(event, values)) }) } } diff --git a/src/actions/ScanActions.tsx b/src/actions/ScanActions.tsx index e6ffab9a08b..29b83841faf 100644 --- a/src/actions/ScanActions.tsx +++ b/src/actions/ScanActions.tsx @@ -307,13 +307,9 @@ async function sweepPrivateKeys(wallet: EdgeCurrencyWallet, privateKeys: string[ const shownWalletGetCryptoModals: string[] = [] -export function checkAndShowGetCryptoModal(navigation: NavigationBase, selectedWalletId?: string, tokenId?: EdgeTokenId): ThunkAction> { - return async (dispatch, getState) => { +export function checkAndShowGetCryptoModal(navigation: NavigationBase, wallet: EdgeCurrencyWallet, tokenId: EdgeTokenId): ThunkAction> { + return async dispatch => { try { - const state = getState() - const { currencyWallets } = state.core.account - const wallet: EdgeCurrencyWallet = currencyWallets[selectedWalletId ?? state.ui.wallets.selectedWalletId] - tokenId = tokenId === undefined ? getWalletTokenId(wallet, state.ui.wallets.selectedCurrencyCode) : tokenId const currencyCode = getCurrencyCode(wallet, tokenId) // check if balance is zero const balance = wallet.balanceMap.get(tokenId) diff --git a/src/actions/TrackingActions.ts b/src/actions/TrackingActions.ts deleted file mode 100644 index 055805e322c..00000000000 --- a/src/actions/TrackingActions.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { convertCurrency } from '../selectors/WalletSelectors' -import { ThunkAction } from '../types/reduxTypes' -import { AccountReferral } from '../types/ReferralTypes' -import { getHistoricalRate } from '../util/exchangeRates' -import { logEvent, TrackingEventName, TrackingValues } from '../util/tracking' - -/** - * Tracks a conversion, which involves some of revenue. - */ -export function trackConversion( - event: TrackingEventName, - opts: { - currencyCode: string - exchangeAmount: number - pluginId: string - orderId?: string - } -): ThunkAction { - return (dispatch, getState) => { - const state = getState() - const { currencyCode, exchangeAmount, pluginId, orderId } = opts - - // Look up the dollar value: - const dollarValue: number = parseFloat(convertCurrency(state, currencyCode, 'iso:USD', String(exchangeAmount))) - - // Record the event: - const { accountReferral } = state.account - return logEvent(event, { - dollarValue, - pluginId, - orderId, - ...makeTrackingValues(accountReferral) - }) - } -} - -export async function trackConversionWithReferral( - event: TrackingEventName, - opts: { - destCurrencyCode: string - destExchangeAmount: string - destPluginId?: string - sourceCurrencyCode: string - sourceExchangeAmount: string - sourcePluginId?: string - orderId?: string - pluginId: string - }, - accountReferral: AccountReferral -): Promise { - const { destCurrencyCode, destExchangeAmount, destPluginId, pluginId, sourceCurrencyCode, sourceExchangeAmount, sourcePluginId, orderId } = opts - - // Look up the dollar value: - const rate = await getHistoricalRate(`${destCurrencyCode}_iso:USD`, new Date().toISOString()) - const dollarValue = Number(destExchangeAmount) * rate - - // Record the event: - logEvent(event, { - dollarValue, - pluginId, - orderId, - destCurrencyCode, - destExchangeAmount, - destPluginId, - sourceCurrencyCode, - sourceExchangeAmount, - sourcePluginId, - ...makeTrackingValues(accountReferral) - }) -} - -/** - * Tracks an event tied to a particular account's affiliate information, - * such as creating the initial wallets. - */ -export function trackAccountEvent(event: TrackingEventName, trackingValues: TrackingValues = {}): ThunkAction { - return (dispatch, getState) => { - const state = getState() - - // Record the event: - const { accountReferral } = state.account - logEvent(event, { - ...trackingValues, - ...makeTrackingValues(accountReferral) - }) - } -} - -/** - * Turn account affiliate information into clean tracking values. - * Obfuscates the creation date so the server can't guess account identities. - */ -function makeTrackingValues(accountReferral: AccountReferral): TrackingValues { - const { creationDate, installerId } = accountReferral - if (installerId == null || creationDate == null) return {} - return { - accountDate: creationDate.toISOString().replace(/-\d\dT.*/, ''), - installerId - } -} diff --git a/src/actions/WalletActions.tsx b/src/actions/WalletActions.tsx index e444d1c3011..346c94d605c 100644 --- a/src/actions/WalletActions.tsx +++ b/src/actions/WalletActions.tsx @@ -11,13 +11,13 @@ import { Airship, showError, showToast } from '../components/services/AirshipIns import { FIO_WALLET_TYPE, getSpecialCurrencyInfo, SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' import { lstrings } from '../locales/strings' import { getDisplayDenomination } from '../selectors/DenominationSelectors' -import { convertCurrencyFromExchangeRates } from '../selectors/WalletSelectors' import { Dispatch, RootState, ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' import { MapObject } from '../types/types' -import { getCurrencyCode, getCurrencyInfos, getToken, isKeysOnlyPlugin, makeCreateWalletType } from '../util/CurrencyInfoHelpers' +import { getCurrencyCode, getToken, isKeysOnlyPlugin } from '../util/CurrencyInfoHelpers' import { getWalletName } from '../util/CurrencyWalletHelpers' import { fetchInfo } from '../util/network' +import { convertCurrencyFromExchangeRates } from '../util/utils' import { refreshConnectedWallets } from './FioActions' export interface SelectWalletTokenParams { @@ -90,11 +90,7 @@ function selectEOSWallet(navigation: NavigationBase, walletId: string, currencyC return async (dispatch, getState) => { const state = getState() const wallet = state.core.account.currencyWallets[walletId] - const { - name, - currencyInfo: { currencyCode, pluginId } - } = wallet - const walletName = name ?? '' + const { currencyCode, pluginId } = wallet.currencyInfo const { publicAddress } = await wallet.getReceiveAddress({ tokenId: null }) if (publicAddress !== '') { @@ -110,25 +106,18 @@ function selectEOSWallet(navigation: NavigationBase, walletId: string, currencyC await dispatch(updateWalletsRequest()) // not activated yet // find fiat and crypto (EOSIO) types and populate scene props - const currencyInfos = getCurrencyInfos(state.core.account) - const currencyInfo = currencyInfos.find(info => info.currencyCode === currencyCode) - if (!currencyInfo) throw new Error('CannotFindCurrencyInfo') - const selectedWalletType = makeCreateWalletType(currencyInfo) const specialCurrencyInfo = getSpecialCurrencyInfo(pluginId) if (specialCurrencyInfo.skipAccountNameValidation) { navigation.push('createWalletAccountSelect', { - selectedWalletType, - accountName: walletName, - existingWalletId: walletId + accountName: getWalletName(wallet), + walletId }) } else { - const createWalletAccountSetupSceneProps = { + navigation.push('createWalletAccountSetup', { accountHandle: '', - selectedWalletType, isReactivation: true, - existingWalletId: walletId - } - navigation.push('createWalletAccountSetup', createWalletAccountSetupSceneProps) + walletId + }) } Airship.show<'ok' | undefined>(bridge => ( diff --git a/src/actions/WalletListActions.tsx b/src/actions/WalletListActions.tsx index fe48e0184ed..0680fe1c5dc 100644 --- a/src/actions/WalletListActions.tsx +++ b/src/actions/WalletListActions.tsx @@ -10,9 +10,9 @@ import { Airship, showError } from '../components/services/AirshipInstance' import { lstrings } from '../locales/strings' import { GetState, ThunkAction } from '../types/reduxTypes' import { NavigationBase } from '../types/routerTypes' -import { getCreateWalletType } from '../util/CurrencyInfoHelpers' import { parseDeepLink } from '../util/DeepLinkParser' import { logActivity } from '../util/logger' +import { getUniqueWalletName } from './CreateWalletActions' import { launchDeepLink } from './DeepLinkingActions' export function updateWalletsSort(walletsSort: SortOption): ThunkAction { @@ -36,7 +36,7 @@ export function linkReferralWithCurrencies(navigation: NavigationBase, uri: stri for (const match of currencyCodeMatches) { const currencyCode = match.toUpperCase().replace(/%/g, '') const address = await getFirstCurrencyAddress(currencyCode, getState) - if (address == null) continue + if (address == null) return uri = uri.replace(match, address) } } @@ -47,43 +47,20 @@ export function linkReferralWithCurrencies(navigation: NavigationBase, uri: stri } } -const getFirstCurrencyAddress = async (currencyCode: string, getState: GetState) => { - // Wallet Check +const getFirstCurrencyAddress = async (currencyCode: string, getState: GetState): Promise => { const state = getState() const { account } = state.core - const edgeWallets = state.core.account.currencyWallets - const walletIds = Object.keys(edgeWallets) - const walletId = walletIds.find(id => { - const edgeWallet = edgeWallets[id] - const walletCurrency = edgeWallet.currencyInfo.currencyCode.toUpperCase() - return walletCurrency === currencyCode - }) - if (walletId) { - const wallet = edgeWallets[walletId] - return (await wallet.getReceiveAddress({ tokenId: null })).publicAddress - } - - // Wallet Creation - const { defaultIsoFiat } = state.ui.settings - - const createWalletTypes = getCreateWalletType(account, currencyCode) - if (!createWalletTypes) throw new Error(lstrings.wallet_list_referral_link_currency_invalid) + const { currencyWallets, currencyConfig } = account - const askUserToCreateWallet = await createWalletCheckModal(currencyCode) - if (!askUserToCreateWallet) throw new Error(lstrings.wallet_list_referral_link_cancelled_wallet_creation) - - const createWallet = account.createCurrencyWallet(createWalletTypes.walletType, { - name: createWalletTypes.currencyName, - fiatCurrencyCode: defaultIsoFiat - }) - const wallet = await showFullScreenSpinner(lstrings.wallet_list_referral_link_currency_loading, createWallet) - logActivity(`Create Wallet (wallet list): ${account.username} -- ${createWalletTypes.walletType} -- ${defaultIsoFiat ?? ''}`) - - const receiveAddress = await wallet.getReceiveAddress({ tokenId: null }) - return receiveAddress.publicAddress -} + // If we have a wallet, use that: + const walletId = Object.keys(currencyWallets).find(walletId => currencyWallets[walletId].currencyInfo.currencyCode === currencyCode) + if (walletId != null) { + const wallet = currencyWallets[walletId] + const address = await wallet.getReceiveAddress({ tokenId: null }) + return address.publicAddress + } -const createWalletCheckModal = async (currencyCode: string): Promise => { + // Ask the user if they want a wallet: const result = await Airship.show<'ok' | 'cancel' | undefined>(bridge => ( => }} /> )) - return result === 'ok' + if (result !== 'ok') return + + // Create the wallet: + const pluginId = Object.keys(currencyConfig).find(pluginId => currencyConfig[pluginId].currencyInfo.currencyCode === currencyCode) + if (pluginId == null) { + throw new Error(lstrings.wallet_list_referral_link_currency_invalid) + } + + const { walletType } = currencyConfig[pluginId].currencyInfo + const { defaultIsoFiat } = state.ui.settings + + const walletPromise = account.createCurrencyWallet(walletType, { + fiatCurrencyCode: defaultIsoFiat, + name: getUniqueWalletName(account, pluginId) + }) + const wallet = await showFullScreenSpinner(lstrings.wallet_list_referral_link_currency_loading, walletPromise) + logActivity(`Create Wallet (wallet list): ${account.username} -- ${walletType} -- ${defaultIsoFiat ?? ''}`) + + const address = await wallet.getReceiveAddress({ tokenId: null }) + return address.publicAddress } diff --git a/src/app.ts b/src/app.ts index 00221cb3ead..5f383515e35 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,14 @@ /* global __DEV__ */ +/** + * Uncomment next line to get perfomance logging of component + * rerenders + */ +// import './wdyr' + import Bugsnag from '@bugsnag/react-native' import { asObject, asString } from 'cleaners' -import { Text, TextInput } from 'react-native' +import { LogBox, Text, TextInput } from 'react-native' import RNFS from 'react-native-fs' import { initDeviceSettings } from './actions/DeviceSettingsActions' @@ -10,6 +16,9 @@ import { changeTheme, getTheme } from './components/services/ThemeContext' import { ENV } from './env' import { NumberMap } from './types/types' import { log, logToServer } from './util/logger' +import { initInfoServer } from './util/network' + +LogBox.ignoreLogs(['Require cycle:']) Bugsnag.start({ onError: event => { @@ -216,3 +225,4 @@ if (ENV.DEBUG_THEME) { } initDeviceSettings().catch(err => console.log(err)) +initInfoServer().catch(err => console.log(err)) diff --git a/src/components/FioAddress/DomainListModal.tsx b/src/components/FioAddress/DomainListModal.tsx index 2f40ba38eb0..3f23eb30027 100644 --- a/src/components/FioAddress/DomainListModal.tsx +++ b/src/components/FioAddress/DomainListModal.tsx @@ -168,7 +168,7 @@ class DomainListModalComponent extends React.Component { /> { const { addressTitles } = this.props const { paymentWallet } = this.state if (paymentWallet) { - const availbleBalance = getAvailableBalance(paymentWallet) + const availbleBalance = getAvailableBalance(paymentWallet, null) this.setState({ balance: this.formatFio(availbleBalance) }) } else { showError(addressTitles ? lstrings.fio_wallet_missing_for_fio_address : lstrings.fio_wallet_missing_for_fio_domain) diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 0cae6c14ad7..fa8aa884513 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -13,7 +13,7 @@ import { showReEnableOtpModal } from '../actions/SettingsActions' import { CryptoExchangeScene as CryptoExchangeSceneComponent } from '../components/scenes/CryptoExchangeScene' import { HomeSceneUi4 as HomeSceneUi4Component } from '../components/ui4/scenes/HomeSceneUi4' import { ENV } from '../env' -import { getExperimentConfigValue } from '../experimentConfig' +import { DEFAULT_EXPERIMENT_CONFIG, ExperimentConfig, getExperimentConfig } from '../experimentConfig' import { useAsyncEffect } from '../hooks/useAsyncEffect' import { useMount } from '../hooks/useMount' import { lstrings } from '../locales/strings' @@ -231,7 +231,11 @@ const firstSceneScreenOptions: StackNavigationOptions = { export const Main = () => { const theme = useTheme() - const [legacyLanding, setLegacyLanding] = React.useState(isMaestro() ? false : undefined) + const dispatch = useDispatch() + + // TODO: Create a new provider instead to serve the experimentConfig globally + const [experimentConfig, setExperimentConfig] = React.useState(isMaestro() ? DEFAULT_EXPERIMENT_CONFIG : undefined) + const [hasInitialScenesLoaded, setHasInitialScenesLoaded] = React.useState(false) // Match react navigation theme background with the patina theme @@ -248,11 +252,11 @@ export const Main = () => { const localUsers = useSelector(state => state.core.context.localUsers) useMount(() => { - logEvent('Start_App', { numAccounts: localUsers.length }) + dispatch(logEvent('Start_App', { numAccounts: localUsers.length })) if (localUsers.length === 0) { - logEvent('Start_App_No_Accounts') + dispatch(logEvent('Start_App_No_Accounts')) } else { - logEvent('Start_App_With_Accounts') + dispatch(logEvent('Start_App_With_Accounts')) } // Used to re-enable animations to login scene: @@ -265,7 +269,7 @@ export const Main = () => { useAsyncEffect( async () => { if (isMaestro()) return - setLegacyLanding((await getExperimentConfigValue('legacyLanding')) === 'legacyLanding') + setExperimentConfig(await getExperimentConfig()) }, [], 'setLegacyLanding' @@ -273,19 +277,19 @@ export const Main = () => { return ( <> - {legacyLanding == null ? ( + {experimentConfig == null ? ( ) : ( - - + + )} @@ -755,6 +759,7 @@ const EdgeBuyTabScreen = () => { }} /> + { - render() { - const { children, marginRem, paddingRem, warning, theme } = this.props - const styles = getStyles(theme) - const margin = sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)) - const padding = sidesToPadding(mapSides(fixSides(paddingRem, 1), theme.rem)) - - return ( - - {children} - - ) - } -} - -const getStyles = cacheStyles((theme: Theme) => ({ - wrapper: { - width: '100%' - }, - container: { - borderWidth: theme.cardBorder, - borderColor: theme.cardBorderColor, - borderRadius: theme.cardBorderRadius - }, - warning: { - borderColor: theme.warningIcon - } -})) - -export const Card = withTheme(CardComponent) diff --git a/src/components/cards/IconMessageCard.tsx b/src/components/cards/IconMessageCard.tsx index 2229e5416b6..12e51247b76 100644 --- a/src/components/cards/IconMessageCard.tsx +++ b/src/components/cards/IconMessageCard.tsx @@ -22,12 +22,15 @@ export function IconMessageCard(props: Props) { const { iconOrUri, message, title, testIds, onPress = () => {}, onClose } = props const theme = useTheme() const styles = getStyles(theme) + const imageSrc = React.useMemo(() => { + if (typeof iconOrUri === 'string') return { uri: iconOrUri } + }, [iconOrUri]) return ( - {typeof iconOrUri === 'string' ? : iconOrUri} + {typeof iconOrUri === 'string' && imageSrc != null ? : iconOrUri} {title == null ? null : ( diff --git a/src/components/cards/VisaCardCard.tsx b/src/components/cards/VisaCardCard.tsx index 1fc103ccae7..b496438dc17 100644 --- a/src/components/cards/VisaCardCard.tsx +++ b/src/components/cards/VisaCardCard.tsx @@ -35,7 +35,7 @@ export const VisaCardCard = (props: Props) => { const dispatch = useDispatch() const handlePress = useHandler(() => { - logEvent('Visa_Card_Launch') + dispatch(logEvent('Visa_Card_Launch')) dispatch(executePluginAction(navigation, 'rewardscard', 'sell')).catch(err => showError(err)) }) diff --git a/src/components/common/AnimatedNumber.tsx b/src/components/common/AnimatedNumber.tsx index 1e3053b0d3a..3dd71c17f9a 100644 --- a/src/components/common/AnimatedNumber.tsx +++ b/src/components/common/AnimatedNumber.tsx @@ -1,8 +1,9 @@ -import React from 'react' -import { LayoutChangeEvent, Text, TextStyle, View, ViewStyle } from 'react-native' +import React, { useMemo } from 'react' +import { LayoutChangeEvent, StyleProp, Text, TextStyle, View, ViewStyle } from 'react-native' import Animated, { Easing, EasingFunction, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated' import { useHandler } from '../../hooks/useHandler' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' const ANIMATION_DURATION_DEFAULT = 1000 const NUMBERS = Array(10) @@ -26,10 +27,13 @@ export const AnimatedNumber = (props: AnimatedNumberProps): JSX.Element => { setDigitHeight(event.nativeEvent.layout.height) }) + const numberStyle: StyleProp = useMemo(() => [style, { flexDirection: 'row' }], [style]) + const zeroStyle: StyleProp = useMemo(() => [textStyle, { position: 'absolute', top: -999999 }], [textStyle]) + return ( <> {digitHeight !== 0 ? ( - + {animateToNumbersArr.map((n, index) => { return ( { })} ) : ( - + 0 )} @@ -72,6 +76,9 @@ interface AnimatedDigitProps { const AnimatedDigit = (props: AnimatedDigitProps): JSX.Element => { const { animationDuration, digit, easing, textStyle, index, numberHeight } = props const animY = useSharedValue(0) + const textStyleProp: StyleProp = useMemo(() => [textStyle, { height: numberHeight }], [numberHeight, textStyle]) + const containerStyle: StyleProp = useMemo(() => ({ height: numberHeight, overflow: 'hidden' }), [numberHeight]) + const styles = getStyles(useTheme()) if (!isIntegerDigit(digit)) { animY.value = withTiming(0, { duration: animationDuration, easing }) @@ -91,21 +98,28 @@ const AnimatedDigit = (props: AnimatedDigitProps): JSX.Element => { }) if (!isIntegerDigit(digit)) { return ( - + {digit} ) } return ( - + {NUMBERS.map(number => ( - - {number} + + {number} ))} ) } +const getStyles = cacheStyles((theme: Theme) => ({ + textContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center' + } +})) diff --git a/src/components/common/Collapsable.tsx b/src/components/common/Collapsable.tsx deleted file mode 100644 index b0485f74f2d..00000000000 --- a/src/components/common/Collapsable.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react' -import Animated, { Easing, useAnimatedStyle, withTiming } from 'react-native-reanimated' - -import { useTheme } from '../services/ThemeContext' - -const TIME_SCALE = 500 - -interface Props { - minHeightRem: number - maxHeightRem?: number - duration?: number - isCollapsed: boolean - children: React.ReactNode -} - -export function Collapsable(props: Props) { - const { minHeightRem, maxHeightRem = minHeightRem, isCollapsed, duration = TIME_SCALE, children } = props - const theme = useTheme() - - const minHeight = theme.rem(minHeightRem) - const maxHeight = theme.rem(maxHeightRem) - - const heightAnimation = useAnimatedStyle(() => ({ - height: withTiming(isCollapsed ? minHeight : maxHeight, { duration, easing: Easing.out(Easing.poly(5)) }) - })) - - return {children} -} diff --git a/src/components/common/EdgeAnim.tsx b/src/components/common/EdgeAnim.tsx index abdee7dfe08..5cfb2178cc0 100644 --- a/src/components/common/EdgeAnim.tsx +++ b/src/components/common/EdgeAnim.tsx @@ -24,6 +24,37 @@ export const DEFAULT_ANIMATION_DURATION_MS = 300 export const LAYOUT_ANIMATION = LinearTransition.duration(DEFAULT_ANIMATION_DURATION_MS) export const MAX_LIST_ITEMS_ANIM = 10 +// Commonly used enter/exit animations. Use these to prevent +// dynamically created objects as params that cause a re-render +export const fadeIn: Anim = { type: 'fadeIn' } +export const fadeInUp: Anim = { type: 'fadeInUp' } +export const fadeInUp20: Anim = { type: 'fadeInUp', distance: 20 } +export const fadeInUp25: Anim = { type: 'fadeInUp', distance: 25 } +export const fadeInUp30: Anim = { type: 'fadeInUp', distance: 30 } +export const fadeInUp40: Anim = { type: 'fadeInUp', distance: 40 } +export const fadeInUp50: Anim = { type: 'fadeInUp', distance: 50 } +export const fadeInUp60: Anim = { type: 'fadeInUp', distance: 60 } +export const fadeInUp80: Anim = { type: 'fadeInUp', distance: 80 } +export const fadeInUp90: Anim = { type: 'fadeInUp', distance: 90 } +export const fadeInUp110: Anim = { type: 'fadeInUp', distance: 110 } +export const fadeInUp120: Anim = { type: 'fadeInUp', distance: 120 } +export const fadeInUp140: Anim = { type: 'fadeInUp', distance: 140 } +export const fadeInDown: Anim = { type: 'fadeInDown' } +export const fadeInDown10: Anim = { type: 'fadeInDown', distance: 10 } +export const fadeInDown20: Anim = { type: 'fadeInDown', distance: 20 } +export const fadeInDown30: Anim = { type: 'fadeInDown', distance: 30 } +export const fadeInDown40: Anim = { type: 'fadeInDown', distance: 40 } +export const fadeInDown50: Anim = { type: 'fadeInDown', distance: 50 } +export const fadeInDown60: Anim = { type: 'fadeInDown', distance: 60 } +export const fadeInDown75: Anim = { type: 'fadeInDown', distance: 75 } +export const fadeInDown80: Anim = { type: 'fadeInDown', distance: 80 } +export const fadeInDown90: Anim = { type: 'fadeInDown', distance: 90 } +export const fadeInDown110: Anim = { type: 'fadeInDown', distance: 110 } +export const fadeInDown120: Anim = { type: 'fadeInDown', distance: 120 } +export const fadeInDown140: Anim = { type: 'fadeInDown', distance: 140 } +export const fadeInLeft: Anim = { type: 'fadeInLeft' } +export const fadeOut: Anim = { type: 'fadeOut' } + type AnimBuilder = typeof ComplexAnimationBuilder type AnimTypeFadeIns = 'fadeIn' | 'fadeInDown' | 'fadeInUp' | 'fadeInLeft' | 'fadeInRight' type AnimTypeFadeOuts = 'fadeOut' | 'fadeOutDown' | 'fadeOutUp' | 'fadeOutLeft' | 'fadeOutRight' @@ -87,7 +118,7 @@ const getAnimBuilder = (anim?: Anim) => { return builder } -export const EdgeAnim = ({ children, disableAnimation, enter, exit, visible = true, ...rest }: Props): JSX.Element | null => { +const EdgeAnimInner = ({ children, disableAnimation, enter, exit, visible = true, ...rest }: Props): JSX.Element | null => { if (!visible) return null const entering = getAnimBuilder(enter) const exiting = getAnimBuilder(exit) @@ -103,3 +134,5 @@ export const EdgeAnim = ({ children, disableAnimation, enter, exit, visible = tr ) } + +export const EdgeAnim = React.memo(EdgeAnimInner) diff --git a/src/components/common/KeyboardTracker.ts b/src/components/common/KeyboardTracker.ts deleted file mode 100644 index 5d30a689758..00000000000 --- a/src/components/common/KeyboardTracker.ts +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from 'react' -import { Animated, Keyboard, KeyboardEvent, Platform } from 'react-native' - -interface Props { - children: (animation: Animated.Value, layout: number) => React.ReactNode - downValue?: number - upValue?: number | ((keyboardHeight: number) => number) -} - -/** - * Tracks the current keyboard state, - * and updates an animated value as the keyboard moves up or down. - * - * The animation moves smoothly between a "down value" and an "up value". - * You provide these endpoints as props, so they can accomodate things - * like notches or weird layouts. If your layout changes (like when the - * device rotates), just pass new values for `upValue` or `downValue`, - * and the animated value will adjust to match. If `upValue` depends on - * the keyboard height, just pass a function instead of number. - * - * This component also provides a "layout value" to your component. - * The layout value changes to the final `upValue` right before - * the keyboard starts sliding up, and goes back to `downValue` - * after the keyboard finishes disappearing. This gives you a chance - * to make layout updates in preparation for the animation, like adding - * extra space to the bottom of your component so a gap doesn't appear. - */ -export class KeyboardTracker extends React.Component { - animation: Animated.Value - animationGoal: number | undefined - nextDuration: number - sub: KeyboardSubscriber - - constructor(props: Props) { - super(props) - - if (globalKeyboardSubscriber == null) { - globalKeyboardSubscriber = new KeyboardSubscriber() - } - this.sub = globalKeyboardSubscriber - this.sub.register(this) - this.animation = new Animated.Value(this.calculateGoal()) - this.nextDuration = 0 - } - - calculateGoal() { - const { downValue = 0, upValue = height => height } = this.props - const { keyboardHeight, keyboardHiding } = this.sub - - if (keyboardHiding) return downValue - return typeof upValue === 'function' ? upValue(keyboardHeight) : upValue - } - - setNextDuration(duration: number) { - this.nextDuration = duration - } - - triggerAnimation() { - const nextGoal = this.calculateGoal() - if (nextGoal !== this.animationGoal) { - if (this.nextDuration !== 0) { - Animated.timing(this.animation, { - duration: this.nextDuration, - toValue: nextGoal, - useNativeDriver: false - }).start() - } else { - this.animation.setValue(nextGoal) - } - this.animationGoal = nextGoal - this.nextDuration = 0 - } - } - - updateLayout() { - if (this.props.children.length > 1) this.forceUpdate() - else this.triggerAnimation() - } - - componentWillUnmount() { - this.sub.unregister(this) - } - - componentDidUpdate() { - this.triggerAnimation() - } - - render() { - const { children } = this.props - return children(this.animation, this.calculateGoal()) - } -} - -/** - * All KeyboardTracker instances share the same subscription singleton. - */ -class KeyboardSubscriber { - trackers: KeyboardTracker[] - - // Hiding means we are either down or moving down: - keyboardHiding: boolean - keyboardHeight: number - - constructor() { - this.trackers = [] - this.keyboardHeight = 0 - this.keyboardHiding = true - - // Subscribe to the keyboard: - if (Platform.OS === 'android') { - Keyboard.addListener('keyboardDidHide', (event: KeyboardEvent) => { - this.keyboardHiding = true - this.keyboardHeight = 0 - this._updateLayouts() - }) - Keyboard.addListener('keyboardDidShow', (event: KeyboardEvent) => { - this.keyboardHiding = false - this.keyboardHeight = event.endCoordinates.height - this._updateLayouts() - }) - } else { - Keyboard.addListener('keyboardDidHide', (event: KeyboardEvent) => { - this.keyboardHeight = 0 - this._updateLayouts() - }) - Keyboard.addListener('keyboardWillHide', (event: KeyboardEvent) => { - this.keyboardHiding = true - this._setDurations(event.duration) - this._triggerAnimations() - }) - Keyboard.addListener('keyboardWillShow', (event: KeyboardEvent) => { - this.keyboardHiding = false - this.keyboardHeight = event.endCoordinates.height - this._setDurations(event.duration) - this._updateLayouts() - }) - } - } - - register(tracker: KeyboardTracker) { - this.trackers.push(tracker) - } - - unregister(tracker: KeyboardTracker) { - this.trackers = this.trackers.filter(item => item !== tracker) - } - - _setDurations(duration: number) { - for (const tracker of this.trackers) { - tracker.setNextDuration(duration) - } - } - - _triggerAnimations() { - for (const tracker of this.trackers) { - tracker.triggerAnimation() - } - } - - _updateLayouts() { - for (const tracker of this.trackers) { - tracker.updateLayout() - } - } -} - -let globalKeyboardSubscriber: KeyboardSubscriber | null = null diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index 0835fe9841e..bf3d941891a 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -1,9 +1,10 @@ import { getDefaultHeaderHeight } from '@react-navigation/elements' -import { useFocusEffect, useNavigation } from '@react-navigation/native' +import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native' import * as React from 'react' -import { useCallback, useMemo } from 'react' -import { Animated, StyleSheet, View } from 'react-native' -import Reanimated from 'react-native-reanimated' +import { useEffect, useMemo, useState } from 'react' +import { StyleSheet, View, ViewStyle } from 'react-native' +import { useKeyboardHandler, useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller' +import Reanimated, { runOnJS, useAnimatedStyle } from 'react-native-reanimated' import { EdgeInsets, useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' @@ -17,7 +18,6 @@ import { NotificationView } from '../notification/NotificationView' import { useTheme } from '../services/ThemeContext' import { MAX_TAB_BAR_HEIGHT } from '../themed/MenuTabs' import { AccentColors, DotsBackground } from '../ui4/DotsBackground' -import { KeyboardTracker } from './KeyboardTracker' export interface InsetStyle { paddingTop: number @@ -107,60 +107,10 @@ interface SceneWrapperProps { * negative margin style rules to be used to offset these insets. */ function SceneWrapperComponent(props: SceneWrapperProps): JSX.Element { - const { avoidKeyboard = false } = props - - // Subscribe to the window size: - const { height: frameHeight } = useSafeAreaFrame() - - // These represent the distance from the top of the screen to the top of - // the keyboard depending if the keyboard is down or up. - const downValue = frameHeight - const upValue = useCallback((keyboardHeight: number) => downValue - keyboardHeight, [downValue]) - - return avoidKeyboard ? ( - - {(keyboardAnimation, trackerValue) => ( - - )} - - ) : ( - - ) -} -export const SceneWrapper = React.memo(SceneWrapperComponent) - -const styles = StyleSheet.create({ - sceneContainer: { - // Children: - alignItems: 'stretch', - flexDirection: 'column', - justifyContent: 'flex-start' - } -}) - -interface SceneWrapperInnerProps extends SceneWrapperProps { - keyboardAnimation: Animated.Value | undefined - trackerValue: number -} - -function SceneWrapperInnerComponent(props: SceneWrapperInnerProps) { - const { keyboardAnimation, trackerValue } = props const { overrideDots, accentColors, - avoidKeyboard = false, + avoidKeyboard = props.scroll ?? false, backgroundGradientColors, backgroundGradientStart, backgroundGradientEnd, @@ -181,6 +131,15 @@ function SceneWrapperInnerComponent(props: SceneWrapperInnerProps) { const navigation = useNavigation() const theme = useTheme() + // We need to track this state in the JS thread because insets are not shared values + const [isKeyboardOpen, setIsKeyboardOpen] = useState(false) + useKeyboardHandler({ + onStart(event) { + 'worklet' + runOnJS(setIsKeyboardOpen)(event.progress === 1) + } + }) + // Reset the footer ratio when focused // We can do this because multiple calls to resetFooterRatio isn't costly // because it just sets snapTo SharedValue to `1` @@ -205,18 +164,14 @@ function SceneWrapperInnerComponent(props: SceneWrapperInnerProps) { const notificationHeight = theme.rem(4) const headerBarHeight = getDefaultHeaderHeight({ height: frameHeight, width: frameWidth }, false, 0) - // Derive the keyboard height by getting the difference between screen height - // and trackerValue. This value should be from zero to keyboard height - // depending on the open state of the keyboard - const keyboardHeight = frameHeight - trackerValue - const isKeyboardOpen = avoidKeyboard && keyboardHeight !== 0 - // Calculate app insets considering the app's header, tab-bar, // notification area, etc: const maybeHeaderHeight = hasHeader ? headerBarHeight : 0 const maybeNotificationHeight = isLightAccount ? notificationHeight : 0 - const maybeTabBarHeight = hasTabs ? MAX_TAB_BAR_HEIGHT : 0 - const maybeInsetBottom = !hasTabs && !isKeyboardOpen ? safeAreaInsets.bottom : 0 + // Ignore tab bar height when keyboard is open because it is rendered behind it + const maybeTabBarHeight = hasTabs && !isKeyboardOpen ? MAX_TAB_BAR_HEIGHT : 0 + // Ignore inset bottom when keyboard is open because it is rendered behind it + const maybeInsetBottom = !isKeyboardOpen ? safeAreaInsets.bottom : 0 const insets: EdgeInsets = useMemo( () => ({ top: safeAreaInsets.top + maybeHeaderHeight, @@ -267,13 +222,43 @@ function SceneWrapperInnerComponent(props: SceneWrapperInnerProps) { [hasTabs, insetStyle, insets, isKeyboardOpen, undoInsetStyle] ) + // Animated style for max-height to respond to keyboard + const { height: keyboardHeightDiff } = useReanimatedKeyboardAnimation() + const keyboardAwareStyle = useAnimatedStyle(() => { + const maybeKeyboardHeightDiff = avoidKeyboard ? keyboardHeightDiff.value : 0 + return { + maxHeight: frameHeight + maybeKeyboardHeightDiff + } + }, [avoidKeyboard, frameHeight]) + // If function children, the caller handles the insets and overscroll const memoizedChildren = useMemo(() => (typeof children === 'function' ? children(sceneWrapperInfo) : children), [children, sceneWrapperInfo]) + if (scroll) { + return ( + <> + + + {memoizedChildren} + + {renderFooter == null ? null : ( + + )} + {hasNotifications ? : null} + + ) + } + if (avoidKeyboard) { return ( <> - + {memoizedChildren} - {renderFooter == null ? null : } - + {renderFooter == null ? null : ( + + )} + {hasNotifications ? : null} ) } - if (scroll) { - return ( - <> - - - {memoizedChildren} - - - ) - } - return ( <> @@ -320,57 +290,75 @@ function SceneWrapperInnerComponent(props: SceneWrapperInnerProps) { {memoizedChildren} - {renderFooter == null ? null : } + {renderFooter == null ? null : ( + + )} {hasNotifications ? : null} ) } -const SceneWrapperInner = React.memo(SceneWrapperInnerComponent) +export const SceneWrapper = React.memo(SceneWrapperComponent) -interface SceneWrapperScrollViewProps - extends Pick { +const styles = StyleSheet.create({ + sceneContainer: { + // Children: + alignItems: 'stretch', + flexDirection: 'column', + justifyContent: 'flex-start' + } +}) + +interface SceneWrapperScrollViewProps extends Pick { children: React.ReactNode + keyboardAwareStyle: ViewStyle insetStyle: InsetStyle layoutStyle: { height: number width: number } - navigation: NavigationBase - sceneWrapperInfo: SceneWrapperInfo } function SceneWrapperScrollViewComponent(props: SceneWrapperScrollViewProps) { - const { children, insetStyle, layoutStyle, navigation, sceneWrapperInfo } = props - const { hasNotifications = false, hasTabs = false, footerHeight = 0, keyboardShouldPersistTaps, padding = 0, renderFooter } = props + const { children, keyboardAwareStyle, insetStyle, layoutStyle } = props + const { keyboardShouldPersistTaps, padding = 0 } = props // If the scene has scroll, this will be required for tabs and/or header animation const handleScroll = useSceneScrollHandler() return ( - <> - - {children} - - {renderFooter == null ? null : } - {hasNotifications ? : null} - + + {children} + ) } const SceneWrapperScrollView = React.memo(SceneWrapperScrollViewComponent) -interface SceneWrapperFooterContainerProps extends Required> { +interface SceneWrapperFooterContainerProps extends Required> { sceneWrapperInfo: SceneWrapperInfo } function SceneWrapperFooterContainerComponent(props: SceneWrapperFooterContainerProps) { - const { hasTabs, sceneWrapperInfo, renderFooter } = props + const { footerHeight, hasTabs, sceneWrapperInfo, renderFooter } = props + + // Set the global shared value for the footerHeight so that way the + // background in the MenuTabs can translate accordingly + const footerHeightShared = useSceneFooterState(state => state.footerHeight) + const isFocused = useIsFocused() + useEffect(() => { + if (isFocused) { + footerHeightShared.value = footerHeight + return () => { + footerHeightShared.value = 0 + } + } + }, [footerHeight, footerHeightShared, isFocused]) // Portal the render function to the SceneFooterState if (hasTabs) { diff --git a/src/components/icons/CryptoIcon.tsx b/src/components/icons/CryptoIcon.tsx deleted file mode 100644 index 44c5a91ee9f..00000000000 --- a/src/components/icons/CryptoIcon.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { EdgeTokenId } from 'edge-core-js' -import * as React from 'react' -import { StyleSheet, View } from 'react-native' -import FastImage from 'react-native-fast-image' - -import compromisedIcon from '../../assets/images/compromisedIcon.png' -import { useWatch } from '../../hooks/useWatch' -import { useSelector } from '../../types/reactRedux' -import { getCurrencyIconUris } from '../../util/CdnUris' -import { fixSides, mapSides, sidesToMargin } from '../../util/sides' -import { WalletSyncCircle } from '../progress-indicators/WalletSyncCircle' -import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' - -interface Props { - // Main props - If non is specified, would just render an empty view - pluginId?: string // Needed when walletId is not supplied and we still want to get an icon - tokenId: EdgeTokenId // Needed when it's a token (not the plugin's native currency) - walletId?: string // To allow showing the progress ratio sync circle - - // Image props - hideSecondary?: boolean // Only show the currency icon for token (no secondary icon for the network) - mono?: boolean // To use the mono dark icon logo - - // Styling props - marginRem?: number | number[] - sizeRem?: number -} - -const CryptoIconComponent = (props: Props) => { - const { hideSecondary = false, marginRem, mono = false, sizeRem = 2, tokenId, walletId } = props - - const theme = useTheme() - const styles = getStyles(theme) - const size = theme.rem(sizeRem) - - // Track wallets state from account and update the wallet when ready - const account = useSelector(state => state.core.account) - const currencyWallets = useWatch(account, 'currencyWallets') - const wallet = walletId != null ? currencyWallets[walletId] : null - const compromised = useSelector(state => { - if (walletId == null) return 0 - const { modalShown = 0 } = state.ui?.settings?.securityCheckedWallets?.[walletId] ?? {} - return modalShown > 0 - }) - - const { pluginId = wallet?.currencyInfo.pluginId } = props - - // Primary Currency icon - const primaryCurrencyIcon = React.useMemo(() => { - if (pluginId == null) return null - - // Get Currency Icon URI - const icon = getCurrencyIconUris(pluginId, tokenId) - const source = { uri: mono ? icon.symbolImageDarkMono : icon.symbolImage } - - // Return Currency logo from the edge server - return - }, [pluginId, tokenId, mono]) - - // Secondary (parent) currency icon (if it's a token) - const secondaryCurrencyIcon = React.useMemo(() => { - if (compromised) { - return - } - - // Skip if this is not a token: - if (pluginId == null || tokenId == null || tokenId === pluginId) { - return null - } - - // Get Parent Icon URI - const icon = getCurrencyIconUris(pluginId, null) - const source = { uri: mono ? icon.symbolImageDarkMono : icon.symbolImage } - - // Return Parent logo from the edge server - return - }, [compromised, mono, pluginId, styles.parentIcon, tokenId]) - - // Main view styling - const spacingStyle = React.useMemo( - () => ({ - ...sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)), - height: size, - width: size - }), - [marginRem, size, theme.rem] - ) - - return ( - - {wallet == null ? null : ( - - )} - {primaryCurrencyIcon} - {hideSecondary ? null : secondaryCurrencyIcon} - - ) -} - -const getStyles = cacheStyles((theme: Theme) => ({ - parentIcon: { - position: 'absolute', - bottom: 0, - right: 0, - width: '50%', - height: '50%' - } -})) - -export const CryptoIcon = React.memo(CryptoIconComponent) diff --git a/src/components/icons/FiatIcon.tsx b/src/components/icons/FiatIcon.tsx index 29ba3218aad..651954a71bf 100644 --- a/src/components/icons/FiatIcon.tsx +++ b/src/components/icons/FiatIcon.tsx @@ -27,21 +27,34 @@ export const FiatIconComponent = (props: Props) => { const styles = getStyles(theme) const fiatBackgroundIcon = getCurrencyIconUris('fiat', null) - const source = { uri: mono ? fiatBackgroundIcon.symbolImageDarkMono : fiatBackgroundIcon.symbolImage } + const source = React.useMemo( + () => ({ uri: mono ? fiatBackgroundIcon.symbolImageDarkMono : fiatBackgroundIcon.symbolImage }), + [fiatBackgroundIcon.symbolImage, fiatBackgroundIcon.symbolImageDarkMono, mono] + ) const fiatSymbol = getSymbolFromCurrency(fixFiatCurrencyCode(fiatCurrencyCode)) - const fiatSymbolSizing = { fontSize: theme.rem(sizeRem * 0.625) } // Main view styling - const iconSizing = { - ...sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)), - height: theme.rem(sizeRem), - width: theme.rem(sizeRem) - } + const viewStyle = React.useMemo(() => { + return [ + styles.fiatIcon, + { + ...sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)), + height: theme.rem(sizeRem), + width: theme.rem(sizeRem) + } + ] + }, [marginRem, sizeRem, styles.fiatIcon, theme]) + + const textStyle = React.useMemo(() => { + const fiatSymbolSizing = { fontSize: theme.rem(sizeRem * 0.625) } + + return [styles.fiatSymbol, fiatSymbolSizing] + }, [sizeRem, styles.fiatSymbol, theme]) return ( - + - + {fiatSymbol} diff --git a/src/components/legacy/Buttons/PrimaryButton.ui.tsx b/src/components/legacy/Buttons/PrimaryButton.ui.tsx deleted file mode 100644 index fcc6bebb81a..00000000000 --- a/src/components/legacy/Buttons/PrimaryButton.ui.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react' -import { StyleSheet, Text as RNText, TextStyle, TouchableHighlight, TouchableHighlightProps, View } from 'react-native' - -import { THEME } from '../../../theme/variables/airbitz' -import { scale } from '../../../util/scaling' - -interface TextProps { - children: React.ReactNode - style?: TextStyle -} -class Text extends React.Component { - render() { - const { children, style, ...props } = this.props - return ( - - {children} - - ) - } -} - -interface Props extends TouchableHighlightProps { - children: React.ReactNode - style?: TextStyle - onPress?: () => unknown -} - -export class PrimaryButton extends React.Component { - static Text = Text - render() { - const { children, style, ...props } = this.props - return ( - - {children} - - ) - } -} - -const primaryButtonUnderlay = { color: THEME.COLORS.PRIMARY } - -const styles = StyleSheet.create({ - button: { - padding: 14, - borderRadius: 5, - alignItems: 'center', - justifyContent: 'center', - flex: -1 - }, - buttonText: { - fontFamily: THEME.FONTS.DEFAULT, - fontSize: scale(18), - lineHeight: scale(18), - position: 'relative', - top: 1 - }, - // PRIMARY BUTTON - primaryButton: { - backgroundColor: THEME.COLORS.SECONDARY - }, - primaryButtonText: { - color: THEME.COLORS.WHITE - } -}) diff --git a/src/components/legacy/FormattedText/FormattedText.ui.tsx b/src/components/legacy/FormattedText/FormattedText.ui.tsx deleted file mode 100644 index a171854678f..00000000000 --- a/src/components/legacy/FormattedText/FormattedText.ui.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react' -import { Text, TextStyle } from 'react-native' - -import { scale } from '../../../util/scaling' -import { styles } from './style' - -interface Props { - children: React.ReactNode - fontSize?: number - isBold?: boolean - style?: TextStyle -} - -export class FormattedText extends React.Component { - style: TextStyle | undefined - nativeForward: any - - constructor(props: Props) { - super(props) - // @ts-expect-error - this.style = this.props.isBold ? [styles.boldStyle] : [styles.defaultStyle] - - if (props.style) { - if (Array.isArray(props.style)) { - // @ts-expect-error - this.style = this.style.concat(props.style) - } else { - // @ts-expect-error - this.style.push(props.style) - } - } - } - - handleRef = (element: any) => { - this.nativeForward = element - } - - setNativeProps(props: Props) { - this.nativeForward.setNativeProps(props) - } - - render() { - const fontSize = this.props.fontSize ? scale(this.props.fontSize) : scale(14) - return ( - - {this.props.children} - - ) - } -} diff --git a/src/components/legacy/FormattedText/style.ts b/src/components/legacy/FormattedText/style.ts deleted file mode 100644 index dfdc8e2c16d..00000000000 --- a/src/components/legacy/FormattedText/style.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { THEME } from '../../../theme/variables/airbitz' -export const styles = { - defaultStyle: { - fontFamily: THEME.FONTS.DEFAULT - }, - boldStyle: { - fontFamily: THEME.FONTS.BOLD - } -} diff --git a/src/components/modals/AddressModal.tsx b/src/components/modals/AddressModal.tsx index 742be0df3a3..e0fceb54147 100644 --- a/src/components/modals/AddressModal.tsx +++ b/src/components/modals/AddressModal.tsx @@ -1,7 +1,6 @@ -import { FlashList } from '@shopify/flash-list' import { EdgeAccount, EdgeCurrencyConfig, EdgeCurrencyWallet } from 'edge-core-js' import * as React from 'react' -import { ActivityIndicator, Image, TouchableWithoutFeedback, View } from 'react-native' +import { ActivityIndicator, FlatList, Image, Text, TouchableWithoutFeedback, View } from 'react-native' import { AirshipBridge } from 'react-native-airship' import { sprintf } from 'sprintf-js' @@ -14,11 +13,10 @@ import { connect } from '../../types/reactRedux' import { ResolutionError } from '../../types/ResolutionError' import { FioAddress, FlatListItem } from '../../types/types' import { checkPubAddress, FioAddresses, getFioAddressCache } from '../../util/FioAddressUtils' -import { FormattedText as Text } from '../legacy/FormattedText/FormattedText.ui' import { showError } from '../services/AirshipInstance' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' import { FilledTextInput } from '../themed/FilledTextInput' -import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' +import { ButtonUi4 } from '../ui4/ButtonUi4' import { ModalUi4 } from '../ui4/ModalUi4' interface OwnProps { @@ -319,9 +317,9 @@ export class AddressModalComponent extends React.Component { showSpinner={showSpinner} /> {!userFioAddressesLoading ? ( - { )} - + {/* TODO: Sync between LoginUi <-> Gui + + */} + ) } @@ -350,14 +351,20 @@ const getStyles = cacheStyles((theme: Theme) => ({ alignItems: 'center' }, fioAddressText: { + color: theme.primaryText, + fontFamily: theme.fontFaceDefault, fontSize: theme.rem(1), - paddingLeft: theme.rem(0.75), - color: theme.primaryText + paddingLeft: theme.rem(0.75) }, loaderContainer: { - flex: 1, + flexGrow: 1, + flexShrink: 1, justifyContent: 'center', alignItems: 'center' + }, + listContainer: { + flexGrow: 0, + flexShrink: 1 } })) diff --git a/src/components/modals/ButtonsModal.tsx b/src/components/modals/ButtonsModal.tsx index 1f0cd515958..e05205434b5 100644 --- a/src/components/modals/ButtonsModal.tsx +++ b/src/components/modals/ButtonsModal.tsx @@ -109,7 +109,7 @@ export function ButtonsModal(prop ) } - return + return })} diff --git a/src/components/modals/ConfirmContinueModal.tsx b/src/components/modals/ConfirmContinueModal.tsx index 607e3e75832..21c203685cc 100644 --- a/src/components/modals/ConfirmContinueModal.tsx +++ b/src/components/modals/ConfirmContinueModal.tsx @@ -72,7 +72,7 @@ export function ConfirmContinueModal(props: Props) { - + ) diff --git a/src/components/modals/FioExpiredModal.tsx b/src/components/modals/FioExpiredModal.tsx index 301d96c909c..2aec80ee151 100644 --- a/src/components/modals/FioExpiredModal.tsx +++ b/src/components/modals/FioExpiredModal.tsx @@ -14,7 +14,7 @@ export function FioExpiredModal(props: { bridge: AirshipBridge; fioName bridge.resolve(false)}> {lstrings.fio_domain_details_expired_soon} {fioName} - bridge.resolve(true)} /> + bridge.resolve(true)} /> ) } diff --git a/src/components/modals/FlipInputModal2.tsx b/src/components/modals/FlipInputModal2.tsx index 0503903c102..12a1d67610a 100644 --- a/src/components/modals/FlipInputModal2.tsx +++ b/src/components/modals/FlipInputModal2.tsx @@ -203,7 +203,7 @@ const FlipInputModal2Component = React.forwardRef((pro onNext={handleCloseModal} /> {getSpecialCurrencyInfo(pluginId).noMaxSpend !== true && hideMaxButton !== true ? ( - + ) : null} ) diff --git a/src/components/modals/LogsModal.tsx b/src/components/modals/LogsModal.tsx index ed7ee19a1ba..fabcecf7956 100644 --- a/src/components/modals/LogsModal.tsx +++ b/src/components/modals/LogsModal.tsx @@ -87,7 +87,7 @@ export const LogsModal = (props: Props) => { {isDangerous ? null : ( )} - + ) } diff --git a/src/components/modals/PermissionsSettingModal.tsx b/src/components/modals/PermissionsSettingModal.tsx index 31af199dfb0..77bdc2edfca 100644 --- a/src/components/modals/PermissionsSettingModal.tsx +++ b/src/components/modals/PermissionsSettingModal.tsx @@ -47,7 +47,7 @@ export function PermissionsSettingModal(props: { return ( {message} - + ) } diff --git a/src/components/modals/RawTextModal.tsx b/src/components/modals/RawTextModal.tsx index d1c93041857..07fdc571b68 100644 --- a/src/components/modals/RawTextModal.tsx +++ b/src/components/modals/RawTextModal.tsx @@ -32,9 +32,7 @@ export function RawTextModal(props: Props) { {body} - {disableCopy ? null : ( - - )} + {disableCopy ? null : } ) } diff --git a/src/components/modals/ScanModal.tsx b/src/components/modals/ScanModal.tsx index adcec0a61c2..7532814baee 100644 --- a/src/components/modals/ScanModal.tsx +++ b/src/components/modals/ScanModal.tsx @@ -196,7 +196,7 @@ export const ScanModal = (props: Props) => { return ( {lstrings.scan_camera_permission_denied} - + ) } diff --git a/src/components/modals/SwapVerifyTermsModal.tsx b/src/components/modals/SwapVerifyTermsModal.tsx index 068f36eb731..57de3e48190 100644 --- a/src/components/modals/SwapVerifyTermsModal.tsx +++ b/src/components/modals/SwapVerifyTermsModal.tsx @@ -70,8 +70,8 @@ function SwapVerifyTermsModal(props: Props) { onCancel={() => bridge.resolve(false)} > {lstrings.swap_terms_statement} - bridge.resolve(true)} /> - bridge.resolve(false)} /> + bridge.resolve(true)} /> + bridge.resolve(false)} /> {termsUri == null ? null : ( await Linking.openURL(termsUri)}> diff --git a/src/components/modals/TextInputModal.tsx b/src/components/modals/TextInputModal.tsx index 155fae79ea9..f96727a21c6 100644 --- a/src/components/modals/TextInputModal.tsx +++ b/src/components/modals/TextInputModal.tsx @@ -92,7 +92,7 @@ export function TextInputModal(props: Props) { const isAndroid = Platform.OS === 'android' // TODO: Address this in ButtonsViewUi4 - const androidButtonMargin = isAndroid ? [0.5, 0.5, 2, 0.5] : 0.5 + const androidButtonMargin = isAndroid ? [1, 1, 2, 1] : 1 return ( bridge.resolve(undefined)}> diff --git a/src/components/modals/TransferModal.tsx b/src/components/modals/TransferModal.tsx index 39849314aa9..09eed98c310 100644 --- a/src/components/modals/TransferModal.tsx +++ b/src/components/modals/TransferModal.tsx @@ -83,7 +83,7 @@ export const TransferModal = ({ account, bridge, depositOrSend, navigation }: Pr if (result?.type === 'wallet') { const { walletId, tokenId } = result await dispatch(selectWalletToken({ navigation, walletId, tokenId })) - navigation.navigate('request', {}) + navigation.navigate('request', { tokenId, walletId }) } } }) diff --git a/src/components/modals/UpdateModal.tsx b/src/components/modals/UpdateModal.tsx index fcf4881a0ef..ebd983bcd76 100644 --- a/src/components/modals/UpdateModal.tsx +++ b/src/components/modals/UpdateModal.tsx @@ -43,7 +43,7 @@ export function UpdateModal(props: Props) { > {message} - + ) } diff --git a/src/components/modals/WcSmartContractModal.tsx b/src/components/modals/WcSmartContractModal.tsx index 8256a00c500..af8223b0119 100644 --- a/src/components/modals/WcSmartContractModal.tsx +++ b/src/components/modals/WcSmartContractModal.tsx @@ -129,6 +129,13 @@ export const WcSmartContractModal = (props: Props) => { await walletConnect.approveRequest(topic, requestId, signedTxs) break } + case 'cosmos_getAccounts': + case 'cosmos_signDirect': + case 'cosmos_signAmino': { + const cleanPayload = asEither(asCosmosGetAccountsPayload, asCosmosSignDirectPayload, asCosmosSignAminoPayload)(payload) + const result = await wallet.signMessage('', { otherParams: cleanPayload }) + await walletConnect.approveRequest(topic, requestId, JSON.parse(result)) + } } } catch (e: any) { await walletConnect.rejectRequest(topic, requestId) @@ -247,7 +254,42 @@ const asAlgoWcRpcPayload = asObject({ ) }) -const asPayload = asObject({ method: asEither(asAlgoPayloadMethod, asEvmSignMethod, asEvmTransactionMethod) }).withRest +const asCosmosPayloadMethod = asValue('cosmos_getAccounts', 'cosmos_signDirect', 'cosmos_signAmino') +const asCosmosGetAccountsPayload = asObject({ + method: asValue('cosmos_getAccounts'), + params: asObject({}) +}) +const asCosmosSignDirectPayload = asObject({ + method: asValue('cosmos_signDirect'), + params: asObject({ + signerAddress: asString, + signDoc: asObject({ + chainId: asString, + accountNumber: asString, + authInfoBytes: asString, + bodyBytes: asString + }) + }) +}) +const asCosmosSignAminoPayload = asObject({ + method: asValue('cosmos_signAmino'), + params: asObject({ + signerAddress: asString, + signDoc: asObject({ + chain_id: asString, + account_number: asString, + sequence: asString, + memo: asString, + msgs: asArray(asUnknown), + fee: asObject({ + amount: asArray(asObject({ denom: asString, amount: asString })), + gas: asString + }) + }) + }) +}) + +const asPayload = asObject({ method: asEither(asCosmosPayloadMethod, asAlgoPayloadMethod, asEvmSignMethod, asEvmTransactionMethod) }).withRest export const asWcSmartContractModalProps = asObject({ dApp: asObject({ diff --git a/src/components/navigation/FlashNotification.tsx b/src/components/navigation/FlashNotification.tsx index 68c1f82fdc9..acaa66c4813 100644 --- a/src/components/navigation/FlashNotification.tsx +++ b/src/components/navigation/FlashNotification.tsx @@ -3,11 +3,9 @@ import { Text, View } from 'react-native' import { AirshipBridge } from 'react-native-airship' import AntDesignIcon from 'react-native-vector-icons/AntDesign' -import { useHandler } from '../../hooks/useHandler' import { THEME } from '../../theme/variables/airbitz' import { AirshipDropdown } from '../common/AirshipDropdown' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' -import { ModalFooter } from '../themed/ModalParts' interface Props { bridge: AirshipBridge @@ -20,14 +18,11 @@ export function FlashNotification(props: Props) { const theme = useTheme() const styles = getStyles(theme) - const handleClose = useHandler(() => bridge.resolve()) - return ( {message} - ) diff --git a/src/components/notification/NotificationView.tsx b/src/components/notification/NotificationView.tsx index a546cb70136..7a8fc9c2638 100644 --- a/src/components/notification/NotificationView.tsx +++ b/src/components/notification/NotificationView.tsx @@ -11,7 +11,7 @@ import { lstrings } from '../../locales/strings' import { useSceneFooterState } from '../../state/SceneFooterState' import { useDispatch, useSelector } from '../../types/reactRedux' import { NavigationBase } from '../../types/routerTypes' -import { EdgeAnim } from '../common/EdgeAnim' +import { EdgeAnim, fadeIn, fadeOut } from '../common/EdgeAnim' import { styled } from '../hoc/styled' import { showError } from '../services/AirshipInstance' import { useTheme } from '../services/ThemeContext' @@ -27,9 +27,6 @@ interface Props { let hasInteractedWithBackupModalLocal = false -const fadeIn = { type: 'fadeIn' } as const -const fadeOut = { type: 'fadeOut' } as const - const NotificationViewComponent = (props: Props) => { const { navigation, hasTabs, footerHeight } = props const theme = useTheme() diff --git a/src/components/scenes/ChangeMiningFeeScene.tsx b/src/components/scenes/ChangeMiningFeeScene.tsx index c4c643a4a8e..47360411eb7 100644 --- a/src/components/scenes/ChangeMiningFeeScene.tsx +++ b/src/components/scenes/ChangeMiningFeeScene.tsx @@ -1,4 +1,4 @@ -import { JsonObject } from 'edge-core-js' +import { EdgeCurrencyWallet, EdgeSpendInfo, EdgeTokenId, JsonObject } from 'edge-core-js' import * as React from 'react' import { ScrollView, View } from 'react-native' import Evilicons from 'react-native-vector-icons/EvilIcons' @@ -8,12 +8,11 @@ import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { FEE_STRINGS } from '../../constants/WalletAndCurrencyConstants' import { useIconColor } from '../../hooks/useIconColor' import { lstrings } from '../../locales/strings' -import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { FeeOption } from '../../types/types' -import { getTokenId } from '../../util/CurrencyInfoHelpers' import { darkenHexColor } from '../../util/utils' import { SceneWrapper } from '../common/SceneWrapper' +import { withWallet } from '../hoc/withWallet' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' import { SettingsRadioRow } from '../settings/SettingsRadioRow' import { Alert } from '../themed/Alert' @@ -22,7 +21,17 @@ import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' import { AccentColors } from '../ui4/DotsBackground' -interface OwnProps extends EdgeSceneProps<'changeMiningFee2'> {} +export interface ChangeMiningFeeParams { + maxSpendSet: boolean + spendInfo: EdgeSpendInfo + tokenId: EdgeTokenId + walletId: string + onSubmit: (networkFeeOption: FeeOption, customNetworkFee: JsonObject) => void +} + +interface OwnProps extends EdgeSceneProps<'changeMiningFee2'> { + wallet: EdgeCurrencyWallet +} interface HookProps { iconColor?: string @@ -69,8 +78,7 @@ export class ChangeMiningFeeComponent extends React.PureComponent | undefined { - const { route } = this.props - const { wallet } = route.params + const { wallet } = this.props if (wallet.currencyInfo.defaultSettings != null) { const { customFeeSettings } = wallet.currencyInfo.defaultSettings return customFeeSettings @@ -144,7 +152,7 @@ export class ChangeMiningFeeComponent extends React.PureComponent + ) @@ -216,14 +224,11 @@ const getStyles = cacheStyles((theme: Theme) => { const ChangeMiningFeeSceneThemed = withTheme(ChangeMiningFeeComponent) -export const ChangeMiningFeeScene = (props: OwnProps) => { - const account = useSelector(state => state.core.account) - const currencyCode = useSelector(state => state.ui.wallets.selectedCurrencyCode) - const walletId = useSelector(state => state.ui.wallets.selectedWalletId) - const wallet = account.currencyWallets[walletId] ?? {} - const { pluginId = '' } = wallet.currencyInfo ?? {} - const tokenId = getTokenId(account, pluginId, currencyCode) +export const ChangeMiningFeeScene = withWallet((props: OwnProps) => { + const { route, wallet } = props + const { tokenId } = route.params + const { pluginId } = wallet.currencyInfo - const iconColor = useIconColor({ pluginId, tokenId: tokenId !== undefined ? tokenId : '' }) + const iconColor = useIconColor({ pluginId, tokenId }) return -} +}) diff --git a/src/components/scenes/ChangePasswordScene.tsx b/src/components/scenes/ChangePasswordScene.tsx index f37389a6b2b..e2dc0e2fd86 100644 --- a/src/components/scenes/ChangePasswordScene.tsx +++ b/src/components/scenes/ChangePasswordScene.tsx @@ -1,40 +1,33 @@ -import { EdgeAccount, EdgeContext } from 'edge-core-js' import { ChangePasswordScreen } from 'edge-login-ui-rn' import * as React from 'react' -import { connect } from '../../types/reactRedux' +import { useHandler } from '../../hooks/useHandler' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { logActivity } from '../../util/logger' import { logEvent } from '../../util/tracking' import { SceneWrapper } from '../common/SceneWrapper' -interface OwnProps extends EdgeSceneProps<'changePassword'> {} +interface Props extends EdgeSceneProps<'changePassword'> {} -interface StateProps { - account: EdgeAccount - context: EdgeContext -} -type Props = StateProps & OwnProps +export const ChangePasswordScene = (props: Props) => { + const { navigation } = props + const account = useSelector(state => state.core.account) + const context = useSelector(state => state.core.context) + const dispatch = useDispatch() -export class ChangePasswordComponent extends React.Component { - render() { - const { context, account, navigation } = this.props - const handleComplete = () => { - logActivity(`Password Changed: ${account.username}`) - navigation.goBack() - } - return ( - - - - ) - } -} + const handleComplete = useHandler(() => { + logActivity(`Password Changed: ${account.username}`) + navigation.goBack() + }) -export const ChangePasswordScene = connect( - state => ({ - context: state.core.context, - account: state.core.account - }), - dispatch => ({}) -)(ChangePasswordComponent) + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) + + return ( + + + + ) +} diff --git a/src/components/scenes/ChangePinScene.tsx b/src/components/scenes/ChangePinScene.tsx index 407fedf1126..747f58c1cb1 100644 --- a/src/components/scenes/ChangePinScene.tsx +++ b/src/components/scenes/ChangePinScene.tsx @@ -1,40 +1,33 @@ -import { EdgeAccount, EdgeContext } from 'edge-core-js' import { ChangePinScreen } from 'edge-login-ui-rn' import * as React from 'react' -import { connect } from '../../types/reactRedux' +import { useHandler } from '../../hooks/useHandler' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { logActivity } from '../../util/logger' import { logEvent } from '../../util/tracking' import { SceneWrapper } from '../common/SceneWrapper' -interface OwnProps extends EdgeSceneProps<'changePin'> {} +interface Props extends EdgeSceneProps<'changePin'> {} -interface StateProps { - account: EdgeAccount - context: EdgeContext -} -type Props = StateProps & OwnProps +export const ChangePinScene = (props: Props) => { + const { navigation } = props + const account = useSelector(state => state.core.account) + const context = useSelector(state => state.core.context) + const dispatch = useDispatch() -export class ChangePinComponent extends React.Component { - render() { - const { context, account, navigation } = this.props - const handleComplete = () => { - logActivity(`PIN Changed: ${account.username}`) - navigation.goBack() - } - return ( - - - - ) + const handleComplete = () => { + logActivity(`PIN Changed: ${account.username}`) + navigation.goBack() } -} -export const ChangePinScene = connect( - state => ({ - context: state.core.context, - account: state.core.account - }), - dispatch => ({}) -)(ChangePinComponent) + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) + + return ( + + + + ) +} diff --git a/src/components/scenes/CoinRankingDetailsScene.tsx b/src/components/scenes/CoinRankingDetailsScene.tsx index 0426b83158f..895e179e8c4 100644 --- a/src/components/scenes/CoinRankingDetailsScene.tsx +++ b/src/components/scenes/CoinRankingDetailsScene.tsx @@ -11,7 +11,7 @@ import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { formatLargeNumberString as formatLargeNumber } from '../../util/utils' import { SwipeChart } from '../charts/SwipeChart' -import { EdgeAnim } from '../common/EdgeAnim' +import { EdgeAnim, fadeInLeft } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' @@ -167,7 +167,7 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { return ( - + {`${currencyName} (${currencyCodeUppercase})`} diff --git a/src/components/scenes/CoinRankingScene.tsx b/src/components/scenes/CoinRankingScene.tsx index 6f2461f28f4..8027d6bbc71 100644 --- a/src/components/scenes/CoinRankingScene.tsx +++ b/src/components/scenes/CoinRankingScene.tsx @@ -212,6 +212,7 @@ const CoinRankingComponent = (props: Props) => { sceneWrapperInfo => { return ( { diff --git a/src/components/scenes/ConfirmScene.tsx b/src/components/scenes/ConfirmScene.tsx index 77a2d8b4a16..30eb01a3bb6 100644 --- a/src/components/scenes/ConfirmScene.tsx +++ b/src/components/scenes/ConfirmScene.tsx @@ -63,7 +63,7 @@ const ConfirmComponent = (props: Props) => { {renderInfoTiles()} - + diff --git a/src/components/scenes/CrashScene.tsx b/src/components/scenes/CrashScene.tsx index 6d65f353fd7..23ed186addb 100644 --- a/src/components/scenes/CrashScene.tsx +++ b/src/components/scenes/CrashScene.tsx @@ -1,12 +1,14 @@ import * as React from 'react' -import { Text } from 'react-native' +import { Platform, Text } from 'react-native' import AntDesignIcon from 'react-native-vector-icons/AntDesign' import { sprintf } from 'sprintf-js' import { lstrings } from '../../locales/strings' import { config } from '../../theme/appConfig' +import { openBrowserUri } from '../../util/WebUtils' import { SceneWrapper } from '../common/SceneWrapper' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' +import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' interface Props {} @@ -17,11 +19,16 @@ export function CrashScene(props: Props): React.ReactElement { const theme = useTheme() const styles = getStyles(theme) + const url = Platform.OS === 'ios' ? config.forceCloseUrlIos : config.forceCloseUrlAndroid + return ( {lstrings.error_boundary_title} - {sprintf(lstrings.error_boundary_message_s, config.supportEmail)} + {sprintf(lstrings.error_boundary_message_s, config.appNameShort)} + openBrowserUri(url) }} /> + {lstrings.error_boundary_message2} + openBrowserUri(config.supportContactSite) }} /> ) } diff --git a/src/components/scenes/CreateWalletAccountSelectScene.tsx b/src/components/scenes/CreateWalletAccountSelectScene.tsx index c22a5606e8b..478a5b26e5c 100644 --- a/src/components/scenes/CreateWalletAccountSelectScene.tsx +++ b/src/components/scenes/CreateWalletAccountSelectScene.tsx @@ -1,3 +1,4 @@ +import { EdgeCurrencyWallet } from 'edge-core-js' import * as React from 'react' import { View } from 'react-native' import { cacheStyles } from 'react-native-patina' @@ -11,11 +12,12 @@ import { getExchangeDenomination } from '../../selectors/DenominationSelectors' import { config } from '../../theme/appConfig' import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' -import { getTokenIdForced, getWalletTokenId } from '../../util/CurrencyInfoHelpers' +import { getWalletTokenId } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { logEvent } from '../../util/tracking' import { SceneWrapper } from '../common/SceneWrapper' import { IconDataRow } from '../data/row/IconDataRow' +import { withWallet } from '../hoc/withWallet' import { Airship, showError } from '../services/AirshipInstance' import { Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' @@ -23,7 +25,12 @@ import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' import { CardUi4 } from '../ui4/CardUi4' import { CryptoIconUi4 } from '../ui4/CryptoIconUi4' -export interface AccountPaymentParams { +export interface CreateWalletAccountSelectParams { + accountName: string + walletId: string +} + +export interface ActivationPaymentInfo { requestedAccountName: string currencyCode: string ownerPublicKey: string @@ -31,38 +38,36 @@ export interface AccountPaymentParams { requestedAccountCurrencyCode: string } -interface Props extends EdgeSceneProps<'createWalletAccountSelect'> {} +interface Props extends EdgeSceneProps<'createWalletAccountSelect'> { + wallet: EdgeCurrencyWallet +} -export const CreateWalletAccountSelectScene = (props: Props) => { - const { route } = props - const { selectedWalletType, accountName, existingWalletId } = route.params +export const CreateWalletAccountSelectScene = withWallet((props: Props) => { + const { route, wallet: existingWallet } = props + const { accountName } = route.params const dispatch = useDispatch() const theme = useTheme() const styles = getStyles(theme) + const { currencyCode: existingCurrencyCode, pluginId: existingPluginId } = existingWallet.currencyInfo const account = useSelector(state => state.core.account) - const currencyWallets = useSelector(state => state.core.account.currencyWallets) const supportedAssets = useSelector(state => state.ui.createWallet.handleActivationInfo.supportedAssets) const activationCost = useSelector(state => state.ui.createWallet.handleActivationInfo.activationCost) const paymentCurrencyCode = useSelector(state => state.ui.createWallet.walletAccountActivationPaymentInfo.currencyCode) const amount = useSelector(state => state.ui.createWallet.walletAccountActivationPaymentInfo.amount) - const existingCoreWallet = currencyWallets[existingWalletId] const paymentDenominationSymbol = useSelector(state => - paymentCurrencyCode != null && existingCoreWallet != null - ? getExchangeDenomination(state, existingCoreWallet.currencyInfo.pluginId, paymentCurrencyCode).symbol ?? '' - : '' + paymentCurrencyCode != null ? getExchangeDenomination(state, existingPluginId, paymentCurrencyCode).symbol ?? '' : '' ) const walletAccountActivationQuoteError = useSelector(state => state.ui.createWallet.walletAccountActivationQuoteError) const instructionSyntax = sprintf( lstrings.create_wallet_account_select_instructions_with_cost_4s, - selectedWalletType.currencyCode, - selectedWalletType.currencyCode, + existingCurrencyCode, + existingCurrencyCode, config.appNameShort, - `${activationCost} ${selectedWalletType.currencyCode}` + `${activationCost} ${existingCurrencyCode}` ) - const confirmMessageSyntax = sprintf(lstrings.create_wallet_account_make_payment_2s, selectedWalletType.currencyCode, existingCoreWallet.name) - const tokenId = getTokenIdForced(account, existingCoreWallet.currencyInfo.pluginId, selectedWalletType.currencyCode) + const confirmMessageSyntax = sprintf(lstrings.create_wallet_account_make_payment_2s, existingCurrencyCode, existingWallet.name) const [isCreatingWallet, setIsCreatingWallet] = React.useState(true) const [walletId, setWalletId] = React.useState('') @@ -72,9 +77,9 @@ export const CreateWalletAccountSelectScene = (props: Props) => { const paymentTokenId = paymentCurrencyCode === '' ? null : getWalletTokenId(paymentWallet, paymentCurrencyCode) const handleRenameAndReturnWallet = useHandler(async () => { - await existingCoreWallet.renameWallet(accountName) + await existingWallet.renameWallet(accountName) setIsCreatingWallet(false) - return existingCoreWallet + return existingWallet }) const handleSelect = useHandler(() => { @@ -87,12 +92,12 @@ export const CreateWalletAccountSelectScene = (props: Props) => { dispatch({ type: 'WALLET_ACCOUNT_ACTIVATION_ESTIMATE_ERROR', data: '' }) setWalletId(walletId) const createdWalletInstance = await handleRenameAndReturnWallet() - const paymentInfo: AccountPaymentParams = { + const paymentInfo: ActivationPaymentInfo = { requestedAccountName: accountName, currencyCode, ownerPublicKey: createdWalletInstance.publicWalletInfo.keys.ownerPublicKey, activePublicKey: createdWalletInstance.publicWalletInfo.keys.publicKey, - requestedAccountCurrencyCode: selectedWalletType.currencyCode + requestedAccountCurrencyCode: existingCurrencyCode } dispatch(fetchWalletAccountActivationPaymentInfo(paymentInfo, createdWalletInstance)) } @@ -108,14 +113,14 @@ export const CreateWalletAccountSelectScene = (props: Props) => { const handleCancel = useHandler(() => setWalletId('')) React.useEffect(() => { - logEvent('Activate_Wallet_Select') - dispatch(fetchAccountActivationInfo(selectedWalletType.walletType)).catch(err => showError(err)) - }, [selectedWalletType.walletType, dispatch]) + dispatch(logEvent('Activate_Wallet_Select')) + dispatch(fetchAccountActivationInfo(existingPluginId)).catch(err => showError(err)) + }, [existingPluginId, dispatch]) return ( - + {isRenderSelect ? instructionSyntax : confirmMessageSyntax} @@ -127,7 +132,7 @@ export const CreateWalletAccountSelectScene = (props: Props) => { {lstrings.create_wallet_account_amount_due} - {activationCost} {selectedWalletType.currencyCode} + {activationCost} {existingCurrencyCode} @@ -137,7 +142,7 @@ export const CreateWalletAccountSelectScene = (props: Props) => { leftText={getWalletName(paymentWallet)} leftSubtext={`${lstrings.send_confirmation_balance}: ${paymentWallet.balanceMap.get(paymentTokenId)} ${paymentCurrencyCode}`} rightText={`${paymentDenominationSymbol} ${amount} ${paymentCurrencyCode}`} - rightSubText={`≈ ${activationCost} ${selectedWalletType.currencyCode}`} + rightSubText={`≈ ${activationCost} ${existingCurrencyCode}`} /> )} @@ -146,6 +151,7 @@ export const CreateWalletAccountSelectScene = (props: Props) => { ) : ( <> @@ -163,7 +169,7 @@ export const CreateWalletAccountSelectScene = (props: Props) => { ) -} +}) const getStyles = cacheStyles((theme: Theme) => ({ createWalletPromptArea: { diff --git a/src/components/scenes/CreateWalletAccountSetupScene.tsx b/src/components/scenes/CreateWalletAccountSetupScene.tsx index f064dfeecda..3ceb3356b30 100644 --- a/src/components/scenes/CreateWalletAccountSetupScene.tsx +++ b/src/components/scenes/CreateWalletAccountSetupScene.tsx @@ -1,29 +1,40 @@ +import { EdgeCurrencyWallet } from 'edge-core-js' import * as React from 'react' import { View } from 'react-native' import { sprintf } from 'sprintf-js' +import { useHandler } from '../../hooks/useHandler' import { useMount } from '../../hooks/useMount' import { lstrings } from '../../locales/strings' -import { useSelector } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' -import { getTokenId } from '../../util/CurrencyInfoHelpers' import { logEvent } from '../../util/tracking' import { SceneWrapper } from '../common/SceneWrapper' +import { withWallet } from '../hoc/withWallet' import { FilledTextInput } from '../themed/FilledTextInput' import { MainButton } from '../themed/MainButton' import { ModalMessage } from '../themed/ModalParts' import { CryptoIconUi4 } from '../ui4/CryptoIconUi4' -interface Props extends EdgeSceneProps<'createWalletAccountSetup'> {} +export interface CreateWalletAccountSetupParams { + accountHandle?: string + isReactivation?: boolean + walletId: string +} + +interface Props extends EdgeSceneProps<'createWalletAccountSetup'> { + wallet: EdgeCurrencyWallet +} /** * Allows the user to choose an EOS handle. */ -export function CreateWalletAccountSetupScene(props: Props): JSX.Element { - const { navigation, route } = props - const { accountHandle: initialValue = '', existingWalletId, selectedWalletType } = route.params - const { currencyCode, pluginId } = selectedWalletType +export const CreateWalletAccountSetupScene = withWallet((props: Props) => { + const { navigation, route, wallet: existingWallet } = props + const { accountHandle: initialValue = '' } = route.params + const { currencyCode: existingCurrencyCode, pluginId: existingPluginId } = existingWallet.currencyInfo + const dispatch = useDispatch() const account = useSelector(state => state.core.account) const [errorMessage, setErrorMessage] = React.useState() const [spinning, setSpinning] = React.useState(false) @@ -35,13 +46,12 @@ export function CreateWalletAccountSetupScene(props: Props): JSX.Element { } async function checkHandle() { - const currencyPlugin = account.currencyConfig[pluginId] + const currencyPlugin = account.currencyConfig[existingPluginId] const data = await currencyPlugin.otherMethods.validateAccount(text) if (data.result === 'AccountAvailable') { navigation.navigate('createWalletAccountSelect', { - selectedWalletType, accountName: text, - existingWalletId + walletId: existingWallet.id }) } } @@ -62,19 +72,21 @@ export function CreateWalletAccountSetupScene(props: Props): JSX.Element { .finally(() => setSpinning(false)) } - const tokenId = getTokenId(account, pluginId, currencyCode) ?? null + const trackWalletActivate = useHandler(() => { + dispatch(logEvent('Activate_Wallet_Start')) + }) - useMount(() => logEvent('Activate_Wallet_Start')) + useMount(trackWalletActivate) return ( - + {/* This is an abuse of ModalMessage, but EdgeText breaks this text by setting numberOfLines. Switch to MessageText if we ever define that: */} - {sprintf(lstrings.create_wallet_account_review_instructions, currencyCode)} + {sprintf(lstrings.create_wallet_account_review_instructions, existingCurrencyCode)} {lstrings.create_wallet_account_requirements_eos} ) -} +}) diff --git a/src/components/scenes/CreateWalletCompletionScene.tsx b/src/components/scenes/CreateWalletCompletionScene.tsx index 016ab3aa6fc..ba30819fe09 100644 --- a/src/components/scenes/CreateWalletCompletionScene.tsx +++ b/src/components/scenes/CreateWalletCompletionScene.tsx @@ -1,15 +1,16 @@ -import { EdgeCurrencyWallet } from 'edge-core-js' +import { EdgeCreateCurrencyWallet } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator, View } from 'react-native' import { FlatList } from 'react-native-gesture-handler' import FontAwesome5 from 'react-native-vector-icons/FontAwesome5' import IonIcon from 'react-native-vector-icons/Ionicons' -import { createWallet, enableTokensAcrossWallets, PLACEHOLDER_WALLET_ID, splitCreateWalletItems } from '../../actions/CreateWalletActions' +import { createWallets, enableTokensAcrossWallets, PLACEHOLDER_WALLET_ID } from '../../actions/CreateWalletActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' +import { splitCreateWalletItems, WalletCreateItem } from '../../selectors/getCreateWalletList' import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { SceneWrapper } from '../common/SceneWrapper' @@ -20,7 +21,6 @@ import { CreateWalletSelectCryptoRow } from '../themed/CreateWalletSelectCryptoR import { EdgeText } from '../themed/EdgeText' import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' -import { WalletCreateItem } from '../themed/WalletList' export interface CreateWalletCompletionParams { createWalletList: WalletCreateItem[] @@ -49,23 +49,6 @@ const CreateWalletCompletionComponent = (props: Props) => { // We only want to render a single token row so we'll take the first one, if present, and use it in the renderRow logic and itemStatus map const tokenKey: string | undefined = newTokenItems[0]?.key - const walletPromises = React.useMemo Promise>>(() => { - return newWalletItems.map(item => { - return async () => - await createWallet(account, { - walletType: item.walletType, - walletName: walletNames[item.key], - fiatCurrencyCode: `iso:${fiatCode}`, - keyOptions: keyOptions.get(item.pluginId), - importText - }) - }) - }, [account, fiatCode, importText, keyOptions, newWalletItems, walletNames]) - - const tokenPromise = React.useMemo(() => { - return async () => await dispatch(enableTokensAcrossWallets(newTokenItems)) - }, [dispatch, newTokenItems]) - // Mainnet wallets first followed by our single token item, if necessary const filteredCreateItemsForDisplay = React.useMemo(() => { const items: WalletCreateItem[] = [...newWalletItems] @@ -88,28 +71,48 @@ const CreateWalletCompletionComponent = (props: Props) => { // Create the wallets and enable the tokens useAsyncEffect( async () => { - const promises: Array<(() => Promise) | (() => Promise)> = [...walletPromises] - if (tokenKey != null) promises.push(tokenPromise) - - for (const [i, promise] of promises.entries()) { - try { - const wallet = await promise() - // We created a wallet so let's Update relevant pending tokens with the new walletId - if (wallet != null) { - newTokenItems - .filter(item => item.pluginId === wallet.currencyInfo.pluginId && item.createWalletIds[0] === PLACEHOLDER_WALLET_ID) - .forEach(item => (item.createWalletIds = [wallet.id])) + // Create tokens on existing wallets: + let tokenPromise: Promise | undefined + if (tokenKey != null) { + tokenPromise = dispatch(enableTokensAcrossWallets(newTokenItems)).then( + () => setItemStatus(currentState => ({ ...currentState, [tokenKey]: 'complete' })), + error => { + showError(error) + setItemStatus(currentState => ({ ...currentState, [tokenKey]: 'error' })) } - setItemStatus(currentState => ({ ...currentState, [filteredCreateItemsForDisplay[i].key]: 'complete' })) - } catch (e) { - showError(e) + ) + } + + // Create new wallets in parallel: + const walletResults = await createWallets( + account, + newWalletItems.map( + (item): EdgeCreateCurrencyWallet => ({ + enabledTokenIds: newTokenItems + .filter(tokenItem => tokenItem.createWalletIds[0] === PLACEHOLDER_WALLET_ID && tokenItem.pluginId === item.pluginId) + .map(tokenItem => tokenItem.tokenId), + fiatCurrencyCode: `iso:${fiatCode}`, + importText, + keyOptions: keyOptions.get(item.pluginId), + name: walletNames[item.key], + walletType: item.walletType + }) + ) + ) + + // Check the wallet results: + for (let i = 0; i < walletResults.length; ++i) { + const result = walletResults[i] + if (!result.ok) { + showError(result.error) setItemStatus(currentState => ({ ...currentState, [filteredCreateItemsForDisplay[i].key]: 'error' })) + } else { + setItemStatus(currentState => ({ ...currentState, [filteredCreateItemsForDisplay[i].key]: 'complete' })) } - - flatListRef.current?.scrollToIndex({ animated: true, index: i, viewPosition: 0.5 }) } - setDone(true) + if (tokenPromise != null) await tokenPromise + setDone(true) return () => {} }, [], @@ -156,9 +159,8 @@ const CreateWalletCompletionComponent = (props: Props) => { disabled={!done} label={!done ? undefined : lstrings.string_done_cap} type="secondary" - marginRem={[0, 0, 0.5]} + marginRem={[0, 0, 1]} onPress={() => navigation.navigate('walletsTab', { screen: 'walletList' })} - alignSelf="center" /> ) @@ -180,7 +182,6 @@ const CreateWalletCompletionComponent = (props: Props) => { extraData={itemStatus} ref={flatListRef} renderItem={renderRow} - scrollEnabled={done} scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> {renderNextButton} diff --git a/src/components/scenes/CreateWalletImportOptionsScene.tsx b/src/components/scenes/CreateWalletImportOptionsScene.tsx index 76b425f51c0..c41899fd8e1 100644 --- a/src/components/scenes/CreateWalletImportOptionsScene.tsx +++ b/src/components/scenes/CreateWalletImportOptionsScene.tsx @@ -7,6 +7,7 @@ import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { ImportKeyOption, SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' +import { WalletCreateItem } from '../../selectors/getCreateWalletList' import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { FlatListItem } from '../../types/types' @@ -18,7 +19,6 @@ import { EdgeText } from '../themed/EdgeText' import { MainButton } from '../themed/MainButton' import { ModalMessage } from '../themed/ModalParts' import { SceneHeader } from '../themed/SceneHeader' -import { WalletCreateItem } from '../themed/WalletList' import { CryptoIconUi4 } from '../ui4/CryptoIconUi4' import { RowUi4 } from '../ui4/RowUi4' @@ -224,14 +224,7 @@ const CreateWalletImportOptionsComponent = (props: Props) => { renderItem={renderOptions} scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> - + ) diff --git a/src/components/scenes/CreateWalletImportScene.tsx b/src/components/scenes/CreateWalletImportScene.tsx index 974f3a8db9e..ab480f37359 100644 --- a/src/components/scenes/CreateWalletImportScene.tsx +++ b/src/components/scenes/CreateWalletImportScene.tsx @@ -4,12 +4,13 @@ import { View } from 'react-native' import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' import { sprintf } from 'sprintf-js' -import { PLACEHOLDER_WALLET_ID, splitCreateWalletItems } from '../../actions/CreateWalletActions' +import { PLACEHOLDER_WALLET_ID } from '../../actions/CreateWalletActions' import ImportKeySvg from '../../assets/images/import-key-icon.svg' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' +import { splitCreateWalletItems, WalletCreateItem } from '../../selectors/getCreateWalletList' import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { SceneWrapper } from '../common/SceneWrapper' @@ -20,7 +21,6 @@ import { EdgeText } from '../themed/EdgeText' import { FilledTextInput, FilledTextInputRef } from '../themed/FilledTextInput' import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' -import { WalletCreateItem } from '../themed/WalletList' export interface CreateWalletImportParams { createWalletList: WalletCreateItem[] @@ -165,7 +165,7 @@ const CreateWalletImportComponent = (props: Props) => { onSubmitEditing={handleNext} ref={textInputRef} /> - + ) diff --git a/src/components/scenes/CreateWalletSelectCryptoScene.tsx b/src/components/scenes/CreateWalletSelectCryptoScene.tsx index da4cc1d5df1..16df1118f6c 100644 --- a/src/components/scenes/CreateWalletSelectCryptoScene.tsx +++ b/src/components/scenes/CreateWalletSelectCryptoScene.tsx @@ -3,11 +3,18 @@ import { Keyboard, ListRenderItemInfo, Switch, View } from 'react-native' import { FlatList } from 'react-native-gesture-handler' import { sprintf } from 'sprintf-js' -import { enableTokensAcrossWallets, MainWalletCreateItem, PLACEHOLDER_WALLET_ID, splitCreateWalletItems } from '../../actions/CreateWalletActions' +import { enableTokensAcrossWallets, PLACEHOLDER_WALLET_ID } from '../../actions/CreateWalletActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' +import { + filterWalletCreateItemListBySearchText, + getCreateWalletList, + MainWalletCreateItem, + splitCreateWalletItems, + WalletCreateItem +} from '../../selectors/getCreateWalletList' import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps, NavigationProp } from '../../types/routerTypes' import { EdgeAsset } from '../../types/types' @@ -23,7 +30,6 @@ import { Fade } from '../themed/Fade' import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' import { SimpleTextInput } from '../themed/SimpleTextInput' -import { filterWalletCreateItemListBySearchText, getCreateWalletList, WalletCreateItem } from '../themed/WalletList' import { WalletListCurrencyRow } from '../themed/WalletListCurrencyRow' export interface CreateWalletSelectCryptoParams { @@ -72,38 +78,37 @@ const CreateWalletSelectCryptoComponent = (props: Props) => { ) const [selectedItems, setSelectedItems] = React.useState(() => { - return createWalletList.reduce((map: { [key: string]: boolean }, item) => { - const { key, pluginId, tokenId } = item - map[key] = defaultSelection.find(edgeTokenId => edgeTokenId.pluginId === pluginId && edgeTokenId.tokenId === tokenId) != null - if (item.walletType === 'wallet:bitcoin-bip44') map[key] = false // HACK: Make sure we don't select both bitcoin wallet choices - if (item.walletType === 'wallet:litecoin-bip44') map[key] = false // HACK: Make sure we don't select both litecoin wallet choices - return map - }, {}) + const out = new Set() + for (const asset of defaultSelection) { + const item = createWalletList.find(item => item.pluginId === asset.pluginId && item.tokenId === asset.tokenId) + if (item != null) out.add(item.key) + } + return out }) - const [numSelected, setNumSelected] = React.useState(Object.values(selectedItems).filter(Boolean).length) - - const createMainnetItem = useHandler(pluginId => { + const findMainnetItem = (pluginId: string): MainWalletCreateItem => { const newItem = createWalletList.find(item => item.pluginId === pluginId) return newItem as MainWalletCreateItem - }) + } const handleCreateWalletToggle = useHandler((key: string) => { - setSelectedItems({ ...selectedItems, [key]: !selectedItems[key] }) - - // Update the count with the new value - setNumSelected(!selectedItems[key] ? numSelected + 1 : numSelected - 1) + setSelectedItems(state => { + const copy = new Set(state) + if (copy.has(key)) copy.delete(key) + else copy.add(key) + return copy + }) }) const handleNextPress = useHandler(async () => { - if (numSelected === 0) { + if (selectedItems.size === 0) { showError(lstrings.create_wallet_no_assets_selected) return } - if (newAccountFlow != null) logEvent('Signup_Wallets_Selected_Next', { numSelectedWallets: numSelected }) + if (newAccountFlow != null) dispatch(logEvent('Signup_Wallets_Selected_Next', { numSelectedWallets: selectedItems.size })) - const createItems = createWalletList.filter(item => selectedItems[item.key]) + const createItems = createWalletList.filter(item => selectedItems.has(item.key)) const { newWalletItems, newTokenItems } = splitCreateWalletItems(createItems) // Filter duplicates @@ -112,15 +117,15 @@ const CreateWalletSelectCryptoComponent = (props: Props) => { const existingWalletIds = [...(pluginIdWalletIdsMap[pluginId] ?? [])] // Determine if the user selected a new wallet for this pluginId. - const newItem = createMainnetItem(pluginId) - if (selectedItems[newItem.key]) { + const newItem = findMainnetItem(pluginId) + if (selectedItems.has(newItem.key)) { existingWalletIds.push(PLACEHOLDER_WALLET_ID) } if (existingWalletIds.length === 0) { // If the user hasn't selected the parent wallet to create, add it for them if (!newWalletItems.some(item => item.pluginId === pluginId)) { - const newItem = createMainnetItem(pluginId) + const newItem = findMainnetItem(pluginId) newWalletItems.push(newItem) } newTokenItems.forEach(item => { @@ -211,17 +216,18 @@ const CreateWalletSelectCryptoComponent = (props: Props) => { const { key, displayName, pluginId, tokenId } = item.item const accessibilityHint = sprintf(lstrings.create_wallet_hint, displayName) + const selected = selectedItems.has(key) const toggle = ( handleCreateWalletToggle(key)} /> ) @@ -241,13 +247,13 @@ const CreateWalletSelectCryptoComponent = (props: Props) => { const renderNextButton = React.useMemo( () => ( - 0} visible={numSelected > 0} duration={300}> + 0} visible={selectedItems.size > 0} duration={300}> - + ), - [defaultSelection, handleNextPress, numSelected, styles.bottomButton] + [defaultSelection, handleNextPress, selectedItems, styles.bottomButton] ) return ( diff --git a/src/components/scenes/CreateWalletSelectFiatScene.tsx b/src/components/scenes/CreateWalletSelectFiatScene.tsx index 831b5b74c60..5906c526dfc 100644 --- a/src/components/scenes/CreateWalletSelectFiatScene.tsx +++ b/src/components/scenes/CreateWalletSelectFiatScene.tsx @@ -5,13 +5,14 @@ import { FlatList } from 'react-native-gesture-handler' import IonIcon from 'react-native-vector-icons/Ionicons' import { sprintf } from 'sprintf-js' -import { createWallet, enableTokensAcrossWallets, getUniqueWalletName, splitCreateWalletItems } from '../../actions/CreateWalletActions' +import { createWallet, enableTokensAcrossWallets, getUniqueWalletName } from '../../actions/CreateWalletActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { FIAT_COUNTRY } from '../../constants/CountryConstants' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' +import { splitCreateWalletItems, WalletCreateItem } from '../../selectors/getCreateWalletList' import { getDefaultFiat } from '../../selectors/SettingsSelectors' import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' @@ -30,7 +31,6 @@ import { EdgeText } from '../themed/EdgeText' import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' import { SelectableRow } from '../themed/SelectableRow' -import { WalletCreateItem } from '../themed/WalletList' export interface CreateWalletSelectFiatParams { createWalletList: WalletCreateItem[] @@ -80,11 +80,15 @@ const CreateWalletSelectFiatComponent = (props: Props) => { if (newWalletItems.length === 1 && newTokenItems.length === 0) { const item = newWalletItems[0] try { - await createWallet(account, { walletType: item.walletType, walletName: walletNames[item.key], fiatCurrencyCode: `iso:${fiat.value}` }) - logEvent('Create_Wallet_Success') + await createWallet(account, { + fiatCurrencyCode: `iso:${fiat.value}`, + name: walletNames[item.key], + walletType: item.walletType + }) + dispatch(logEvent('Create_Wallet_Success')) } catch (error: any) { showError(error) - logEvent('Create_Wallet_Failed', { error: String(error) }) + dispatch(logEvent('Create_Wallet_Failed', { error: String(error) })) } navigation.navigate('walletsTab', { screen: 'walletList' }) return @@ -224,8 +228,8 @@ const CreateWalletSelectFiatComponent = (props: Props) => { renderItem={renderCurrencyRow} scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> - - + + ) @@ -234,6 +238,7 @@ const CreateWalletSelectFiatComponent = (props: Props) => { const getStyles = cacheStyles((theme: Theme) => ({ content: { flex: 1, + margin: theme.rem(0.5), paddingTop: theme.rem(0.5) }, cryptoTypeLogo: { diff --git a/src/components/scenes/CryptoExchangeQuoteScene.tsx b/src/components/scenes/CryptoExchangeQuoteScene.tsx index 0877fb8e13b..470b6866957 100644 --- a/src/components/scenes/CryptoExchangeQuoteScene.tsx +++ b/src/components/scenes/CryptoExchangeQuoteScene.tsx @@ -13,7 +13,7 @@ import { EdgeSceneProps } from '../../types/routerTypes' import { getSwapPluginIconUri } from '../../util/CdnUris' import { logEvent } from '../../util/tracking' import { PoweredByCard } from '../cards/PoweredByCard' -import { EdgeAnim } from '../common/EdgeAnim' +import { EdgeAnim, fadeInDown30, fadeInDown60, fadeInDown90, fadeInDown120, fadeInUp30, fadeInUp60, fadeInUp90 } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' import { SwapProviderRow } from '../data/row/SwapProviderRow' import { ButtonsModal } from '../modals/ButtonsModal' @@ -95,7 +95,7 @@ export const CryptoExchangeQuoteScene = (props: Props) => { useEffect(() => { const swapConfig = account.swapConfig[pluginId] - logEvent('Exchange_Shift_Quote') + dispatch(logEvent('Exchange_Shift_Quote')) swapVerifyTerms(swapConfig) .then(async result => { if (!result) await dispatch(exchangeTimerExpired(navigation, selectedQuote, onApprove)) @@ -186,35 +186,35 @@ export const CryptoExchangeQuoteScene = (props: Props) => { return ( - + {showFeeWarning ? ( - + ) : null} - + - + - + {selectedQuote.isEstimate ? ( - + ) : null} {selectedQuote.canBePartial ? ( - + { ) : null} - + {renderTimer()} diff --git a/src/components/scenes/CryptoExchangeScene.tsx b/src/components/scenes/CryptoExchangeScene.tsx index 86a93f279f5..c3185a8c3f6 100644 --- a/src/components/scenes/CryptoExchangeScene.tsx +++ b/src/components/scenes/CryptoExchangeScene.tsx @@ -1,7 +1,7 @@ import { div, gt, gte } from 'biggystring' import { EdgeAccount, EdgeTokenId } from 'edge-core-js' import * as React from 'react' -import { Keyboard, View } from 'react-native' +import { Keyboard } from 'react-native' import { sprintf } from 'sprintf-js' import { getQuoteForTransaction, selectWalletForExchange, SetNativeAmountInfo } from '../../actions/CryptoExchangeActions' @@ -16,7 +16,7 @@ import { emptyCurrencyInfo, GuiCurrencyInfo } from '../../types/types' import { getTokenId, getWalletTokenId } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { DECIMAL_PRECISION, zeroString } from '../../util/utils' -import { EdgeAnim } from '../common/EdgeAnim' +import { EdgeAnim, fadeInDown30, fadeInDown60, fadeInDown90, fadeInUp60, fadeInUp90 } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' import { WalletListModal, WalletListResult } from '../modals/WalletListModal' import { Airship, showError, showWarning } from '../services/AirshipInstance' @@ -24,10 +24,10 @@ import { cacheStyles, Theme, ThemeProps, useTheme } from '../services/ThemeConte import { CryptoExchangeFlipInputWrapper } from '../themed/CryptoExchangeFlipInputWrapperComponent' import { ExchangedFlipInputAmounts } from '../themed/ExchangedFlipInput2' import { LineTextDivider } from '../themed/LineTextDivider' -import { MainButton } from '../themed/MainButton' import { MiniButton } from '../themed/MiniButton' import { SceneHeader } from '../themed/SceneHeader' import { AlertCardUi4 } from '../ui4/AlertCardUi4' +import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' interface OwnProps extends EdgeSceneProps<'exchange'> {} @@ -236,7 +236,7 @@ export class CryptoExchangeComponent extends React.Component { const showNext = this.props.fromCurrencyCode !== '' && this.props.toCurrencyCode !== '' && !!parseFloat(primaryNativeAmount) if (!showNext) return null if (this.checkExceedsAmount()) return null - return + return } renderAlert = () => { @@ -293,11 +293,11 @@ export class CryptoExchangeComponent extends React.Component { const toHeaderText = sprintf(lstrings.exchange_to_wallet, toWalletName) return ( - - + <> + - + { focusMe={this.focusFromWallet} onNext={this.handleNext} > - {this.props.hasMaxSpend ? : null} + {this.props.hasMaxSpend ? ( + + ) : null} - + { onNext={this.handleNext} /> - {this.renderAlert()} - {this.renderButton()} - + {this.renderAlert()} + {this.renderButton()} + ) } } const getStyles = cacheStyles((theme: Theme) => ({ - sceneContainer: { - marginHorizontal: theme.rem(0.5), - flex: 1 - }, mainScrollView: { flex: 1 }, @@ -431,7 +429,7 @@ export const CryptoExchangeScene = (props: OwnProps) => { }) return ( - + {} export const CurrencyNotificationScene = (props: Props) => { diff --git a/src/components/scenes/CurrencySettingsScene.tsx b/src/components/scenes/CurrencySettingsScene.tsx index efcec05d47b..c507e22e453 100644 --- a/src/components/scenes/CurrencySettingsScene.tsx +++ b/src/components/scenes/CurrencySettingsScene.tsx @@ -1,3 +1,4 @@ +import { EdgeCurrencyInfo } from 'edge-core-js' import * as React from 'react' import { Text } from 'react-native' @@ -13,6 +14,10 @@ import { SettingsRadioRow } from '../settings/SettingsRadioRow' import { MaybeBlockbookSetting, MaybeCustomServersSetting, MaybeElectrumSetting } from '../themed/MaybeCustomServersSetting' import { MaybeMoneroUserSettings } from '../themed/MaybeMoneroUserSettings' +export interface CurrencySettingsParams { + currencyInfo: EdgeCurrencyInfo +} + interface Props extends EdgeSceneProps<'currencySettings'> {} export function CurrencySettingsScene(props: Props) { diff --git a/src/components/scenes/DefaultFiatSettingScene.tsx b/src/components/scenes/DefaultFiatSettingScene.tsx index 38ca2f39285..e4967af9a29 100644 --- a/src/components/scenes/DefaultFiatSettingScene.tsx +++ b/src/components/scenes/DefaultFiatSettingScene.tsx @@ -100,6 +100,7 @@ export class DefaultFiatSettingComponent extends React.Component { automaticallyAdjustContentInsets={false} contentContainerStyle={{ ...insetStyle, paddingTop: 0 }} data={filteredArray} + keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" keyExtractor={this.keyExtractor} renderItem={this.renderFiatTypeResult} diff --git a/src/components/scenes/DevTestScene.tsx b/src/components/scenes/DevTestScene.tsx index cac20cf55ea..95d5fbae9c5 100644 --- a/src/components/scenes/DevTestScene.tsx +++ b/src/components/scenes/DevTestScene.tsx @@ -45,6 +45,7 @@ export function DevTestScene(props: Props) { const [filledTextInputValue5, setFilledTextInputValue5] = useState('') const [filledTextInputValue6, setFilledTextInputValue6] = useState('') const [filledTextInputValue7, setFilledTextInputValue7] = useState('') + const [filledTextInputValue8, setFilledTextInputValue8] = useState('') const walletId = selectedWallet?.wallet.id ?? '' const tokenId = selectedWallet?.tokenId ?? null const exchangedFlipInputRef = React.useRef(null) @@ -164,6 +165,17 @@ export function DevTestScene(props: Props) { error="Error" maxLength={100} /> + <> + + Ensure errors above don't push me down + {}} label="Mini" marginRem={0.5} type="secondary" mini /> + ButtonsViews {} }} @@ -298,6 +311,17 @@ export function DevTestScene(props: Props) { {} }} secondary2={{ label: 'Secondary', onPress: () => {} }} layout="row" /> + + Loose Buttons (0.5rem margin) + + {}} label="Mini" type="secondary" mini /> + {}} label="Mini" type="secondary" mini /> + + + {}} label="Primary" type="primary" /> + {}} label="Secondary" type="secondary" /> + {}} label="Tertiary" type="tertiary" /> + @@ -306,5 +330,7 @@ export function DevTestScene(props: Props) { const OutlinedView = styled(View)({ borderWidth: 1, - borderColor: 'white' + borderColor: 'white', + alignItems: 'center', + justifyContent: 'center' }) diff --git a/src/components/scenes/EdgeLoginScene.tsx b/src/components/scenes/EdgeLoginScene.tsx index d125c5095e9..f812c568650 100644 --- a/src/components/scenes/EdgeLoginScene.tsx +++ b/src/components/scenes/EdgeLoginScene.tsx @@ -19,6 +19,10 @@ import { TitleText } from '../text/TitleText' import { Fade } from '../themed/Fade' import { MainButton } from '../themed/MainButton' +export interface EdgeLoginParams { + lobbyId: string +} + interface Props extends EdgeSceneProps<'edgeLogin'> {} export const EdgeLoginScene = (props: Props) => { diff --git a/src/components/scenes/EditTokenScene.tsx b/src/components/scenes/EditTokenScene.tsx index fae1372c259..c43da0e47de 100644 --- a/src/components/scenes/EditTokenScene.tsx +++ b/src/components/scenes/EditTokenScene.tsx @@ -1,5 +1,5 @@ import { asMaybe, asObject, asString } from 'cleaners' -import { EdgeCurrencyWallet, EdgeToken } from 'edge-core-js' +import { EdgeCurrencyWallet, EdgeToken, EdgeTokenId, JsonObject } from 'edge-core-js' import * as React from 'react' import { ScrollView } from 'react-native' import { sprintf } from 'sprintf-js' @@ -22,6 +22,15 @@ import { FilledTextInput } from '../themed/FilledTextInput' import { SceneHeader } from '../themed/SceneHeader' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' +export interface EditTokenParams { + currencyCode?: string + displayName?: string + multiplier?: string + networkLocation?: JsonObject + tokenId?: EdgeTokenId // Acts like "add token" if this is missing + walletId: string +} + interface Props extends EdgeSceneProps<'editToken'> { wallet: EdgeCurrencyWallet } diff --git a/src/components/scenes/Fio/FioAddressListScene.tsx b/src/components/scenes/Fio/FioAddressListScene.tsx index f09f9c80e3e..e2ff9ccaf0a 100644 --- a/src/components/scenes/Fio/FioAddressListScene.tsx +++ b/src/components/scenes/Fio/FioAddressListScene.tsx @@ -11,7 +11,7 @@ import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { FioAddress, FioDomain } from '../../../types/types' -import { EdgeAnim } from '../../common/EdgeAnim' +import { EdgeAnim, fadeIn, fadeOut } from '../../common/EdgeAnim' import { SceneWrapper } from '../../common/SceneWrapper' import { FioNameRow } from '../../FioAddress/FioName' import { FullScreenLoader } from '../../progress-indicators/FullScreenLoader' @@ -145,7 +145,7 @@ export class FioAddressList extends React.Component { /> ))} - + diff --git a/src/components/scenes/Fio/FioAddressRegisterScene.tsx b/src/components/scenes/Fio/FioAddressRegisterScene.tsx index de12e480677..5836ee6b115 100644 --- a/src/components/scenes/Fio/FioAddressRegisterScene.tsx +++ b/src/components/scenes/Fio/FioAddressRegisterScene.tsx @@ -12,7 +12,7 @@ import { FioDomain, FioPublicDomain } from '../../../types/types' import { getWalletName } from '../../../util/CurrencyWalletHelpers' import { checkIsDomainPublic } from '../../../util/FioAddressUtils' import { openLink } from '../../../util/utils' -import { EdgeAnim } from '../../common/EdgeAnim' +import { EdgeAnim, fadeIn, fadeOut } from '../../common/EdgeAnim' import { SceneWrapper } from '../../common/SceneWrapper' import { DomainListModal } from '../../FioAddress/DomainListModal' import { TextInputModal } from '../../modals/TextInputModal' @@ -381,7 +381,7 @@ export class FioAddressRegister extends React.Component { } return ( - + ) diff --git a/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx b/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx index e6ab4d11ca6..55df00304ad 100644 --- a/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx +++ b/src/components/scenes/Fio/FioAddressRegisterSelectWalletScene.tsx @@ -15,6 +15,7 @@ import { EdgeAsset } from '../../../types/types' import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' import { getWalletName } from '../../../util/CurrencyWalletHelpers' import { getRegInfo } from '../../../util/FioAddressUtils' +import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' import { WalletListModal, WalletListResult } from '../../modals/WalletListModal' import { Airship, showError } from '../../services/AirshipInstance' @@ -39,6 +40,7 @@ interface OwnProps extends EdgeSceneProps<'fioAddressRegisterSelectWallet'> {} interface DispatchProps { onSelectWallet: (walletId: string, currencyCode: string) => void + onLogEvent: (event: TrackingEventName, values: TrackingValues) => void } interface LocalState { @@ -131,7 +133,7 @@ export class FioAddressRegisterSelectWallet extends React.Component { - const { isConnected, state, navigation, route } = this.props + const { isConnected, state, navigation, route, onLogEvent } = this.props const { selectedWallet, fioAddress } = route.params const { feeValue, paymentInfo: allPaymentInfo } = this.state const { account } = state.core @@ -152,7 +154,8 @@ export class FioAddressRegisterSelectWallet extends React.Component { const styles = getStyles(theme) return ( - + {`${lstrings.fio_address_details_screen_expires} `} {formatDate(new Date(expiration))} - + ) } @@ -43,11 +42,11 @@ export class FioAddressRegistered extends React.Component { - {lstrings.fio_address_details_screen_registered} - {fioName} + {lstrings.fio_address_details_screen_registered} + {fioName} {this.renderExpDate()} - navigation.navigate('fioAddressList', {})} label={lstrings.title_fio_names} /> + navigation.navigate('fioAddressList', {})} label={lstrings.title_fio_names} /> ) @@ -70,6 +69,7 @@ const getStyles = cacheStyles((theme: Theme) => ({ }, text: { color: theme.primaryText, + fontFamily: theme.fontFaceDefault, fontSize: theme.rem(1) }, image: { diff --git a/src/components/scenes/Fio/FioAddressSettingsScene.tsx b/src/components/scenes/Fio/FioAddressSettingsScene.tsx index 73a5249077d..90ef2dbdd6a 100644 --- a/src/components/scenes/Fio/FioAddressSettingsScene.tsx +++ b/src/components/scenes/Fio/FioAddressSettingsScene.tsx @@ -7,6 +7,7 @@ import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { addBundledTxs, getAddBundledTxsFee, getTransferFee } from '../../../util/FioAddressUtils' +import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' import { FioActionSubmit } from '../../FioAddress/FioActionSubmit' import { ButtonsModal } from '../../modals/ButtonsModal' @@ -30,12 +31,16 @@ interface StateProps { interface DispatchProps { refreshAllFioAddresses: () => Promise + onLogEvent: (event: TrackingEventName, values: TrackingValues) => void } interface OwnProps extends EdgeSceneProps<'fioAddressSettings'> {} type Props = StateProps & DispatchProps & ThemeProps & OwnProps +/** + * FIO "Reload and Transfer" scene. + */ export class FioAddressSettingsComponent extends React.Component { state: LocalState = { showAddBundledTxs: false, @@ -90,14 +95,17 @@ export class FioAddressSettingsComponent extends React.Component await getTransferFee(fioWallet) onAddBundledTxsSubmit = async (fioWallet: EdgeCurrencyWallet, fee: number) => { - const { isConnected, route } = this.props + const { isConnected, route, onLogEvent } = this.props const { fioAddressName } = route.params if (!isConnected) { showError(lstrings.fio_network_alert_text) return } - return await addBundledTxs(fioWallet, fioAddressName, fee) + await addBundledTxs(fioWallet, fioAddressName, fee) + + const { currencyCode, pluginId } = fioWallet.currencyInfo + onLogEvent('Fio_Handle_Bundled_Tx', { nativeAmount: String(fee), pluginId, currencyCode }) } goToTransfer = (params: { fee: number }) => { @@ -205,6 +213,9 @@ export const FioAddressSettingsScene = connect ({ async refreshAllFioAddresses() { await dispatch(refreshAllFioAddresses()) + }, + onLogEvent(event: TrackingEventName, values: TrackingValues) { + dispatch(logEvent(event, values)) } }) )(withTheme(FioAddressSettingsComponent)) diff --git a/src/components/scenes/Fio/FioCreateHandleScene.tsx b/src/components/scenes/Fio/FioCreateHandleScene.tsx index aaccd4ffc2d..9616d0c8283 100644 --- a/src/components/scenes/Fio/FioCreateHandleScene.tsx +++ b/src/components/scenes/Fio/FioCreateHandleScene.tsx @@ -16,6 +16,7 @@ import { lstrings } from '../../../locales/strings' import { useDispatch, useSelector } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { getFioCustomizeHandleImage } from '../../../util/CdnUris' +import { logEvent } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' import { showError, showToast } from '../../services/AirshipInstance' import { Theme, useTheme } from '../../services/ThemeContext' @@ -45,6 +46,9 @@ const asFreeFioDomain = asObject({ free: asValue(true) }) +/** + * 'Personalize Your Wallet' Scene for creating a free FIO handles for new users (via FioCreateHandleModal) + */ export const FioCreateHandleScene = (props: Props) => { const { navigation, route } = props const { freeRegApiToken, freeRegRefCode } = route.params @@ -125,6 +129,9 @@ export const FioCreateHandleScene = (props: Props) => { await dispatch(refreshAllFioAddresses()) showToast(lstrings.fio_free_handle_complete) + + await dispatch(logEvent('Fio_Handle_Register', { dollarValue: 3 })) + navigation.pop() } catch (e: any) { // Rejected somehow, see if error is readable diff --git a/src/components/scenes/Fio/FioDomainRegisterScene.tsx b/src/components/scenes/Fio/FioDomainRegisterScene.tsx index 845b6582351..5aabaa89183 100644 --- a/src/components/scenes/Fio/FioDomainRegisterScene.tsx +++ b/src/components/scenes/Fio/FioDomainRegisterScene.tsx @@ -8,7 +8,7 @@ import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { getWalletName } from '../../../util/CurrencyWalletHelpers' -import { EdgeAnim } from '../../common/EdgeAnim' +import { EdgeAnim, fadeIn, fadeOut } from '../../common/EdgeAnim' import { SceneWrapper } from '../../common/SceneWrapper' import { TextInputModal } from '../../modals/TextInputModal' import { WalletListModal, WalletListResult } from '../../modals/WalletListModal' @@ -197,9 +197,9 @@ export class FioDomainRegister extends React.PureComponent { if (isValid && isAvailable && !loading) { return ( - + { - + diff --git a/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx b/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx index ab395ea98f1..b06ce13ffdb 100644 --- a/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx +++ b/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx @@ -16,6 +16,7 @@ import { EdgeAsset } from '../../../types/types' import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' import { getWalletName } from '../../../util/CurrencyWalletHelpers' import { getDomainRegInfo } from '../../../util/FioAddressUtils' +import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' import { ButtonsModal } from '../../modals/ButtonsModal' import { WalletListModal, WalletListResult } from '../../modals/WalletListModal' @@ -42,6 +43,7 @@ interface OwnProps extends EdgeSceneProps<'fioDomainRegisterSelectWallet'> {} interface DispatchProps { onSelectWallet: (walletId: string, currencyCode: string) => void + onLogEvent: (event: TrackingEventName, values: TrackingValues) => void } interface LocalState { @@ -121,7 +123,7 @@ class FioDomainRegisterSelectWallet extends React.PureComponent { - const { isConnected, state, navigation, route } = this.props + const { isConnected, state, navigation, route, onLogEvent: logEvent } = this.props const { fioDomain, selectedWallet } = route.params const { feeValue, paymentInfo: allPaymentInfo, paymentWallet } = this.state const { account } = state.core @@ -185,6 +187,7 @@ class FioDomainRegisterSelectWallet extends React.PureComponent )).catch(err => showError(err)) + logEvent('Fio_Domain_Register', { exchangeAmount: String(feeValue), currencyCode: paymentWallet.currencyCode }) navigation.navigate('homeTab', { screen: 'home' }) } } @@ -233,7 +236,7 @@ class FioDomainRegisterSelectWallet extends React.PureComponent {!loading && paymentWallet && paymentWallet.id && ( - + )} {errorMessage != null && } @@ -286,6 +289,9 @@ export const FioDomainRegisterSelectWalletScene = connect Promise + onLogEvent: (event: TrackingEventName, values: TrackingValues) => void } interface OwnProps extends EdgeSceneProps<'fioDomainSettings'> {} @@ -107,7 +109,7 @@ export class FioDomainSettingsComponent extends React.Component { } renewDomain = async (fioWallet: EdgeCurrencyWallet, renewalFee: number) => { - const { isConnected, route } = this.props + const { isConnected, route, onLogEvent } = this.props const { fioDomainName } = route.params if (!isConnected) { @@ -115,6 +117,13 @@ export class FioDomainSettingsComponent extends React.Component { } await renewFioDomain(fioWallet, fioDomainName, renewalFee) + + const { currencyCode, pluginId } = fioWallet.currencyInfo + onLogEvent('Fio_Domain_Renew', { + nativeAmount: String(renewalFee), + currencyCode, + pluginId + }) } goToTransfer = (params: { fee: number }) => { @@ -242,6 +251,9 @@ export const FioDomainSettingsScene = connect ({ async refreshAllFioAddresses() { await dispatch(refreshAllFioAddresses()) + }, + onLogEvent(event: TrackingEventName, values: TrackingValues) { + dispatch(logEvent(event, values)) } }) )(withTheme(FioDomainSettingsComponent)) diff --git a/src/components/scenes/Fio/FioNameConfirmScene.tsx b/src/components/scenes/Fio/FioNameConfirmScene.tsx index 40118d2e83d..c9552853243 100644 --- a/src/components/scenes/Fio/FioNameConfirmScene.tsx +++ b/src/components/scenes/Fio/FioNameConfirmScene.tsx @@ -7,6 +7,7 @@ import { lstrings } from '../../../locales/strings' import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { fioMakeSpend, fioSignAndBroadcast } from '../../../util/FioAddressUtils' +import { logEvent, TrackingEventName, TrackingValues } from '../../../util/tracking' import { SceneWrapper } from '../../common/SceneWrapper' import { FioActionSubmit } from '../../FioAddress/FioActionSubmit' import { ButtonsModal } from '../../modals/ButtonsModal' @@ -23,7 +24,11 @@ interface StateProps { interface OwnProps extends EdgeSceneProps<'fioDomainConfirm' | 'fioNameConfirm'> {} -type Props = StateProps & OwnProps & ThemeProps +interface DispatchProps { + onLogEvent: (event: TrackingEventName, values: TrackingValues) => void +} + +type Props = StateProps & OwnProps & ThemeProps & DispatchProps const ONE_FREE_ADDRESS_PER_DOMAIN_ERROR = 'ONE_FREE_ADDRESS_PER_DOMAIN_ERROR' @@ -39,7 +44,7 @@ class FioNameConfirm extends React.PureComponent { } saveFioName = async () => { - const { navigation, route } = this.props + const { navigation, route, onLogEvent } = this.props const { fioName, paymentWallet, ownerPublicKey, fee } = route.params const { isConnected, fioPlugin } = this.props @@ -106,10 +111,18 @@ class FioNameConfirm extends React.PureComponent { } } else { try { + const { currencyCode, pluginId } = paymentWallet.currencyInfo if (this.isFioAddress()) { let edgeTx = await fioMakeSpend(paymentWallet, 'registerFioAddress', { fioAddress: fioName }) edgeTx = await fioSignAndBroadcast(paymentWallet, edgeTx) await paymentWallet.saveTx(edgeTx) + + onLogEvent('Fio_Handle_Register', { + nativeAmount: edgeTx.nativeAmount, + currencyCode, + pluginId + }) + // @ts-expect-error window.requestAnimationFrame(() => navigation.navigate('fioAddressRegisterSuccess', { @@ -121,6 +134,13 @@ class FioNameConfirm extends React.PureComponent { edgeTx = await fioSignAndBroadcast(paymentWallet, edgeTx) await paymentWallet.saveTx(edgeTx) const expiration = edgeTx.otherParams?.broadcastResult?.expiration + + onLogEvent('Fio_Domain_Register', { + nativeAmount: edgeTx.nativeAmount, + currencyCode, + pluginId + }) + // @ts-expect-error window.requestAnimationFrame(() => navigation.navigate('fioAddressRegisterSuccess', { @@ -174,10 +194,14 @@ const getStyles = cacheStyles((theme: Theme) => ({ } })) -export const FioNameConfirmScene = connect( +export const FioNameConfirmScene = connect( state => ({ fioPlugin: state.core.account.currencyConfig.fio, isConnected: state.network.isConnected }), - dispatch => ({}) + dispatch => ({ + onLogEvent(event: TrackingEventName, values: TrackingValues) { + dispatch(logEvent(event, values)) + } + }) )(withTheme(FioNameConfirm)) diff --git a/src/components/scenes/Fio/FioRequestConfirmationScene.tsx b/src/components/scenes/Fio/FioRequestConfirmationScene.tsx index 57aa09eca78..d60eefe1bc4 100644 --- a/src/components/scenes/Fio/FioRequestConfirmationScene.tsx +++ b/src/components/scenes/Fio/FioRequestConfirmationScene.tsx @@ -11,7 +11,7 @@ import { getExchangeRate, getSelectedCurrencyWallet } from '../../../selectors/W import { connect } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { emptyCurrencyInfo, GuiCurrencyInfo } from '../../../types/types' -import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' +import { getTokenIdForced, getWalletTokenId } from '../../../util/CurrencyInfoHelpers' import { addToFioAddressCache, checkPubAddress, @@ -113,7 +113,7 @@ export class FioRequestConfirmationConnected extends React.Component { - const { fioPlugin, primaryCurrencyInfo, isConnected, edgeWallet, chainCode, account, navigation, route } = this.props + const { account, chainCode, currencyCode, edgeWallet, fioPlugin, isConnected, navigation, primaryCurrencyInfo, route } = this.props const { amounts } = route.params const { walletAddresses, fioAddressFrom } = this.state const walletAddress = walletAddresses.find(({ fioAddress }) => fioAddress === fioAddressFrom) @@ -197,7 +197,8 @@ export class FioRequestConfirmationConnected extends React.Component {} interface SectionData { @@ -51,75 +54,82 @@ interface SectionData { message: string title: string } -const sections: SectionData[] = [ - { - image: uspImage0, - key: 'slide1', - message: lstrings.getting_started_slide_1_message, - title: lstrings.getting_started_slide_1_title, - footnote: lstrings.getting_started_slide_1_footnote - }, - { - image: uspImage1, - key: 'slide2', - message: lstrings.getting_started_slide_2_message, - title: lstrings.getting_started_slide_2_title - }, - { - image: uspImage2, - key: 'slide3', - message: lstrings.getting_started_slide_3_message, - title: lstrings.getting_started_slide_3_title - }, - { - image: uspImage3, - key: 'slide4', - message: lstrings.getting_started_slide_4_message, - title: lstrings.getting_started_slide_4_title - } -] + +const slide1 = { + image: uspImage0, + key: 'slide1', + message: lstrings.getting_started_slide_1_message, + title: lstrings.getting_started_slide_1_title, + footnote: lstrings.getting_started_slide_1_footnote +} +const slide1Alt = { + image: uspImage0, + key: 'slide1Alt', + message: lstrings.getting_started_slide_1_message_alt, + title: lstrings.getting_started_slide_1_title +} +const slide2 = { + image: uspImage1, + key: 'slide2', + message: lstrings.getting_started_slide_2_message, + title: lstrings.getting_started_slide_2_title +} +const slide3 = { + image: uspImage2, + key: 'slide3', + message: lstrings.getting_started_slide_3_message, + title: lstrings.getting_started_slide_3_title +} +const slide4 = { + image: uspImage3, + key: 'slide4', + message: lstrings.getting_started_slide_4_message, + title: lstrings.getting_started_slide_4_title +} + +const sectionsVariantMap: { [key: string]: SectionData[] } = { + default: [slide1, slide2, slide3, slide4], + C_UspsMinusWGYC: [slide2, slide3, slide4], + D_UspsAltWGYC: [slide1Alt, slide2, slide3, slide4] +} export const GettingStartedScene = (props: Props) => { - const { navigation } = props + const { navigation, route } = props + const dispatch = useDispatch() + const { experimentConfig } = route.params + const { createAccountType, landingType } = experimentConfig const context = useSelector(state => state.core.context) const isLoggedIn = useSelector(state => state.ui.settings.settingsLoaded ?? false) const localUsers = useWatch(context, 'localUsers') const hasLocalUsers = localUsers.length > 0 - const [isFinalSwipeEnabled, setIsFinalSwipeEnabled] = React.useState(true) - const [createAccountType, setCreateAccountType] = React.useState('full') + const sections: SectionData[] = sectionsVariantMap[landingType] ?? sectionsVariantMap.default // An extra index is added to account for the extra initial usp slide OR to // allow the SwipeOffsetDetector extra room for the user to swipe beyond to // trigger the final navigation. - // Feature is under A/B testing. - const paginationCount = sections.length + (isFinalSwipeEnabled ? 1 : 0) + const paginationCount = sections.length + 1 const swipeOffset = useSharedValue(0) // Route helpers - const getNewAccountRoute = (createAccountType: CreateAccountType): InitialRouteName => { - return hasLocalUsers || createAccountType === 'full' ? 'new-account' : 'new-light-account' - } - const getPasswordLoginRoute = (createAccountType: CreateAccountType): InitialRouteName => { - return hasLocalUsers || createAccountType === 'full' ? 'login-password' : 'login-password-light' - } + const newAccountRoute: InitialRouteName = hasLocalUsers || createAccountType === 'full' ? 'new-account' : 'new-light-account' + + const passwordLoginRoute: InitialRouteName = hasLocalUsers || createAccountType === 'full' ? 'login-password' : 'login-password-light' const handleFinalSwipe = useHandler(() => { - if (isFinalSwipeEnabled) { - // This delay is necessary to properly reset the scene since it remains on - // the stack. - setTimeout(() => { - swipeOffset.value = 0 - }, 500) - - logEvent('Signup_Welcome') - - // Either route to password login or account creation - if (hasLocalUsers) { - navigation.navigate('login', { loginUiInitialRoute: getPasswordLoginRoute(createAccountType) }) - } else { - navigation.navigate('login', { loginUiInitialRoute: getNewAccountRoute(createAccountType) }) - } + // This delay is necessary to properly reset the scene since it remains on + // the stack. + setTimeout(() => { + swipeOffset.value = 0 + }, 500) + + dispatch(logEvent('Signup_Welcome')) + + // Either route to password login or account creation + if (hasLocalUsers) { + navigation.navigate('login', { loginUiInitialRoute: passwordLoginRoute, experimentConfig }) + } else { + navigation.navigate('login', { loginUiInitialRoute: newAccountRoute, experimentConfig }) } }) @@ -128,17 +138,17 @@ export const GettingStartedScene = (props: Props) => { }) const handlePressSignIn = useHandler(() => { - logEvent('Welcome_Signin') - navigation.navigate('login', { loginUiInitialRoute: getPasswordLoginRoute(createAccountType) }) + dispatch(logEvent('Welcome_Signin')) + navigation.navigate('login', { loginUiInitialRoute: passwordLoginRoute, experimentConfig }) }) const handlePressSignUp = useHandler(() => { - logEvent('Signup_Welcome') - navigation.navigate('login', { loginUiInitialRoute: getNewAccountRoute(createAccountType) }) + dispatch(logEvent('Signup_Welcome')) + navigation.navigate('login', { loginUiInitialRoute: newAccountRoute, experimentConfig }) }) const handlePressSkip = useHandler(() => { - navigation.navigate('login', {}) + navigation.navigate('login', { experimentConfig }) }) // Redirect to login or new account screen if the user swipes past the last @@ -146,31 +156,21 @@ export const GettingStartedScene = (props: Props) => { useAnimatedReaction( () => swipeOffset.value, value => { - if (value === 5) { + if (value === paginationCount) { runOnJS(handleFinalSwipe)() } } ) - // Initialize variant config values - useAsyncEffect( - async () => { - setIsFinalSwipeEnabled((await getExperimentConfigValue('swipeLastUsp')) === 'true') - setCreateAccountType(await getExperimentConfigValue('createAccountType')) - }, - [], - 'GettingStartedScene' - ) - // Redirect to login screen if device has memory of accounts // HACK: It's unknown how the localUsers dependency makes the routing work // properly, but use isLoggedIn explicitly to address the bug where this // effect would cause an unwanted navigation while logged in. useEffect(() => { if (localUsers.length > 0 && !isLoggedIn) { - navigation.navigate('login', {}) + navigation.navigate('login', { experimentConfig }) } - }, [isLoggedIn, localUsers, navigation]) + }, [experimentConfig, isLoggedIn, localUsers, navigation]) return ( @@ -213,7 +213,7 @@ export const GettingStartedScene = (props: Props) => { })} - {Array.from({ length: paginationCount + (isFinalSwipeEnabled ? 0 : 1) }).map((_, index) => ( + {Array.from({ length: paginationCount }).map((_, index) => ( handlePressIndicator(index)}> diff --git a/src/components/scenes/GuiPluginListScene.tsx b/src/components/scenes/GuiPluginListScene.tsx index c50c6d4fe59..df3af2affef 100644 --- a/src/components/scenes/GuiPluginListScene.tsx +++ b/src/components/scenes/GuiPluginListScene.tsx @@ -1,5 +1,3 @@ -import AsyncStorage from '@react-native-async-storage/async-storage' -import { asObject, asString } from 'cleaners' import { Disklet } from 'disklet' import { EdgeAccount } from 'edge-core-js/types' import * as React from 'react' @@ -8,6 +6,7 @@ import FastImage from 'react-native-fast-image' import Animated from 'react-native-reanimated' import { showBackupForTransferModal } from '../../actions/BackupModalActions' +import { getDeviceSettings, writeDeveloperPluginUri } from '../../actions/DeviceSettingsActions' import { NestedDisableMap } from '../../actions/ExchangeInfoActions' import { readSyncedSettings, updateOneSetting, writeSyncedSettings } from '../../actions/SettingsActions' import { FLAG_LOGO_URL } from '../../constants/CdnConstants' @@ -18,6 +17,7 @@ import { customPluginRow, guiPlugins } from '../../constants/plugins/GuiPlugins' import sellPluginJsonRaw from '../../constants/plugins/sellPluginList.json' import sellPluginJsonOverrideRaw from '../../constants/plugins/sellPluginListOverride.json' import { ENV } from '../../env' +import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' import { executePlugin } from '../../plugins/gui/fiatPlugin' import { SceneScrollHandler, useSceneScrollHandler } from '../../state/SceneScrollState' @@ -31,7 +31,9 @@ import { getPartnerIconUri } from '../../util/CdnUris' import { filterGuiPluginJson } from '../../util/GuiPluginTools' import { fetchInfo } from '../../util/network' import { bestOfPlugins } from '../../util/ReferralHelpers' +import { logEvent, OnLogEvent } from '../../util/tracking' import { base58ToUuid } from '../../util/utils' +import { EdgeAnim, fadeInUp30, fadeInUp60, fadeInUp90 } from '../common/EdgeAnim' import { InsetStyle, SceneWrapper } from '../common/SceneWrapper' import { CountryListModal } from '../modals/CountryListModal' import { TextInputModal } from '../modals/TextInputModal' @@ -44,6 +46,10 @@ import { CardUi4 } from '../ui4/CardUi4' import { RowUi4 } from '../ui4/RowUi4' import { SectionHeaderUi4 } from '../ui4/SectionHeaderUi4' +export interface GuiPluginListParams { + launchPluginId?: string +} + const buyRaw = buyPluginJsonOverrideRaw.length > 0 ? buyPluginJsonOverrideRaw : buyPluginJsonRaw const sellRaw = sellPluginJsonOverrideRaw.length > 0 ? sellPluginJsonOverrideRaw : sellPluginJsonRaw @@ -91,6 +97,7 @@ interface StateProps { disablePlugins: NestedDisableMap insetStyle: InsetStyle handleScroll: SceneScrollHandler + onLogEvent: OnLogEvent } interface DispatchProps { @@ -104,9 +111,7 @@ interface State { } const BUY_SELL_PLUGIN_REFRESH_INTERVAL = 60000 -const DEVELOPER_PLUGIN_KEY = 'developerPlugin' const PLUGIN_LIST_FILE = 'buySellPlugins.json' -const asDeveloperUri = asObject({ uri: asString }) class GuiPluginList extends React.PureComponent { componentMounted: boolean @@ -124,10 +129,9 @@ class GuiPluginList extends React.PureComponent { async componentDidMount() { this.updatePlugins().catch(err => showError(err)) this.checkCountry() - const text = await AsyncStorage.getItem(DEVELOPER_PLUGIN_KEY) - if (text != null) { - const clean = asDeveloperUri(JSON.parse(text)) - this.setState({ developerUri: clean.uri }) + const { developerPluginUri } = getDeviceSettings() + if (developerPluginUri != null) { + this.setState({ developerUri: developerPluginUri }) } } @@ -234,7 +238,7 @@ class GuiPluginList extends React.PureComponent { * Launch the provided plugin, including pre-flight checks. */ async openPlugin(listRow: GuiPluginRow, longPress: boolean = false) { - const { accountReferral, coreDisklet, countryCode, deviceId, disablePlugins, navigation, account } = this.props + const { coreDisklet, countryCode, deviceId, disablePlugins, navigation, account, onLogEvent } = this.props const { pluginId, paymentType, deepQuery = {} } = listRow const plugin = guiPlugins[pluginId] @@ -266,7 +270,7 @@ class GuiPluginList extends React.PureComponent { this.setState({ developerUri: deepPath }) // Write to disk lazily: - AsyncStorage.setItem(DEVELOPER_PLUGIN_KEY, JSON.stringify({ uri: deepPath })).catch(showError) + writeDeveloperPluginUri(deepPath).catch(error => showError(error)) } } @@ -279,7 +283,6 @@ class GuiPluginList extends React.PureComponent { await executePlugin({ account, - accountReferral, deviceId, direction, disablePlugins: disableProviders, @@ -288,7 +291,8 @@ class GuiPluginList extends React.PureComponent { longPress, navigation, paymentType, - regionCode: { countryCode } + regionCode: { countryCode }, + onLogEvent }) } else { // Launch! @@ -323,7 +327,7 @@ class GuiPluginList extends React.PureComponent { this.showCountrySelectionModal().catch(showError) } - renderPlugin = ({ item }: ListRenderItemInfo) => { + renderPlugin = ({ item, index }: ListRenderItemInfo) => { const { theme } = this.props const { pluginId } = item const plugin = guiPlugins[pluginId] @@ -337,35 +341,37 @@ class GuiPluginList extends React.PureComponent { const poweredBy = plugin.poweredBy ?? plugin.displayName return ( - - } - onPress={async () => await this.openPlugin(item).catch(showError)} - onLongPress={async () => await this.openPlugin(item, true).catch(showError)} - paddingRem={[1, 0.5, 1, 0.5]} - > - - - {item.title} - - {item.description === '' ? null : {item.description}} - {poweredBy != null && item.partnerIconPath != null ? ( - <> - - - {lstrings.plugin_powered_by_space} - - {' ' + poweredBy} - - - ) : null} - - + + + } + onPress={async () => await this.openPlugin(item).catch(showError)} + onLongPress={async () => await this.openPlugin(item, true).catch(showError)} + paddingRem={[1, 0.5, 1, 0.5]} + > + + + {item.title} + + {item.description === '' ? null : {item.description}} + {poweredBy != null && item.partnerIconPath != null ? ( + <> + + + {lstrings.plugin_powered_by_space} + + {' ' + poweredBy} + + + ) : null} + + + ) } @@ -374,28 +380,27 @@ class GuiPluginList extends React.PureComponent { const styles = getStyles(theme) const direction = this.getSceneDirection() const countryData = COUNTRY_CODES.find(country => country['alpha-2'] === countryCode) + const uri = `${FLAG_LOGO_URL}/${countryData?.filename || countryData?.name.toLowerCase().replace(' ', '-')}.png` + const imageSrc = React.useMemo(() => ({ uri }), [uri]) return ( <> - + - - - - - ) - } - body={countryData ? countryData.name : lstrings.buy_sell_crypto_select_country_button} - /> - + + + + + + + } + body={countryData ? countryData.name : lstrings.buy_sell_crypto_select_country_button} + /> + + ) @@ -446,8 +451,7 @@ class GuiPluginList extends React.PureComponent { ListEmptyComponent={this.renderEmptyList} renderItem={this.renderPlugin} keyExtractor={(item: GuiPluginRow) => item.pluginId + item.title} - // XXX: Hack. paddingBottom from insetStyle is not sufficient. - contentContainerStyle={{ ...insetStyle, paddingBottom: theme.rem(6) }} + contentContainerStyle={insetStyle} /> ) @@ -542,6 +546,10 @@ export const GuiPluginListScene = React.memo((props: OwnProps) => { dispatch(updateOneSetting({ countryCode })) } + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) + return ( {({ insetStyle, undoInsetStyle }) => { @@ -562,6 +570,7 @@ export const GuiPluginListScene = React.memo((props: OwnProps) => { updateCountryCode={updateCountryCode} theme={theme} insetStyle={insetStyle} + onLogEvent={handleLogEvent} /> ) diff --git a/src/components/scenes/Loans/LoanCloseScene.tsx b/src/components/scenes/Loans/LoanCloseScene.tsx index 5570dfc7215..6d7dc00d99b 100644 --- a/src/components/scenes/Loans/LoanCloseScene.tsx +++ b/src/components/scenes/Loans/LoanCloseScene.tsx @@ -36,6 +36,10 @@ import { NetworkFeeTile } from '../../tiles/NetworkFeeTile' import { TotalDebtCollateralTile } from '../../tiles/TotalDebtCollateralTile' import { RowUi4 } from '../../ui4/RowUi4' +export interface LoanCloseParams { + loanAccountId: string +} + export interface Props extends EdgeSceneProps<'loanClose'> { loanAccount: LoanAccount } diff --git a/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx b/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx index 6637fbb2ff8..e453305677f 100644 --- a/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx +++ b/src/components/scenes/Loans/LoanCreateConfirmationScene.tsx @@ -1,6 +1,8 @@ import { add, div, gt, max, mul, sub } from 'biggystring' +import { EdgeCurrencyWallet, EdgeTokenId } from 'edge-core-js' import * as React from 'react' +import { PaymentMethod } from '../../../controllers/action-queue/PaymentMethod' import { dryrunActionProgram } from '../../../controllers/action-queue/runtime/dryrunActionProgram' import { ActionOp, SwapActionOp } from '../../../controllers/action-queue/types' import { makeInitialProgramState } from '../../../controllers/action-queue/util/makeInitialProgramState' @@ -12,6 +14,7 @@ import { useExecutionContext } from '../../../hooks/useExecutionContext' import { useTokenDisplayData } from '../../../hooks/useTokenDisplayData' import { useWalletBalance } from '../../../hooks/useWalletBalance' import { lstrings } from '../../../locales/strings' +import { BorrowEngine, BorrowPlugin } from '../../../plugins/borrow-plugins/types' import { convertCurrency } from '../../../selectors/WalletSelectors' import { useDispatch, useSelector } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' @@ -33,6 +36,18 @@ import { NetworkFeeTile } from '../../tiles/NetworkFeeTile' import { RowUi4 } from '../../ui4/RowUi4' import { FormScene } from '../FormScene' +export interface LoanCreateConfirmationParams { + borrowEngine: BorrowEngine + borrowPlugin: BorrowPlugin + destTokenId: EdgeTokenId + destWallet: EdgeCurrencyWallet + nativeDestAmount: string + nativeSrcAmount: string + paymentMethod?: PaymentMethod + srcTokenId: EdgeTokenId + srcWallet: EdgeCurrencyWallet +} + const FEE_VOLATILITY_MULTIPLIER: { [network: string]: string } = { ethereum: '2', polygon: '1.5' diff --git a/src/components/scenes/Loans/LoanCreateScene.tsx b/src/components/scenes/Loans/LoanCreateScene.tsx index 5fed771bac1..a645ab86427 100644 --- a/src/components/scenes/Loans/LoanCreateScene.tsx +++ b/src/components/scenes/Loans/LoanCreateScene.tsx @@ -21,6 +21,7 @@ import { useWalletName } from '../../../hooks/useWalletName' import { useWatch } from '../../../hooks/useWatch' import { toPercentString } from '../../../locales/intl' import { lstrings } from '../../../locales/strings' +import { BorrowEngine, BorrowPlugin } from '../../../plugins/borrow-plugins/types' import { convertCurrency } from '../../../selectors/WalletSelectors' import { config } from '../../../theme/appConfig' import { useSelector } from '../../../types/reactRedux' @@ -45,6 +46,11 @@ import { SceneHeader } from '../../themed/SceneHeader' import { AprCard } from '../../tiles/AprCard' import { CardUi4 } from '../../ui4/CardUi4' +export interface LoanCreateParams { + borrowEngine: BorrowEngine + borrowPlugin: BorrowPlugin +} + interface Props extends EdgeSceneProps<'loanCreate'> {} export const LoanCreateScene = (props: Props) => { @@ -428,7 +434,6 @@ export const LoanCreateScene = (props: Props) => { srcTokenId }) }} - alignSelf="center" /> )} diff --git a/src/components/scenes/Loans/LoanDashboardScene.tsx b/src/components/scenes/Loans/LoanDashboardScene.tsx index d77ca36ac1d..70d48bbf970 100644 --- a/src/components/scenes/Loans/LoanDashboardScene.tsx +++ b/src/components/scenes/Loans/LoanDashboardScene.tsx @@ -132,7 +132,10 @@ export const LoanDashboardScene = (props: Props) => { return getCurrencyInfos(account).find(currencyInfo => pluginId === currencyInfo.pluginId) }, undefined) if (filteredCurrencyInfo == null) throw new Error(`Could not auto-create wallet of the supported types: ${SUPPORTED_WALLET_PLUGIN_IDS.join(', ')}`) - newLoanWallet = await createWallet(account, { walletName: `AAVE Loan Account`, walletType: filteredCurrencyInfo.walletType }) + newLoanWallet = await createWallet(account, { + name: `AAVE Loan Account`, + walletType: filteredCurrencyInfo.walletType + }) } if (newLoanWallet != null) { diff --git a/src/components/scenes/Loans/LoanDetailsScene.tsx b/src/components/scenes/Loans/LoanDetailsScene.tsx index 1cc818a5dd9..3aa6ae7c6fd 100644 --- a/src/components/scenes/Loans/LoanDetailsScene.tsx +++ b/src/components/scenes/Loans/LoanDetailsScene.tsx @@ -41,6 +41,10 @@ import { SceneHeader } from '../../themed/SceneHeader' import { CardUi4 } from '../../ui4/CardUi4' import { CryptoIconUi4 } from '../../ui4/CryptoIconUi4' +export interface LoanDetailsParams { + loanAccountId: string +} + interface Props extends EdgeSceneProps<'loanDetails'> { loanAccount: LoanAccount } diff --git a/src/components/scenes/Loans/LoanManageScene.tsx b/src/components/scenes/Loans/LoanManageScene.tsx index 77b2dbc9971..6b92a28ca9c 100644 --- a/src/components/scenes/Loans/LoanManageScene.tsx +++ b/src/components/scenes/Loans/LoanManageScene.tsx @@ -53,6 +53,11 @@ import { FormScene } from '../FormScene' export type LoanManageType = 'loan-manage-borrow' | 'loan-manage-deposit' | 'loan-manage-repay' | 'loan-manage-withdraw' +export interface LoanManageParams { + loanManageType: LoanManageType + loanAccountId: string +} + // User input display strings const MANAGE_ACTION_DATA_MAP: { [key: string]: { diff --git a/src/components/scenes/Loans/LoanStatusScene.tsx b/src/components/scenes/Loans/LoanStatusScene.tsx index baa8dff2b5e..31c6064187c 100644 --- a/src/components/scenes/Loans/LoanStatusScene.tsx +++ b/src/components/scenes/Loans/LoanStatusScene.tsx @@ -31,6 +31,11 @@ import { EdgeText } from '../../themed/EdgeText' import { MainButton } from '../../themed/MainButton' import { SceneHeader } from '../../themed/SceneHeader' +export interface LoanStatusParams { + actionQueueId: string + loanAccountId: string +} + interface Props extends EdgeSceneProps<'loanStatus'> { loanAccount: LoanAccount } diff --git a/src/components/scenes/LoginScene.tsx b/src/components/scenes/LoginScene.tsx index 747c4884200..204b9d26faf 100644 --- a/src/components/scenes/LoginScene.tsx +++ b/src/components/scenes/LoginScene.tsx @@ -1,5 +1,5 @@ import { EdgeAccount } from 'edge-core-js' -import { LoginScreen } from 'edge-login-ui-rn' +import { InitialRouteName, LoginScreen } from 'edge-login-ui-rn' import { NotificationPermissionsInfo } from 'edge-login-ui-rn/lib/types/ReduxTypes' import * as React from 'react' import { Keyboard, StatusBar, View } from 'react-native' @@ -12,8 +12,7 @@ import { initializeAccount, logoutRequest } from '../../actions/LoginActions' import { updateNotificationSettings } from '../../actions/NotificationActions' import { cacheStyles, Theme, useTheme } from '../../components/services/ThemeContext' import { ENV } from '../../env' -import { ExperimentConfig, getExperimentConfig } from '../../experimentConfig' -import { useAsyncEffect } from '../../hooks/useAsyncEffect' +import { ExperimentConfig } from '../../experimentConfig' import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' @@ -29,6 +28,11 @@ import { Airship, showError } from '../services/AirshipInstance' import { DotsBackground } from '../ui4/DotsBackground' import { LoadingScene } from './LoadingScene' +export interface LoginParams { + experimentConfig: ExperimentConfig // TODO: Create a new provider instead to serve the experimentConfig globally + loginUiInitialRoute?: InitialRouteName +} + // Sneak the BlurView over to the login UI: // @ts-expect-error global.ReactNativeBlurView = BlurView @@ -39,7 +43,7 @@ let firstRun = true export function LoginSceneComponent(props: Props) { const { navigation, route } = props - const { loginUiInitialRoute = 'login' } = route.params ?? {} + const { loginUiInitialRoute = 'login', experimentConfig } = route.params const dispatch = useDispatch() const theme = useTheme() const styles = getStyles(theme) @@ -58,7 +62,6 @@ export function LoginSceneComponent(props: Props) { const [counter, setCounter] = React.useState(0) const [notificationPermissionsInfo, setNotificationPermissionsInfo] = React.useState() const [passwordRecoveryKey, setPasswordRecoveryKey] = React.useState() - const [experimentConfig, setExperimentConfig] = React.useState() const fontDescription = React.useMemo( () => ({ @@ -158,9 +161,9 @@ export function LoginSceneComponent(props: Props) { ) const maybeHandleComplete = - ENV.USE_WELCOME_SCREENS && experimentConfig != null && experimentConfig.legacyLanding === 'uspLanding' + ENV.USE_WELCOME_SCREENS && experimentConfig != null && experimentConfig.landingType !== 'A_legacy' ? () => { - navigation.navigate('gettingStarted', {}) + navigation.navigate('gettingStarted', { experimentConfig }) } : undefined @@ -182,19 +185,13 @@ export function LoginSceneComponent(props: Props) { dispatch(showSendLogsModal()).catch(err => showError(err)) }) - // Wait for the experiment config to initialize before rendering anything - useAsyncEffect( - async () => { - const experimentConfig = await getExperimentConfig() - setExperimentConfig(experimentConfig) - }, - [], - 'LoginSceneComponent' - ) + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) const inMaestro = isMaestro() - return loggedIn || experimentConfig == null ? ( + return loggedIn ? ( ) : ( @@ -217,7 +214,7 @@ export function LoginSceneComponent(props: Props) { skipSecurityAlerts experimentConfig={experimentConfig} onComplete={maybeHandleComplete} - onLogEvent={logEvent} + onLogEvent={handleLogEvent} onLogin={handleLogin} onNotificationPermit={setNotificationPermissionsInfo} /> diff --git a/src/components/scenes/ManageTokensScene.tsx b/src/components/scenes/ManageTokensScene.tsx index e1da029f3c0..b2a61c254d2 100644 --- a/src/components/scenes/ManageTokensScene.tsx +++ b/src/components/scenes/ManageTokensScene.tsx @@ -24,6 +24,10 @@ import { Title } from '../themed/Title' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' import { CryptoIconUi4 } from '../ui4/CryptoIconUi4' +export interface ManageTokensParams { + walletId: string +} + interface Props extends EdgeSceneProps<'manageTokens'> { wallet: EdgeCurrencyWallet } diff --git a/src/components/scenes/MigrateWalletCalculateFeeScene.tsx b/src/components/scenes/MigrateWalletCalculateFeeScene.tsx index 5be4dce0f50..05b0a182b40 100644 --- a/src/components/scenes/MigrateWalletCalculateFeeScene.tsx +++ b/src/components/scenes/MigrateWalletCalculateFeeScene.tsx @@ -1,5 +1,5 @@ import { add, lt } from 'biggystring' -import { EdgeDenomination, EdgeSpendInfo, EdgeTransaction, InsufficientFundsError } from 'edge-core-js' +import { asMaybeInsufficientFundsError, EdgeDenomination, EdgeSpendInfo, EdgeTransaction, InsufficientFundsError } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator, ListRenderItemInfo, View } from 'react-native' import { FlatList } from 'react-native-gesture-handler' @@ -24,6 +24,10 @@ import { SafeSlider } from '../themed/SafeSlider' import { SceneHeader } from '../themed/SceneHeader' import { MigrateWalletItem } from './MigrateWalletSelectCryptoScene' +export interface MigrateWalletCalculateFeeParams { + migrateWalletList: MigrateWalletItem[] +} + interface Props extends EdgeSceneProps<'migrateWalletCalculateFee'> {} type AssetRowState = string | Error @@ -179,7 +183,8 @@ const MigrateWalletCalculateFeeComponent = (props: Props) => { } } catch (e: any) { for (const key of bundlesFeeTotals.keys()) { - if (e instanceof InsufficientFundsError) { + const insufficientFundsError = asMaybeInsufficientFundsError(e) + if (insufficientFundsError != null) { bundlesFeeTotals.set(key, e) } else { bundlesFeeTotals.set(key, Error(lstrings.migrate_unknown_error_fragment)) diff --git a/src/components/scenes/MigrateWalletCompletionScene.tsx b/src/components/scenes/MigrateWalletCompletionScene.tsx index 50a1410a3aa..6da34ceb4aa 100644 --- a/src/components/scenes/MigrateWalletCompletionScene.tsx +++ b/src/components/scenes/MigrateWalletCompletionScene.tsx @@ -23,6 +23,10 @@ import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' import { MigrateWalletItem } from './MigrateWalletSelectCryptoScene' +export interface MigrateWalletCompletionParams { + migrateWalletList: MigrateWalletItem[] +} + interface Props extends EdgeSceneProps<'migrateWalletCompletion'> {} interface MigrateWalletTokenItem extends MigrateWalletItem { @@ -234,9 +238,8 @@ const MigrateWalletCompletionComponent = (props: Props) => { disabled={!done} label={!done ? undefined : lstrings.string_done_cap} type="secondary" - marginRem={[0, 0, 0.5]} + marginRem={[0, 0, 1]} onPress={() => navigation.navigate('walletsTab', { screen: 'walletList' })} - alignSelf="center" /> ) diff --git a/src/components/scenes/MigrateWalletSelectCryptoScene.tsx b/src/components/scenes/MigrateWalletSelectCryptoScene.tsx index a31692342c1..22307fdf7f5 100644 --- a/src/components/scenes/MigrateWalletSelectCryptoScene.tsx +++ b/src/components/scenes/MigrateWalletSelectCryptoScene.tsx @@ -7,6 +7,7 @@ import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstant import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' +import { WalletCreateItem } from '../../selectors/getCreateWalletList' import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { getCurrencyCode, isKeysOnlyPlugin } from '../../util/CurrencyInfoHelpers' @@ -19,7 +20,10 @@ import { CreateWalletSelectCryptoRow } from '../themed/CreateWalletSelectCryptoR import { Fade } from '../themed/Fade' import { MainButton } from '../themed/MainButton' import { SceneHeader } from '../themed/SceneHeader' -import { WalletCreateItem } from '../themed/WalletList' + +export interface MigrateWalletSelectCryptoParams { + preSelectedWalletIds?: string[] +} interface Props extends EdgeSceneProps<'migrateWalletSelectCrypto'> {} @@ -128,7 +132,7 @@ const MigrateWalletSelectCryptoComponent = (props: Props) => { () => ( 0} visible={numSelected > 0} duration={300}> - + ), diff --git a/src/components/scenes/OtpRepairScene.tsx b/src/components/scenes/OtpRepairScene.tsx index b61d092235f..1c8e4701aa0 100644 --- a/src/components/scenes/OtpRepairScene.tsx +++ b/src/components/scenes/OtpRepairScene.tsx @@ -1,55 +1,43 @@ -import { EdgeAccount, EdgeContext } from 'edge-core-js' +import { OtpError } from 'edge-core-js' import { OtpRepairScreen } from 'edge-login-ui-rn' import * as React from 'react' -import { StatusBar, StyleSheet, View } from 'react-native' +import { useHandler } from '../../hooks/useHandler' import { config } from '../../theme/appConfig' -import { THEME } from '../../theme/variables/airbitz' -import { connect } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { logEvent } from '../../util/tracking' +import { SceneWrapper } from '../common/SceneWrapper' -interface OwnProps extends EdgeSceneProps<'otpRepair'> {} - -interface StateProps { - account: EdgeAccount - context: EdgeContext +export interface OtpRepairParams { + otpError: OtpError } -type Props = OwnProps & StateProps -class OtpRepairComponent extends React.Component { - render() { - const { context, account, navigation, route } = this.props - const { otpError } = route.params - const handleComplete = () => navigation.goBack() +interface Props extends EdgeSceneProps<'otpRepair'> {} - return ( - - - - ) - } -} +export const OtpRepairScene = (props: Props) => { + const { navigation, route } = props + const { otpError } = route.params + const account = useSelector(state => state.core.account) + const context = useSelector(state => state.core.context) + const dispatch = useDispatch() -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingTop: StatusBar.currentHeight, - backgroundColor: THEME.COLORS.PRIMARY - } -}) + const handleComplete = useHandler(() => navigation.goBack()) -export const OtpRepairScene = connect( - state => ({ - context: state.core.context, - account: state.core.account - }), - dispatch => ({}) -)(OtpRepairComponent) + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) + + return ( + + + + ) +} diff --git a/src/components/scenes/OtpSettingsScene.tsx b/src/components/scenes/OtpSettingsScene.tsx index a44be2fc249..cdc438e0127 100644 --- a/src/components/scenes/OtpSettingsScene.tsx +++ b/src/components/scenes/OtpSettingsScene.tsx @@ -112,9 +112,9 @@ class OtpSettingsSceneComponent extends React.Component { {otpKey != null ? this.renderKey(otpKey) : null} {otpKey != null ? ( - + ) : ( - + )} ) diff --git a/src/components/scenes/PasswordRecoveryScene.tsx b/src/components/scenes/PasswordRecoveryScene.tsx index 950b6a81d9f..b72b95b5e74 100644 --- a/src/components/scenes/PasswordRecoveryScene.tsx +++ b/src/components/scenes/PasswordRecoveryScene.tsx @@ -1,47 +1,36 @@ -import { EdgeAccount, EdgeContext } from 'edge-core-js' import { PasswordRecoveryScreen } from 'edge-login-ui-rn' import * as React from 'react' +import { useHandler } from '../../hooks/useHandler' import { config } from '../../theme/appConfig' -import { connect } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { logEvent } from '../../util/tracking' import { SceneWrapper } from '../common/SceneWrapper' -interface OwnProps extends EdgeSceneProps<'passwordRecovery'> {} +interface Props extends EdgeSceneProps<'passwordRecovery'> {} -interface StateProps { - account: EdgeAccount - context: EdgeContext -} -type Props = StateProps & OwnProps +export const ChangeRecoveryScene = (props: Props) => { + const { navigation } = props + const account = useSelector(state => state.core.account) + const context = useSelector(state => state.core.context) + const dispatch = useDispatch() -class ChangeRecoveryComponent extends React.Component { - render() { - const { context, account, navigation } = this.props - const handleComplete = () => navigation.goBack() + const handleComplete = useHandler(() => navigation.goBack()) - return ( - - - - ) - } -} + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) -export const ChangeRecoveryScene = connect( - state => ({ - context: state.core.context, - account: state.core.account - }), - dispatch => ({}) -)(ChangeRecoveryComponent) + return ( + + + + ) +} diff --git a/src/components/scenes/RequestScene.tsx b/src/components/scenes/RequestScene.tsx index 7d499178b58..e7de1142284 100644 --- a/src/components/scenes/RequestScene.tsx +++ b/src/components/scenes/RequestScene.tsx @@ -17,15 +17,16 @@ import { lstrings } from '../../locales/strings' import { getDisplayDenomination, getExchangeDenomination } from '../../selectors/DenominationSelectors' import { getExchangeRate } from '../../selectors/WalletSelectors' import { config } from '../../theme/appConfig' -import { connect, useSelector } from '../../types/reactRedux' +import { connect } from '../../types/reactRedux' import { EdgeSceneProps, NavigationBase } from '../../types/routerTypes' import { GuiCurrencyInfo } from '../../types/types' -import { getTokenId, getTokenIdForced, isKeysOnlyPlugin } from '../../util/CurrencyInfoHelpers' +import { getCurrencyCode, isKeysOnlyPlugin } from '../../util/CurrencyInfoHelpers' import { getAvailableBalance, getWalletName } from '../../util/CurrencyWalletHelpers' import { triggerHaptic } from '../../util/haptic' import { convertNativeToDenomination, darkenHexColor, truncateDecimals, zeroString } from '../../util/utils' -import { EdgeAnim } from '../common/EdgeAnim' +import { EdgeAnim, fadeInDown50, fadeInDown75, fadeInUp25, fadeInUp50, fadeInUp80 } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' +import { withWallet } from '../hoc/withWallet' import { AddressModal } from '../modals/AddressModal' import { ButtonsModal } from '../modals/ButtonsModal' import { QrModal } from '../modals/QrModal' @@ -44,7 +45,14 @@ import { ShareButtons } from '../themed/ShareButtons' import { CardUi4 } from '../ui4/CardUi4' import { AccentColors } from '../ui4/DotsBackground' -interface OwnProps extends EdgeSceneProps<'request'> {} +export interface RequestParams { + tokenId: EdgeTokenId + walletId: string +} + +interface OwnProps extends EdgeSceneProps<'request'> { + wallet: EdgeCurrencyWallet +} interface StateProps { currencyCode?: string @@ -248,10 +256,12 @@ export class RequestSceneComponent extends React.Component { + const { navigation } = this.props Airship.show(bridge => ) .then(async result => { if (result?.type === 'wallet') { const { walletId, tokenId } = result + navigation.setParams({ tokenId, walletId }) await this.props.onSelectWallet(this.props.navigation, walletId, tokenId) if (this.flipInputRef.current != null) { @@ -271,7 +281,7 @@ export class RequestSceneComponent extends React.Component {sprintf(lstrings.request_deprecated_currency_code, this.props.primaryCurrencyInfo?.displayCurrencyCode)} - + @@ -306,7 +316,7 @@ export class RequestSceneComponent extends React.Component - + {lstrings.fragment_request_subtitle} {denomString} - + {this.props.showBalance ? {displayBalanceString} : {lstrings.string_show_balance}} @@ -354,7 +364,7 @@ export class RequestSceneComponent extends React.Component {this.state.errorMessage != null ? {this.state.errorMessage} : null} - + )} - + {selectedAddress?.label ?? lstrings.request_qr_your_wallet_address} @@ -409,7 +419,7 @@ export class RequestSceneComponent extends React.Component - + @@ -581,30 +591,17 @@ const getStyles = cacheStyles((theme: Theme) => ({ })) const RequestSceneConnected = connect( - state => { - const { account } = state.core - const { currencyWallets } = account - const walletId = state.ui.wallets.selectedWalletId - const currencyCode: string = state.ui.wallets.selectedCurrencyCode - const wallet: EdgeCurrencyWallet = currencyWallets[walletId] - - if (currencyCode == null || wallet == null) { - return { - publicAddress: '', - fioAddressesExist: false, - isConnected: state.network.isConnected, - showBalance: state.ui.settings.isAccountBalanceVisible - } - } + (state, ownProps) => { + const { route, wallet } = ownProps + const { tokenId } = route.params + const currencyCode = getCurrencyCode(wallet, tokenId) - const { pluginId } = wallet.currencyInfo const primaryDisplayDenomination = getDisplayDenomination(state, wallet.currencyInfo.pluginId, currencyCode) const primaryExchangeDenomination = getExchangeDenomination(state, wallet.currencyInfo.pluginId, currencyCode) const primaryExchangeCurrencyCode: string = primaryExchangeDenomination.name - const tokenId = getTokenIdForced(state.core.account, pluginId, currencyCode) const primaryCurrencyInfo: GuiCurrencyInfo = { - walletId: walletId, + walletId: wallet.id, tokenId, displayCurrencyCode: currencyCode, displayDenomination: primaryDisplayDenomination, @@ -638,15 +635,11 @@ const RequestSceneConnected = connect { - const account = useSelector(state => state.core.account) - const currencyCode = useSelector(state => state.ui.wallets.selectedCurrencyCode) - const walletId = useSelector(state => state.ui.wallets.selectedWalletId) - const wallet = account.currencyWallets[walletId] ?? {} +export const RequestScene = withWallet((props: OwnProps) => { + const { route, wallet } = props + const { tokenId } = route.params + const { pluginId } = wallet.currencyInfo - const { pluginId = '' } = wallet.currencyInfo ?? {} - const tokenId = getTokenId(account, pluginId, currencyCode) - - const iconColor = useIconColor({ pluginId, tokenId: tokenId !== undefined ? tokenId : '' }) + const iconColor = useIconColor({ pluginId, tokenId }) return -} +}) diff --git a/src/components/scenes/SecurityAlertsScene.tsx b/src/components/scenes/SecurityAlertsScene.tsx index e1f00481fce..93b70ff033a 100644 --- a/src/components/scenes/SecurityAlertsScene.tsx +++ b/src/components/scenes/SecurityAlertsScene.tsx @@ -1,46 +1,29 @@ -import { EdgeAccount, EdgeContext } from 'edge-core-js' import { SecurityAlertsScreen } from 'edge-login-ui-rn' import * as React from 'react' -import { StatusBar, StyleSheet, View } from 'react-native' -import { THEME } from '../../theme/variables/airbitz' -import { connect } from '../../types/reactRedux' +import { useHandler } from '../../hooks/useHandler' +import { useDispatch, useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { logEvent } from '../../util/tracking' +import { SceneWrapper } from '../common/SceneWrapper' -interface OwnProps extends EdgeSceneProps<'securityAlerts'> {} +interface Props extends EdgeSceneProps<'securityAlerts'> {} -interface StateProps { - account: EdgeAccount - context: EdgeContext -} -type Props = StateProps & OwnProps - -class SecurityAlertsComponent extends React.Component { - render() { - const { context, account, navigation } = this.props - const handleComplete = () => navigation.pop() +export const SecurityAlertsScene = (props: Props) => { + const { navigation } = props + const account = useSelector(state => state.core.account) + const context = useSelector(state => state.core.context) + const dispatch = useDispatch() - return ( - - - - ) - } -} + const handleComplete = useHandler(() => navigation.pop()) -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingTop: StatusBar.currentHeight, - backgroundColor: THEME.COLORS.PRIMARY - } -}) + const handleLogEvent = useHandler((event, values) => { + dispatch(logEvent(event, values)) + }) -export const SecurityAlertsScene = connect( - state => ({ - context: state.core.context, - account: state.core.account - }), - dispatch => ({}) -)(SecurityAlertsComponent) + return ( + + + + ) +} diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 45c322b1d01..8b1ac0a0c29 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -312,7 +312,8 @@ const SendComponent = (props: Props) => { navigation.navigate('changeMiningFee2', { spendInfo, maxSpendSet: maxSpendSetter >= 0, - wallet: coreWallet, + tokenId, + walletId: coreWallet.id, onSubmit: (networkFeeOption, customNetworkFee) => { setSpendInfo({ ...spendInfo, networkFeeOption, customNetworkFee }) setPinValue(undefined) @@ -892,7 +893,7 @@ const SendComponent = (props: Props) => { // Mount/Unmount life-cycle events: useMount(() => { if (doCheckAndShowGetCryptoModal) { - dispatch(checkAndShowGetCryptoModal(navigation, route.params.walletId, route.params.spendInfo?.tokenId)).catch(err => showError(err)) + dispatch(checkAndShowGetCryptoModal(navigation, coreWallet, tokenId)).catch(err => showError(err)) } }) useUnmount(() => { diff --git a/src/components/scenes/Staking/StakeModifyScene.tsx b/src/components/scenes/Staking/StakeModifyScene.tsx index 740d3d38d37..0a33950ef8c 100644 --- a/src/components/scenes/Staking/StakeModifyScene.tsx +++ b/src/components/scenes/Staking/StakeModifyScene.tsx @@ -5,7 +5,16 @@ import { Image, View } from 'react-native' import { sprintf } from 'sprintf-js' import { lstrings } from '../../../locales/strings' -import { ChangeQuote, ChangeQuoteRequest, QuoteAllocation, StakeBelowLimitError, StakePoolFullError } from '../../../plugins/stake-plugins/types' +import { + ChangeQuote, + ChangeQuoteRequest, + QuoteAllocation, + StakeBelowLimitError, + StakePlugin, + StakePolicy, + StakePoolFullError, + StakePosition +} from '../../../plugins/stake-plugins/types' import { getDisplayDenomination, getExchangeDenominationFromAccount } from '../../../selectors/DenominationSelectors' import { useSelector } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' @@ -33,6 +42,15 @@ import { ErrorTile } from '../../tiles/ErrorTile' import { CardUi4 } from '../../ui4/CardUi4' import { RowUi4 } from '../../ui4/RowUi4' +export interface StakeModifyParams { + title: string + stakePlugin: StakePlugin + walletId: string + stakePolicy: StakePolicy + stakePosition: StakePosition + modification: ChangeQuoteRequest['action'] +} + interface Props extends EdgeSceneProps<'stakeModify'> { wallet: EdgeCurrencyWallet } diff --git a/src/components/scenes/Staking/StakeOverviewScene.tsx b/src/components/scenes/Staking/StakeOverviewScene.tsx index 27dce401a37..bae7cc12654 100644 --- a/src/components/scenes/Staking/StakeOverviewScene.tsx +++ b/src/components/scenes/Staking/StakeOverviewScene.tsx @@ -174,21 +174,9 @@ const StakeOverviewSceneComponent = (props: Props) => { scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> - + {stakePolicy.hideClaimAction ? null : ( - + )} {stakePolicy.hideUnstakeAndClaimAction ? null : ( { disabled={!canUnstakeAndClaim} type="escape" onPress={handleModifyPress('unstakeAndClaim')} - marginRem={[0.25, 0.5, 0.25, 0.5]} + marginRem={0.5} /> )} {stakePolicy.hideUnstakeAction ? null : ( - + )} diff --git a/src/components/scenes/TransactionDetailsScene.tsx b/src/components/scenes/TransactionDetailsScene.tsx index 25791fb0b17..b9f5034b67a 100644 --- a/src/components/scenes/TransactionDetailsScene.tsx +++ b/src/components/scenes/TransactionDetailsScene.tsx @@ -18,12 +18,11 @@ import { useWatch } from '../../hooks/useWatch' import { toPercentString } from '../../locales/intl' import { lstrings } from '../../locales/strings' import { getExchangeDenomination } from '../../selectors/DenominationSelectors' -import { convertCurrencyFromExchangeRates } from '../../selectors/WalletSelectors' import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { getCurrencyCodeWithAccount } from '../../util/CurrencyInfoHelpers' import { matchJson } from '../../util/matchJson' -import { convertNativeToExchange, darkenHexColor } from '../../util/utils' +import { convertCurrencyFromExchangeRates, convertNativeToExchange, darkenHexColor } from '../../util/utils' import { getMemoTitle } from '../../util/validateMemos' import { EdgeAnim } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' @@ -39,6 +38,7 @@ import { AdvancedDetailsCard } from '../ui4/AdvancedDetailsCard' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' import { CardUi4 } from '../ui4/CardUi4' import { AccentColors } from '../ui4/DotsBackground' +import { FiatExchangeDetailsCard } from '../ui4/FiatExchangeDetailsCard' import { RowUi4 } from '../ui4/RowUi4' import { SwapDetailsCard } from '../ui4/SwapDetailsCard' import { TxCryptoAmountRow } from '../ui4/TxCryptoAmountRow' @@ -64,11 +64,12 @@ const TransactionDetailsComponent = (props: Props) => { const iconColor = useIconColor({ pluginId: currencyInfo.pluginId, tokenId }) // Choose a default category based on metadata or the txAction - const { direction, iconPluginId, mergedData, savedData } = getTxActionDisplayInfo(transaction, account, wallet) + const { action, assetAction, direction, iconPluginId, mergedData, savedData } = getTxActionDisplayInfo(transaction, account, wallet) const swapData = convertActionToSwapData(account, transaction) ?? transaction.swapData const thumbnailPath = useContactThumbnail(mergedData.name) ?? pluginIdIcons[iconPluginId ?? ''] + const iconSource = React.useMemo(() => ({ uri: thumbnailPath }), [thumbnailPath]) const [localMetadata, setLocalMetadata] = React.useState({ exchangeAmount: metadata?.exchangeAmount, @@ -309,6 +310,8 @@ const TransactionDetailsComponent = (props: Props) => { backgroundColors[0] = scaledColor } + const fiatAction = action?.actionType === 'fiat' ? action : undefined + return ( { rightButtonType="editable" icon={ thumbnailPath ? ( - + ) : ( ) @@ -390,6 +393,11 @@ const TransactionDetailsComponent = (props: Props) => { {swapData == null ? null : } + + {fiatAction == null || assetAction == null ? null : ( + + )} + @@ -454,6 +462,7 @@ const convertActionToSwapData = (account: EdgeAccount, transaction: EdgeTransact const { swapInfo, orderId, orderUri, isEstimate, toAsset, payoutAddress, payoutWalletId, refundAddress } = action const payoutCurrencyCode = getCurrencyCodeWithAccount(account, toAsset.pluginId, toAsset.tokenId) + if (payoutCurrencyCode == null) return const out: EdgeTxSwap = { orderId, diff --git a/src/components/scenes/TransactionListScene.tsx b/src/components/scenes/TransactionListScene.tsx index ff2251c7276..8ec2bca9669 100644 --- a/src/components/scenes/TransactionListScene.tsx +++ b/src/components/scenes/TransactionListScene.tsx @@ -1,12 +1,11 @@ import { abs, lt } from 'biggystring' import { asArray } from 'cleaners' import { EdgeCurrencyWallet, EdgeTokenId, EdgeTokenMap, EdgeTransaction } from 'edge-core-js' -import { asAssetStatus, AssetStatus } from 'edge-info-server/types' +import { asAssetStatus, AssetStatus } from 'edge-info-server' import * as React from 'react' import { ListRenderItemInfo, RefreshControl, View } from 'react-native' import { getVersion } from 'react-native-device-info' import Animated from 'react-native-reanimated' -import { useSafeAreaInsets } from 'react-native-safe-area-context' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' @@ -24,7 +23,7 @@ import { EdgeSceneProps } from '../../types/routerTypes' import { fetchInfo } from '../../util/network' import { calculateSpamThreshold, darkenHexColor, unixToLocaleDateTime, zeroString } from '../../util/utils' import { AssetStatusCard } from '../cards/AssetStatusCard' -import { EdgeAnim, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim' +import { EdgeAnim, fadeInDown10, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' import { cacheStyles, useTheme } from '../services/ThemeContext' @@ -209,7 +208,7 @@ function TransactionListComponent(props: Props) { /> {assetStatuses.length > 0 && !isSearching ? assetStatuses.map(assetStatus => ( - + )) @@ -258,6 +257,7 @@ function TransactionListComponent(props: Props) { sceneWrapperInfo => { return ( } label={lstrings.export_transaction_export_type} /> {Platform.OS === 'android' ? this.renderAndroidSwitches() : this.renderIosSwitches()} - {disabledExport ? null : } + {disabledExport ? null : } ) } diff --git a/src/components/scenes/UpgradeUsernameScreen.tsx b/src/components/scenes/UpgradeUsernameScreen.tsx index 42ddaf51f96..8e269d2f1fe 100644 --- a/src/components/scenes/UpgradeUsernameScreen.tsx +++ b/src/components/scenes/UpgradeUsernameScreen.tsx @@ -19,7 +19,7 @@ export const UpgradeUsernameScene = (props: Props) => { navigation.goBack() } return ( - + ) diff --git a/src/components/scenes/WalletListScene.tsx b/src/components/scenes/WalletListScene.tsx index 928b33d411f..03149c10c17 100644 --- a/src/components/scenes/WalletListScene.tsx +++ b/src/components/scenes/WalletListScene.tsx @@ -95,14 +95,16 @@ export function WalletListScene(props: Props) { const renderFooter: FooterRender = React.useCallback( sceneWrapperInfo => { + const key = 'WalletListScene-SearchFooter' return sorting ? ( - + ) : ( { await handleNewConnectionPress()} - alignSelf="center" spinner={connecting} /> {lstrings.wc_walletconnect_active_connections} diff --git a/src/components/scenes/WcDisconnectScene.tsx b/src/components/scenes/WcDisconnectScene.tsx index 23e309caf63..21c01f8daaa 100644 --- a/src/components/scenes/WcDisconnectScene.tsx +++ b/src/components/scenes/WcDisconnectScene.tsx @@ -54,7 +54,7 @@ export const WcDisconnectScene = (props: Props) => { - + ) } diff --git a/src/components/themed/ClickableRow.tsx b/src/components/themed/ClickableRow.tsx deleted file mode 100644 index d17afd52ed8..00000000000 --- a/src/components/themed/ClickableRow.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react' -import { TouchableOpacity, View } from 'react-native' - -import { fixSides, mapSides, sidesToMargin, sidesToPadding } from '../../util/sides' -import { showError } from '../services/AirshipInstance' -import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' - -interface Props { - onPress: () => void | Promise - onLongPress?: () => void | Promise - autoHeight?: boolean - children?: React.ReactNode - underline?: boolean - - marginRem?: number[] | number - paddingRem?: number[] | number -} - -export class ClickableRowComponent extends React.PureComponent { - renderContent() { - const { children, marginRem, paddingRem, underline, autoHeight, theme } = this.props - const styles = getStyles(theme) - const margin = sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)) - const padding = sidesToPadding(mapSides(fixSides(paddingRem, 0), theme.rem)) - const containerStyles = [ - styles.rowContainer, - margin, - padding, - underline ? styles.underline : null, - autoHeight ? styles.autoHeight : null, - { - borderColor: 'white', - borderWidth: 0 - } - ] - - return {children} - } - - handlePress = async () => { - const { onPress } = this.props - await onPress()?.catch(err => showError(err)) - } - - render() { - const { onLongPress } = this.props - - return ( - - {this.renderContent()} - - ) - } -} - -const getStyles = cacheStyles((theme: Theme) => ({ - rowContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - height: theme.rem(4.25), - paddingHorizontal: theme.rem(1) - }, - underline: { - borderBottomWidth: theme.thinLineWidth, - borderBottomColor: theme.lineDivider - }, - autoHeight: { - height: 'auto' - } -})) - -export const ClickableRow = withTheme(ClickableRowComponent) diff --git a/src/components/themed/EdgeText.tsx b/src/components/themed/EdgeText.tsx index 078506d0023..ac17c1e6a46 100644 --- a/src/components/themed/EdgeText.tsx +++ b/src/components/themed/EdgeText.tsx @@ -1,38 +1,38 @@ import * as React from 'react' -import { Platform, Text, TextProps, TextStyle } from 'react-native' +import { Platform, StyleProp, Text, TextProps, TextStyle } from 'react-native' -import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' -interface OwnProps { +interface Props extends TextProps { children: React.ReactNode ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip' numberOfLines?: number - style?: TextStyle + style?: StyleProp disableFontScaling?: boolean minimumFontScale?: number } -export class EdgeTextComponent extends React.PureComponent { - render() { - const { children, style, theme, disableFontScaling = false, ...props } = this.props - const { text, androidAdjust } = getStyles(theme) - let { numberOfLines = 1 } = this.props - if (typeof children === 'string' && children.includes('\n')) { - numberOfLines = numberOfLines + (children.match(/\n/g) || []).length - } +export const EdgeText = (props: Props) => { + const { children, style, disableFontScaling = false, ...rest } = props + const theme = useTheme() + const styles = getStyles(theme) - return ( - - {children} - - ) + let { numberOfLines = 1 } = props + if (typeof children === 'string' && children.includes('\n')) { + numberOfLines = numberOfLines + (children.match(/\n/g) ?? []).length } + + return ( + + {children} + + ) } const getStyles = cacheStyles((theme: Theme) => ({ @@ -46,5 +46,3 @@ const getStyles = cacheStyles((theme: Theme) => ({ top: -1 } })) - -export const EdgeText = withTheme(EdgeTextComponent) diff --git a/src/components/themed/FilledTextInput.tsx b/src/components/themed/FilledTextInput.tsx index 9eb153d995d..a550e0d1bd6 100644 --- a/src/components/themed/FilledTextInput.tsx +++ b/src/components/themed/FilledTextInput.tsx @@ -1,3 +1,8 @@ +/** + * IMPORTANT: Changes in this file MUST be synced between edge-react-gui and + * edge-login-ui-rn! + */ + import * as React from 'react' import { useMemo } from 'react' import { ActivityIndicator, Platform, Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native' @@ -234,6 +239,7 @@ export const FilledTextInput = React.forwardRef : null} {secureTextEntry ? ( - + - + ) : null} - + - + {valid != null || error != null || charactersLeft !== '' ? ( - + {valid ?? error ?? null} {charactersLeft} @@ -329,6 +335,14 @@ const Container = styled(Animated.View)<{ ] }) +const TouchContainer = styled(TouchableOpacity)(theme => ({ + // Increase tappable area with padding, while net 0 with negative margin to visually appear as if 0 margins/padding + paddingHorizontal: theme.rem(1), + paddingVertical: theme.rem(1.25), + marginHorizontal: -theme.rem(1), + marginVertical: -theme.rem(1.25) +})) + const IconContainer = styled(View)(theme => ({ paddingHorizontal: theme.rem(0.25) })) @@ -521,11 +535,24 @@ const StyledNumericInput = styledWithRef(NumericInput)<{ ] }) -const MessagesContainer = styled(Animated.View)(theme => ({ - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: theme.rem(0.5) -})) +const MessagesContainer = styled(Animated.View)<{ noLayoutFlow?: boolean }>(theme => props => [ + { + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: theme.rem(0.5), + height: theme.rem(1) + }, + props.noLayoutFlow + ? { + // HACK: If this field has a potential error message, counter-act the + // layout flow to avoid the effect of the error message's appearance + // pushing components below the this text field down. + // If there's a counter, this field is already taking up the maximum + // amount of vertical space, so the above is not an issue. + marginBottom: -theme.rem(1) + } + : {} +]) const Message = styled(Text)<{ danger?: boolean }>(theme => props => [ { diff --git a/src/components/themed/MainButton.tsx b/src/components/themed/MainButton.tsx index 94b62c18bb5..fe86c9da3aa 100644 --- a/src/components/themed/MainButton.tsx +++ b/src/components/themed/MainButton.tsx @@ -12,10 +12,6 @@ interface Props { // and show a spinner until the promise resolves. onPress?: () => void | Promise - // Whether to center the button or stretch to fill the screen. - // Defaults to 'auto', letting the parent component be in charge: - alignSelf?: 'auto' | 'stretch' | 'center' - // True to dim the button & prevent interactions: disabled?: boolean @@ -44,22 +40,10 @@ interface Props { * A stand-alone button to perform the primary action in a modal or scene. */ export function MainButton(props: Props) { - const { - alignSelf = 'auto', - children, - disabled = false, - label, - marginRem = [0, 1, 0, 1], - onPress, - type = 'primary', - paddingRem, - layout, - spinner = false - } = props + const { children, disabled = false, label, marginRem, onPress, type = 'primary', paddingRem, layout, spinner = false } = props return ( { - const { children, noBackgroundBlur = false, sceneWrapperInfo, onLayoutHeight } = props + const { key, children, noBackgroundBlur = false, sceneWrapperInfo, onLayoutHeight } = props const { hasTabs = true, isKeyboardOpen = false } = sceneWrapperInfo ?? {} const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio) - const footerHeightShared = useSceneFooterState(state => state.footerHeight) const safeAreaInsets = useSafeAreaInsets() const maybeInsetBottom = !hasTabs ? safeAreaInsets.bottom : 0 @@ -45,24 +46,13 @@ export const SceneFooterWrapper = (props: SceneFooterProps) => { onLayoutHeight(footerHeight) }, [layout, maybeInsetBottom, onLayoutHeight]) - // Set the global shared value for the footerHeight so that way the - // background in the MenuTabs can translate accordingly - const isFocused = useIsFocused() - useEffect(() => { - if (isFocused) { - footerHeightShared.value = layout?.height ?? 0 - return () => { - footerHeightShared.value = 0 - } - } - }, [footerHeightShared, isFocused, layout?.height]) - // // Render // return ( { - const { placeholder, isSearching, searchText, noBackground, sceneWrapperInfo, onChangeText, onDoneSearching, onLayoutHeight, onStartSearching } = props + const { key, placeholder, isSearching, searchText, noBackground, sceneWrapperInfo, onChangeText, onDoneSearching, onLayoutHeight, onStartSearching } = props const textInputRef = React.useRef(null) @@ -80,7 +83,12 @@ export const SearchFooter = (props: SearchFooterProps) => { // return ( - + { const message = `${sprintf(lstrings.share_subject, config.appName)}\n\n${lstrings.share_message}\n\n` + const website = `${config.website}?af=appreferred-${base58ToUuid(context.clientId)}` const shareOptions = { - message: Platform.OS === 'ios' ? message : message + config.website, - url: Platform.OS === 'ios' ? config.website : '' + message: Platform.OS === 'ios' ? message : message + website, + url: Platform.OS === 'ios' ? website : '' } Share.open(shareOptions).catch(e => console.log(e)) } diff --git a/src/components/themed/SimpleTextInput.tsx b/src/components/themed/SimpleTextInput.tsx index 8024e15de90..34b79ee833d 100644 --- a/src/components/themed/SimpleTextInput.tsx +++ b/src/components/themed/SimpleTextInput.tsx @@ -213,11 +213,11 @@ export const SimpleTextInput = React.forwardRef - + - + ) @@ -303,6 +303,12 @@ const InputField = styledWithRef(AnimatedTextInput)<{ ] }) +const TouchContainer = styled(TouchableOpacity)(theme => ({ + // Increase tappable area with padding, while net 0 with negative margin to visually appear as if 0 margins/padding + padding: theme.rem(1), + margin: -theme.rem(1) +})) + function useAnimatedColorInterpolateFn(fromColor: string, toColor: string, disabledColor: string) { const interpolateFn = useMemo(() => { return (focusValue: SharedValue, disabledValue: SharedValue) => { diff --git a/src/components/themed/SwipeableRow.tsx b/src/components/themed/SwipeableRow.tsx index 00f0b51b79e..25f80b62e92 100644 --- a/src/components/themed/SwipeableRow.tsx +++ b/src/components/themed/SwipeableRow.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { I18nManager, Insets, LayoutChangeEvent, StyleSheet, View } from 'react-native' +import { I18nManager, Insets, LayoutChangeEvent, View } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' import Animated, { AnimationCallback, @@ -12,7 +12,7 @@ import Animated, { withTiming } from 'react-native-reanimated' -import { useTheme } from '../services/ThemeContext' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' interface Props { // The content to render in the row. @@ -99,6 +99,7 @@ export const SwipeableRow = React.forwardRef((props: Prop } = props const theme = useTheme() + const styles = getStyles(theme) const rtl = I18nManager.isRTL ? -1 : 1 // Values driven by the pan gesture: @@ -198,7 +199,7 @@ export const SwipeableRow = React.forwardRef((props: Prop ) }) -const styles = StyleSheet.create({ +const getStyles = cacheStyles((theme: Theme) => ({ childContainer: { // Dummy style needed to avoid breakage. }, @@ -206,11 +207,12 @@ const styles = StyleSheet.create({ overflow: 'hidden' }, underlay: { + borderRadius: theme.cardBorderRadius, overflow: 'hidden', flexDirection: 'row', position: 'absolute', - top: 0, - bottom: 0 + top: theme.rem(0.5), + bottom: theme.rem(0.5) }, underlayLeft: { justifyContent: 'flex-start', @@ -220,4 +222,4 @@ const styles = StyleSheet.create({ right: 0, justifyContent: 'flex-end' } -}) +})) diff --git a/src/components/themed/ThemedModal.tsx b/src/components/themed/ThemedModal.tsx deleted file mode 100644 index fb01407e757..00000000000 --- a/src/components/themed/ThemedModal.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react' -import { StyleSheet, ViewStyle } from 'react-native' -import { AirshipBridge, AirshipModal } from 'react-native-airship' -import { BlurView } from 'rn-id-blurview' - -import { fixSides } from '../../util/sides' -import { useTheme } from '../services/ThemeContext' -import { ModalFooter, ModalScrollArea } from './ModalParts' - -interface Props { - bridge: AirshipBridge - children?: React.ReactNode - onCancel: () => void - - // Control over the content area: - closeButton?: boolean - flexDirection?: ViewStyle['flexDirection'] - justifyContent?: ViewStyle['justifyContent'] - paddingRem?: number[] | number - - // Scroll area with a fade - scroll?: boolean - - // Gives the box a border: - warning?: boolean -} - -/** - * The Airship modal, but connected to our theming system. - */ -export function ThemedModal(props: Props) { - const { bridge, closeButton = true, children, flexDirection, justifyContent, warning = false, scroll = false, onCancel } = props - const paddingRem = fixSides(props.paddingRem, 1) - const theme = useTheme() - - // TODO: The warning styles are incorrectly hard-coded: - const borderColor = warning ? theme.warningText : theme.modalBorderColor - const borderWidth = warning ? 4 : theme.modalBorderWidth - - return ( - } - > - <> - {scroll ? {children} : children} - {closeButton ? : null} - - - ) -} diff --git a/src/components/themed/TransactionListComponents.tsx b/src/components/themed/TransactionListComponents.tsx index 19932b0c9b0..352bd722ad5 100644 --- a/src/components/themed/TransactionListComponents.tsx +++ b/src/components/themed/TransactionListComponents.tsx @@ -58,7 +58,7 @@ const getStyles = cacheStyles((theme: Theme) => ({ }, headerContainer: { paddingLeft: theme.rem(1), - paddingVertical: theme.rem(0.5) + paddingTop: theme.rem(0.5) }, headerDate: { fontSize: theme.rem(0.75), diff --git a/src/components/themed/TransactionListRow.tsx b/src/components/themed/TransactionListRow.tsx index a7a5df696a2..d78045350b6 100644 --- a/src/components/themed/TransactionListRow.tsx +++ b/src/components/themed/TransactionListRow.tsx @@ -135,10 +135,12 @@ export function TransactionListRow(props: Props) { ) + const iconSource = React.useMemo(() => ({ uri: thumbnailPath }), [thumbnailPath]) + const icon = thumbnailPath != null ? ( - + {arrowIcon} ) : ( diff --git a/src/components/themed/TransactionListTop.tsx b/src/components/themed/TransactionListTop.tsx index d47bfef1bdf..d5e2c6f8b8f 100644 --- a/src/components/themed/TransactionListTop.tsx +++ b/src/components/themed/TransactionListTop.tsx @@ -33,9 +33,8 @@ import { Airship, showError } from '../services/AirshipInstance' import { cacheStyles, Theme, ThemeProps, useTheme } from '../services/ThemeContext' import { CardUi4 } from '../ui4/CardUi4' import { CryptoIconUi4 } from '../ui4/CryptoIconUi4' +import { DividerLine } from './DividerLine' import { EdgeText } from './EdgeText' -import { SceneHeader } from './SceneHeader' - interface OwnProps { navigation: NavigationProp<'transactionList'> @@ -304,7 +303,7 @@ export class TransactionListTopComponent extends React.PureComponent { - const { navigation, isLightAccount } = this.props + const { isLightAccount, navigation, tokenId, wallet } = this.props triggerHaptic('impactLight') if (isLightAccount) { @@ -318,7 +317,7 @@ export class TransactionListTopComponent extends React.PureComponent showError(error)) } else { - navigation.push('request', {}) + navigation.push('request', { tokenId, walletId: wallet.id }) } } @@ -405,9 +404,7 @@ export class TransactionListTopComponent extends React.PureComponent} {isEmpty || searching ? null : ( - - {lstrings.fragment_transaction_list_transaction} - + )} diff --git a/src/components/themed/WalletList.tsx b/src/components/themed/WalletList.tsx index f933b181fac..cbefc9e25c0 100644 --- a/src/components/themed/WalletList.tsx +++ b/src/components/themed/WalletList.tsx @@ -1,5 +1,5 @@ import { FlashList } from '@shopify/flash-list' -import { EdgeAccount, EdgeTokenId } from 'edge-core-js' +import { EdgeTokenId } from 'edge-core-js' import * as React from 'react' import { SectionList, ViewStyle } from 'react-native' @@ -7,12 +7,11 @@ import { selectWalletToken } from '../../actions/WalletActions' import { useHandler } from '../../hooks/useHandler' import { useRowLayout } from '../../hooks/useRowLayout' import { lstrings } from '../../locales/strings' +import { filterWalletCreateItemListBySearchText, getCreateWalletList, WalletCreateItem } from '../../selectors/getCreateWalletList' import { useDispatch, useSelector } from '../../types/reactRedux' import { NavigationBase } from '../../types/routerTypes' import { EdgeAsset, FlatListItem, WalletListItem } from '../../types/types' -import { getCreateWalletTypes, getTokenIdForced } from '../../util/CurrencyInfoHelpers' -import { assetOverrides } from '../../util/serverState' -import { normalizeForSearch } from '../../util/utils' +import { checkAssetFilter } from '../../util/CurrencyInfoHelpers' import { showError } from '../services/AirshipInstance' import { searchWalletList } from '../services/SortedWalletList' import { useTheme } from '../services/ThemeContext' @@ -42,16 +41,6 @@ interface Props { onPress?: (walletId: string, tokenId: EdgeTokenId) => void } -export interface WalletCreateItem { - key: string - currencyCode: string - displayName: string - pluginId: string - tokenId: EdgeTokenId // Used for creating tokens - walletType?: string // Used for creating wallets - createWalletIds?: string[] -} - interface Section { title: string data: Array @@ -120,7 +109,7 @@ export function WalletList(props: Props) { // Apply the currency filters: const { pluginId } = wallet.currencyInfo - return checkFilterWallet({ pluginId, tokenId }, allowedAssets, excludeAssets) + return checkAssetFilter({ pluginId, tokenId }, allowedAssets, excludeAssets) }) }, [allowedAssets, allowedWalletIds, excludeAssets, excludeWalletIds, sortedWalletList]) @@ -197,17 +186,7 @@ export function WalletList(props: Props) { const renderRow = useHandler((item: FlatListItem) => { if (item.item.walletId == null) { const createItem: WalletCreateItem = item.item - const { currencyCode, displayName, pluginId, walletType, createWalletIds } = createItem - return ( - - ) + return } const walletItem: WalletListItem = item.item @@ -248,106 +227,3 @@ export function WalletList(props: Props) { /> ) } - -interface CreateWalletListOpts { - filteredWalletList?: WalletListItem[] - filterActivation?: boolean - allowedAssets?: EdgeAsset[] - excludeAssets?: EdgeAsset[] -} - -export const getCreateWalletList = (account: EdgeAccount, opts: CreateWalletListOpts = {}): WalletCreateItem[] => { - const { filteredWalletList = [], filterActivation, allowedAssets, excludeAssets } = opts - const walletList: WalletCreateItem[] = [] - - // Add top-level wallet types: - const createWalletCurrencies = getCreateWalletTypes(account, filterActivation) - for (const createWalletCurrency of createWalletCurrencies) { - const { currencyCode, currencyName, pluginId, walletType } = createWalletCurrency - const tokenId = getTokenIdForced(account, pluginId, currencyCode) - walletList.push({ - key: `create-${walletType}-${pluginId}`, - currencyCode, - displayName: currencyName, - pluginId, - tokenId, - walletType - }) - } - - // Add token types: - for (const pluginId of Object.keys(account.currencyConfig)) { - const currencyConfig = account.currencyConfig[pluginId] - const { builtinTokens, currencyInfo } = currencyConfig - - // Identify which wallets could add the token - const createWalletIds = Object.keys(account.currencyWallets).filter(walletId => account.currencyWallets[walletId].currencyInfo.pluginId === pluginId) - - for (const tokenId of Object.keys(builtinTokens)) { - const { currencyCode, displayName } = builtinTokens[tokenId] - - // Fix for when the token code and chain code are the same (like EOS/TLOS) - if (currencyCode === currencyInfo.currencyCode) continue - - walletList.push({ - key: `create-${currencyInfo.pluginId}-${tokenId}`, - currencyCode, - displayName, - pluginId, - tokenId, - createWalletIds - }) - } - } - - // Filter this list: - const existingWallets: EdgeAsset[] = [] - for (const { wallet, tokenId } of filteredWalletList) { - if (wallet == null) continue - existingWallets.push({ - pluginId: wallet.currencyInfo.pluginId, - tokenId - }) - } - const out = walletList.filter(item => !hasAsset(existingWallets, item) && checkFilterWallet(item, allowedAssets, excludeAssets)) - return out.filter(item => !assetOverrides.disable[item.pluginId]) -} - -export const filterWalletCreateItemListBySearchText = (createWalletList: WalletCreateItem[], searchText: string): WalletCreateItem[] => { - const out: WalletCreateItem[] = [] - const searchTarget = normalizeForSearch(searchText) - for (const item of createWalletList) { - const { currencyCode, displayName, pluginId, walletType } = item - if (normalizeForSearch(currencyCode).includes(searchTarget) || normalizeForSearch(displayName).includes(searchTarget)) { - out.push(item) - continue - } - // Do an additional search for pluginId for mainnet create items - if (walletType != null && normalizeForSearch(pluginId).includes(searchTarget)) { - out.push(item) - } - } - return out -} - -function checkFilterWallet(details: EdgeAsset, allowedAssets?: EdgeAsset[], excludeAssets?: EdgeAsset[]): boolean { - if (allowedAssets != null && !hasAsset(allowedAssets, details)) { - return false - } - if (excludeAssets != null && hasAsset(excludeAssets, details)) { - return false - } - return true -} - -/** - * Returns true if the asset array includes the given asset. - */ -function hasAsset(assets: EdgeAsset[], target: EdgeAsset): boolean { - for (const asset of assets) { - if (asset.pluginId === target.pluginId && asset.tokenId === target.tokenId) { - return true - } - } - return false -} diff --git a/src/components/themed/WalletListCreateRow.tsx b/src/components/themed/WalletListCreateRow.tsx index 8eb36d4b151..ae9ed9a6acb 100644 --- a/src/components/themed/WalletListCreateRow.tsx +++ b/src/components/themed/WalletListCreateRow.tsx @@ -1,16 +1,16 @@ -import { EdgeCurrencyWallet, EdgeTokenId } from 'edge-core-js' +import { EdgeCurrencyWallet, EdgeTokenId, JsonObject } from 'edge-core-js' import * as React from 'react' import { View } from 'react-native' import { TouchableOpacity } from 'react-native-gesture-handler' -import { createWallet, CreateWalletOptions, getUniqueWalletName } from '../../actions/CreateWalletActions' +import { createWallet, getUniqueWalletName } from '../../actions/CreateWalletActions' import { approveTokenTerms } from '../../actions/TokenTermsActions' import { showFullScreenSpinner } from '../../components/modals/AirshipFullScreenSpinner' import { Airship, showError } from '../../components/services/AirshipInstance' -import { getPluginId } from '../../constants/WalletAndCurrencyConstants' import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' +import { WalletCreateItem } from '../../selectors/getCreateWalletList' import { useDispatch, useSelector } from '../../types/reactRedux' import { ThunkAction } from '../../types/reduxTypes' import { getTokenIdForced } from '../../util/CurrencyInfoHelpers' @@ -22,31 +22,27 @@ import { EdgeText } from './EdgeText' import { WalletListCurrencyRow } from './WalletListCurrencyRow' export interface WalletListCreateRowProps { - currencyCode: string - currencyName: string + createItem: WalletCreateItem + createWalletId?: string + trackingEventFailed?: TrackingEventName trackingEventSuccess?: TrackingEventName - createWalletIds?: string[] - pluginId: string - walletType?: string - onPress?: (walletId: string, tokenId: EdgeTokenId) => void } export const WalletListCreateRowComponent = (props: WalletListCreateRowProps) => { const { - currencyCode, - currencyName = '', + createItem, + createWalletId, trackingEventFailed, trackingEventSuccess, - createWalletIds = [], - walletType, - pluginId, - // Callbacks: onPress } = props + const { currencyCode, displayName: currencyName = '', keyOptions = {}, pluginId, walletType } = createItem + const createWalletIds = createWalletId != null ? [createWalletId] : createItem.createWalletIds ?? [] + const account = useSelector(state => state.core.account) const currencyWallets = useWatch(account, 'currencyWallets') @@ -66,9 +62,9 @@ export const WalletListCreateRowComponent = (props: WalletListCreateRowProps) => } pressMutexRef.current = true - const handleRes = (walletId: string) => (onPress != null ? onPress(walletId, tokenId) : null) + const handleRes = (wallet?: EdgeCurrencyWallet) => (onPress != null && wallet != null ? onPress(wallet.id, tokenId) : null) if (walletType != null) { - await dispatch(createAndSelectWallet({ walletType })) + await dispatch(createAndSelectWallet(pluginId, keyOptions)) .then(handleRes) .catch(err => showError(err)) .finally(() => (pressMutexRef.current = false)) @@ -156,7 +152,7 @@ function createAndSelectToken({ trackingEventFailed?: TrackingEventName trackingEventSuccess?: TrackingEventName createWalletId?: string -}): ThunkAction> { +}): ThunkAction> { return async (dispatch, getState) => { const state = getState() const { account } = state.core @@ -175,9 +171,9 @@ function createAndSelectToken({ lstrings.wallet_list_modal_enabling_token, (async (): Promise => { return await createWallet(account, { - walletType, - walletName: getUniqueWalletName(account, pluginId), - fiatCurrencyCode: defaultIsoFiat + fiatCurrencyCode: defaultIsoFiat, + name: getUniqueWalletName(account, pluginId), + walletType }) })() ) @@ -187,30 +183,35 @@ function createAndSelectToken({ await wallet.changeEnabledTokenIds([...wallet.enabledTokenIds, tokenId]) if (trackingEventSuccess != null) logEvent(trackingEventSuccess) - return wallet.id + return wallet } catch (error: any) { showError(error) if (trackingEventFailed != null) logEvent(trackingEventFailed, { error: String(error) }) } - return '' } } -function createAndSelectWallet({ walletType, fiatCurrencyCode }: CreateWalletOptions): ThunkAction> { +function createAndSelectWallet(pluginId: string, keyOptions: JsonObject): ThunkAction> { return async (dispatch, getState) => { const state = getState() const { account } = state.core - const walletName = getUniqueWalletName(account, getPluginId(walletType)) + const { defaultIsoFiat } = state.ui.settings + const { walletType } = account.currencyConfig[pluginId].currencyInfo + try { const wallet = await showFullScreenSpinner( lstrings.wallet_list_modal_creating_wallet, - createWallet(account, { walletName, walletType, fiatCurrencyCode }) + createWallet(account, { + fiatCurrencyCode: defaultIsoFiat, + keyOptions, + name: getUniqueWalletName(account, pluginId), + walletType + }) ) - return wallet.id + return wallet } catch (error: any) { showError(error) } - return '' } } diff --git a/src/components/themed/WalletListHeader.tsx b/src/components/themed/WalletListHeader.tsx index 274ad6c45ba..88245528911 100644 --- a/src/components/themed/WalletListHeader.tsx +++ b/src/components/themed/WalletListHeader.tsx @@ -5,7 +5,7 @@ import Ionicon from 'react-native-vector-icons/Ionicons' import { Fontello } from '../../assets/vector/index' import { lstrings } from '../../locales/strings' import { NavigationBase } from '../../types/routerTypes' -import { EdgeAnim } from '../common/EdgeAnim' +import { EdgeAnim, fadeInUp40, fadeInUp60 } from '../common/EdgeAnim' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' import { BalanceCardUi4 } from '../ui4/BalanceCardUi4' import { SectionHeaderUi4 } from '../ui4/SectionHeaderUi4' @@ -38,12 +38,12 @@ export class WalletListHeaderComponent extends React.PureComponent { return ( <> {searching ? null : ( - + )} {sorting || searching ? null : ( - + )} diff --git a/src/components/themed/WalletListSwipeable.tsx b/src/components/themed/WalletListSwipeable.tsx index 90f53aaf814..15758049b64 100644 --- a/src/components/themed/WalletListSwipeable.tsx +++ b/src/components/themed/WalletListSwipeable.tsx @@ -3,11 +3,11 @@ import * as React from 'react' import { useMemo } from 'react' import { FlatList, RefreshControl } from 'react-native' import Animated from 'react-native-reanimated' -import { useSafeAreaInsets } from 'react-native-safe-area-context' import { selectWalletToken } from '../../actions/WalletActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { useHandler } from '../../hooks/useHandler' +import { filterWalletCreateItemListBySearchText, getCreateWalletList, WalletCreateItem } from '../../selectors/getCreateWalletList' import { useSceneScrollHandler } from '../../state/SceneScrollState' import { useDispatch, useSelector } from '../../types/reactRedux' import { NavigationProp } from '../../types/routerTypes' @@ -16,7 +16,6 @@ import { EdgeAnim, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim' import { InsetStyle } from '../common/SceneWrapper' import { searchWalletList } from '../services/SortedWalletList' import { useTheme } from '../services/ThemeContext' -import { filterWalletCreateItemListBySearchText, getCreateWalletList, WalletCreateItem } from './WalletList' import { WalletListCreateRow } from './WalletListCreateRow' import { WalletListSwipeableCurrencyRow } from './WalletListSwipeableCurrencyRow' import { WalletListSwipeableLoadingRow } from './WalletListSwipeableLoadingRow' @@ -85,17 +84,12 @@ function WalletListSwipeableComponent(props: Props) { const { index } = item if (item.item.key.includes('create-')) { const createItem: WalletCreateItem = item.item - const { currencyCode, displayName, pluginId, walletType, createWalletIds } = createItem return ( ) } @@ -124,22 +118,20 @@ function WalletListSwipeableComponent(props: Props) { const handleScroll = useSceneScrollHandler() - // TODO: Include this fix in the SceneWrapper component - const safeAreaInsets = useSafeAreaInsets() - const contentContainerStyle = useMemo(() => { return { paddingTop: insetStyle.paddingTop + theme.rem(0.5), - paddingBottom: insetStyle.paddingBottom + theme.rem(0.5) + safeAreaInsets.bottom, + paddingBottom: insetStyle.paddingBottom + theme.rem(0.5), paddingLeft: insetStyle.paddingLeft + theme.rem(0.5), paddingRight: insetStyle.paddingRight + theme.rem(0.5) } - }, [insetStyle.paddingBottom, insetStyle.paddingLeft, insetStyle.paddingRight, insetStyle.paddingTop, safeAreaInsets.bottom, theme]) + }, [insetStyle.paddingBottom, insetStyle.paddingLeft, insetStyle.paddingRight, insetStyle.paddingTop, theme]) return ( { if (activated) { - navigation.navigate('request', {}) + navigation.navigate('request', { tokenId, walletId: wallet.id }) } }) .catch(err => showError(err)) diff --git a/src/components/tiles/EditableAmountTile.tsx b/src/components/tiles/EditableAmountTile.tsx index b14904ace9a..3f22bd54a21 100644 --- a/src/components/tiles/EditableAmountTile.tsx +++ b/src/components/tiles/EditableAmountTile.tsx @@ -3,10 +3,9 @@ import { EdgeCurrencyWallet, EdgeDenomination } from 'edge-core-js' import * as React from 'react' import { lstrings } from '../../locales/strings' -import { convertCurrencyFromExchangeRates } from '../../selectors/WalletSelectors' import { GuiExchangeRates } from '../../types/types' import { getWalletFiat } from '../../util/CurrencyWalletHelpers' -import { DECIMAL_PRECISION, getDenomFromIsoCode, zeroString } from '../../util/utils' +import { convertCurrencyFromExchangeRates, DECIMAL_PRECISION, getDenomFromIsoCode, zeroString } from '../../util/utils' import { EdgeAnim } from '../common/EdgeAnim' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' diff --git a/src/components/tiles/Tile.tsx b/src/components/tiles/Tile.tsx deleted file mode 100644 index 0fba0a76aa8..00000000000 --- a/src/components/tiles/Tile.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import Clipboard from '@react-native-clipboard/clipboard' -import * as React from 'react' -import { ActivityIndicator, TouchableWithoutFeedback, View } from 'react-native' -import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome' -import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons' - -import { lstrings } from '../../locales/strings' -import { triggerHaptic } from '../../util/haptic' -import { showError, showToast } from '../services/AirshipInstance' -import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' -import { EdgeText } from '../themed/EdgeText' - -const textHeights = { - small: 2, - medium: 3, - large: 0 -} - -export type TileType = 'copy' | 'editable' | 'questionable' | 'loading' | 'static' | 'touchable' | 'delete' - -interface OwnProps { - body?: string - children?: React.ReactNode - error?: boolean - onPress?: () => Promise | void - title: string - type: TileType - contentPadding?: boolean - maximumHeight?: 'small' | 'medium' | 'large' -} -type Props = OwnProps & ThemeProps - -export class TileComponent extends React.PureComponent { - copy = () => { - if (!this.props.body) return - Clipboard.setString(this.props.body) - showToast(lstrings.fragment_copied) - } - - handlePress = () => { - triggerHaptic('impactLight') - if (this.props.type === 'copy') { - this.copy() - } else { - if (this.props.onPress != null) this.props.onPress()?.catch(err => showError(err)) - } - } - - render() { - const { body, title, contentPadding = true, children, theme, type, maximumHeight = 'medium', error } = this.props - const styles = getStyles(theme) - const numberOfLines = textHeights[maximumHeight] - - if (type === 'loading') { - return ( - - - - {title} - - - - - - ) - } - return ( - - - - - {type === 'editable' && } - {type === 'copy' && } - {type === 'delete' && } - {type === 'questionable' && } - - {title} - - {typeof body === 'string' && ( - - {body} - - )} - {children} - - {type === 'touchable' && ( - - - - )} - - - - - ) - } -} - -const getStyles = cacheStyles((theme: Theme) => ({ - container: { - backgroundColor: theme.tileBackground, - paddingHorizontal: theme.rem(1), - marginTop: theme.rem(1), - paddingBottom: theme.rem(1), - flexDirection: 'row', - alignItems: 'center' - }, - content: { - flex: 1 - }, - contentPadding: { - paddingLeft: theme.rem(0.25) - }, - iconContainer: { - justifyContent: 'center', - alignItems: 'center' - }, - arrowIcon: { - color: theme.iconTappable, - marginHorizontal: theme.rem(0.5), - textAlign: 'center' - }, - textHeader: { - color: theme.secondaryText, - fontSize: theme.rem(0.75), - paddingBottom: theme.rem(0.25), - paddingRight: theme.rem(1) - }, - textHeaderError: { - color: theme.dangerText, - fontSize: theme.rem(0.75) - }, - textBody: { - color: theme.primaryText, - fontSize: theme.rem(1) - }, - editIcon: { - position: 'absolute', - color: theme.iconTappable, - width: theme.rem(0.75), - height: theme.rem(0.75), - top: theme.rem(0.25), - right: 0 - }, - loader: { - marginTop: theme.rem(0.25) - }, - divider: { - height: theme.thinLineWidth, - marginLeft: theme.rem(1), - borderBottomWidth: theme.thinLineWidth, - borderBottomColor: theme.lineDivider - } -})) - -export const Tile = withTheme(TileComponent) diff --git a/src/components/ui4/BalanceCardUi4.tsx b/src/components/ui4/BalanceCardUi4.tsx index 43dfe89fe77..3fb4f6bcc6c 100644 --- a/src/components/ui4/BalanceCardUi4.tsx +++ b/src/components/ui4/BalanceCardUi4.tsx @@ -68,30 +68,25 @@ export const BalanceCardUi4 = (props: Props) => { {lstrings.fragment_wallets_balance_text} - + {!exchangeRatesReady ? ( - {lstrings.exchange_rates_loading} + {lstrings.exchange_rates_loading} ) : animateNumber ? ( - + ) : ( - {balanceString} + {balanceString} )} {onViewAssetsPress == null ? null : ( - {lstrings.view_assets} + {lstrings.view_assets} )} @@ -110,55 +105,71 @@ export const BalanceCardUi4 = (props: Props) => { ) } -const getStyles = cacheStyles((theme: Theme) => ({ - balanceContainer: { - margin: theme.rem(0.5) - }, - titleContainer: { - flexDirection: 'row', - alignItems: 'center' - }, - - rightButtonContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - position: 'absolute', - top: theme.rem(1) + 5, // Fudge factor to align with the larger text on the left - right: theme.rem(1) - }, - tappableText: { - fontSize: theme.rem(0.75), - color: theme.iconTappable - }, - - // These two icons have different bounding boxes. Adjusted to match - eyeIcon: { - alignSelf: 'center', - marginLeft: theme.rem(0.25), - marginRight: theme.rem(0) - }, - balanceText: { - fontSize: theme.rem(1.75), - fontFamily: theme.fontFaceMedium, - color: theme.primaryText, - includeFontPadding: false - }, - balanceTextContainer: { +const getStyles = cacheStyles((theme: Theme) => { + const balanceTextContainer = { marginTop: theme.rem(0.25), marginBottom: theme.rem(0.5), height: theme.rem(2.25) - }, - balanceBoxContainer: { - height: theme.rem(3.25), - marginTop: theme.rem(0.5) - }, - balanceHeader: { - fontSize: theme.rem(1), - color: theme.secondaryText - }, - showBalance: { - fontSize: theme.rem(1.5), - fontFamily: theme.fontFaceMedium } -})) + + const balanceText = { + fontSize: theme.rem(1.75), + fontFamily: theme.fontFaceMedium, + color: theme.primaryText, + includeFontPadding: false, + ...theme.cardTextShadow + } + + return { + balanceContainer: { + margin: theme.rem(0.5) + }, + titleContainer: { + flexDirection: 'row', + alignItems: 'center' + }, + + rightButtonContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + position: 'absolute', + top: 0, + right: 0 + }, + tappableText: { + fontSize: theme.rem(0.75), + color: theme.iconTappable, + margin: theme.rem(1), + marginTop: theme.rem(1) + 3, // Fudge factor to align with the larger text on the left + ...theme.cardTextShadow + }, + + // These two icons have different bounding boxes. Adjusted to match + eyeIcon: { + alignSelf: 'center', + marginLeft: theme.rem(0.25), + marginRight: theme.rem(0), + ...theme.cardTextShadow + }, + balanceText, + balanceTextContainer, + balanceTextNoAnim: { + ...balanceTextContainer, + ...balanceText + }, + balanceBoxContainer: { + height: theme.rem(3.25), + marginTop: theme.rem(0.5) + }, + balanceHeader: { + fontSize: theme.rem(1), + color: theme.secondaryText + }, + ratesLoading: { ...balanceTextContainer, ...theme.cardTextShadow }, + showBalance: { + fontSize: theme.rem(1.5), + fontFamily: theme.fontFaceMedium + } + } +}) diff --git a/src/components/ui4/BlogCard.tsx b/src/components/ui4/BlogCard.tsx index ca1002b1974..4dcca107bc0 100644 --- a/src/components/ui4/BlogCard.tsx +++ b/src/components/ui4/BlogCard.tsx @@ -1,4 +1,4 @@ -import { BlogPost } from 'edge-info-server/types' +import { BlogPost } from 'edge-info-server' import * as React from 'react' import { View } from 'react-native' import FastImage from 'react-native-fast-image' @@ -33,22 +33,23 @@ export const BlogCard = (props: Props) => { const handlePress = useHandler(() => { if (url != null) openBrowserUri(url) }) + const imageSrc = React.useMemo(() => ({ uri: image }), [image]) return ( - + } > - + {title} - + {body} @@ -65,10 +66,12 @@ const getStyles = cacheStyles((theme: Theme) => ({ }, titleText: { fontFamily: theme.fontFaceMedium, - marginBottom: theme.rem(0.25) + marginBottom: theme.rem(0.25), + ...theme.cardTextShadow }, bodyText: { - fontSize: theme.rem(0.65) + fontSize: theme.rem(0.65), + ...theme.cardTextShadow }, nodeBackground: { height: IMAGE_HEIGHT_RATIO, diff --git a/src/components/ui4/ButtonUi4.tsx b/src/components/ui4/ButtonUi4.tsx index 0833d361490..217a97f3be5 100644 --- a/src/components/ui4/ButtonUi4.tsx +++ b/src/components/ui4/ButtonUi4.tsx @@ -22,10 +22,6 @@ interface Props { // and show a spinner until the promise resolves. onPress?: () => void | Promise - // Whether to center the button or stretch to fill the screen. - // Defaults to 'auto', letting the parent component be in charge: - alignSelf?: 'auto' | 'stretch' | 'center' // TODO: Maybe also remove this once column layout is restyled for UI4 - // True to dim the button & prevent interactions: disabled?: boolean @@ -59,20 +55,7 @@ interface Props { * - NOT meant to be used on its own outside of ButtonsViewUi4 unless layout='solo' */ export function ButtonUi4(props: Props) { - const { - layout = 'solo', - alignSelf = 'auto', - children, - disabled = false, - label, - onPress, - type = 'primary', - spinner = false, - mini = false, - marginRem, - paddingRem, - testID - } = props + const { layout = 'solo', children, disabled = false, label, onPress, type = 'primary', spinner = false, mini = false, marginRem, paddingRem, testID } = props // `onPress` promise logic: const [pending, handlePress] = usePendingPress(onPress) @@ -117,14 +100,6 @@ export function ButtonUi4(props: Props) { // manually enabled. const hideContent = pending || spinner - const dynamicGradientStyles = React.useMemo( - () => ({ - alignSelf, - opacity: disabled ? 0.3 : hideContent ? 0.7 : 1 - }), - [alignSelf, disabled, hideContent] - ) - const maybeText = label == null ? null : ( @@ -132,32 +107,56 @@ export function ButtonUi4(props: Props) { ) - const containerStyle = React.useMemo(() => { - const retStyle: ViewStyle[] = [styles.containerCommon] - if (layout === 'column') retStyle.push(styles.containerColumn) - if (layout === 'row') retStyle.push(styles.containerRow) - if (layout === 'solo') retStyle.push(styles.containerSolo) + const touchContainerStyle = React.useMemo(() => { + const retStyle: ViewStyle[] = [styles.touchContainerCommon] + + if (layout === 'column') retStyle.push(styles.touchContainerColumn) + if (layout === 'row') retStyle.push(styles.touchContainerRow) + if (layout === 'solo') retStyle.push(styles.touchContainerSolo) + + const customMargin = marginRem == null ? undefined : sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)) + retStyle.push( + customMargin != null + ? { + // Use margin as padding to increase tappable area + paddingLeft: customMargin.marginLeft, + paddingRight: customMargin.marginRight, + paddingTop: customMargin.marginTop, + paddingBottom: customMargin.marginBottom + } + : styles.touchContainerSpacing + ) - if (type === 'tertiary') retStyle.push(styles.containerTertiary) return retStyle - }, [layout, styles.containerColumn, styles.containerCommon, styles.containerRow, styles.containerSolo, styles.containerTertiary, type]) + }, [layout, marginRem, styles, theme]) - const customMargin = marginRem == null ? undefined : sidesToMargin(mapSides(fixSides(marginRem, 0), theme.rem)) - const customPadding = paddingRem == null ? undefined : sidesToPadding(mapSides(fixSides(paddingRem, 0), theme.rem)) - const finalContainerCommon = React.useMemo( - () => [styles.containerCommon, containerStyle, customMargin, customPadding], - [containerStyle, customMargin, customPadding, styles.containerCommon] - ) + const visibleContainerStyle = React.useMemo(() => { + const retStyle: ViewStyle[] = [styles.visibleContainerCommon] + + if (layout === 'column') retStyle.push(styles.visibleContainerColumn) + if (layout === 'row') retStyle.push(styles.visibleContainerRow) + if (layout === 'solo') retStyle.push(styles.visibleContainerSolo) + if (type === 'tertiary') retStyle.push(styles.visibleContainerTertiary) + + retStyle.push(mini ? styles.visibleSizeMini : type === 'tertiary' ? styles.visibleSizeTertiary : styles.visibleSizeDefault) + + if (paddingRem != null) { + retStyle.push(sidesToPadding(mapSides(fixSides(paddingRem, 0), theme.rem))) + } + + retStyle.push({ + opacity: disabled ? 0.3 : hideContent ? 0.7 : 1 + }) + + return retStyle + }, [disabled, hideContent, layout, mini, paddingRem, styles, theme, type]) return ( - - + + {hideContent ? null : children} {hideContent ? null : maybeText} - {!hideContent ? null : } + {!hideContent ? null : } ) @@ -165,48 +164,71 @@ export function ButtonUi4(props: Props) { const getStyles = cacheStyles((theme: Theme) => { return { - // Common styles: - spinnerCommon: { - height: theme.rem(2) - }, - containerCommon: { - borderRadius: theme.rem(theme.buttonBorderRadiusRem), - alignSelf: 'stretch', + // Invisible Touchable Container Styles: + touchContainerCommon: { alignItems: 'center', justifyContent: 'center' }, - contentCommon: { + touchContainerSpacing: { + // Combination of negative margin and positive padding to increase + // invisible tappable area outside of the bounds of the visible button + margin: -theme.rem(0.5), + padding: theme.rem(0.5) + }, + touchContainerRow: { + alignSelf: 'stretch', + flex: 1 // Size equally against other buttons in the row + }, + touchContainerColumn: { + alignSelf: 'stretch', + flexBasis: 'auto', + flexGrow: 0, + flexShrink: 0 + }, + touchContainerSolo: { + alignSelf: 'center', + flexBasis: 'auto', + flexGrow: 0, + flexShrink: 0 + }, + // Visible Container Styles + visibleContainerCommon: { + borderRadius: theme.rem(theme.buttonBorderRadiusRem), + flexGrow: 0, + flexShrink: 0, alignItems: 'center', - flexDirection: 'row', - justifyContent: 'center' + justifyContent: 'center', + flexDirection: 'row' }, - - // Other styles: - contentSizeDefault: { - paddingHorizontal: theme.rem(2), + visibleSizeDefault: { + paddingHorizontal: theme.rem(1.5), height: theme.rem(3) }, - contentSizeMini: { - paddingHorizontal: theme.rem(1.5), + visibleSizeMini: { + alignSelf: 'center', + paddingHorizontal: theme.rem(1.25), height: theme.rem(2) }, - containerColumn: { + visibleSizeTertiary: { + // Reduce the bounds of a tertiary button so it doesn't appear to be too + // far from other buttons + padding: 0, + height: undefined + }, + visibleContainerColumn: { alignSelf: 'stretch' }, - containerSolo: { - alignSelf: 'center' + visibleContainerRow: { + alignSelf: 'stretch' }, - containerRow: { - flex: 1 + visibleContainerSolo: { + alignSelf: 'center' }, - containerTertiary: { - // Reduce the bounds of a tertiary button so it doesn't appear to be too - // far from other buttons - alignSelf: 'center', - paddingHorizontal: 0, - paddingVertical: 0, - height: undefined + visibleContainerTertiary: { + alignSelf: 'center' }, + + // Content primaryText: { fontFamily: theme.primaryButtonFont, fontSize: theme.rem(theme.primaryButtonFontSizeRem), @@ -224,6 +246,9 @@ const getStyles = cacheStyles((theme: Theme) => { }, leftMarginedText: { marginLeft: theme.rem(0.5) + }, + spinner: { + height: theme.rem(2) } } }) diff --git a/src/components/ui4/ButtonsViewUi4.tsx b/src/components/ui4/ButtonsViewUi4.tsx index c3e2eb51ea8..4c1afd375d9 100644 --- a/src/components/ui4/ButtonsViewUi4.tsx +++ b/src/components/ui4/ButtonsViewUi4.tsx @@ -131,14 +131,16 @@ export const StyledButtonContainer = styled(View)<{ absolute?: boolean; layout: justifyContent: 'center', marginHorizontal: theme.rem(0.5), alignItems: 'center', - flex: 1 + flexGrow: 1, + flexShrink: 1 } : {} const rowStyle: ViewStyle = layout === 'row' ? { - flex: 1, + flexGrow: 1, + flexShrink: 1, flexDirection: 'row-reverse', justifyContent: 'center' } diff --git a/src/components/ui4/CardUi4.tsx b/src/components/ui4/CardUi4.tsx index dec8236c3c6..f9309dc708d 100644 --- a/src/components/ui4/CardUi4.tsx +++ b/src/components/ui4/CardUi4.tsx @@ -92,6 +92,8 @@ export const CardUi4 = (props: Props) => { } } }) + const imageSrc = React.useMemo(() => (typeof icon === 'string' ? { uri: icon } : { uri: '' }), [icon]) + const viewStyle = React.useMemo(() => [styles.cardContainer, margin, padding, fillStyle], [styles.cardContainer, margin, padding, fillStyle]) const nonNullChildren = React.Children.toArray(children).filter(child => child != null && React.isValidElement(child)) if (nonNullChildren.length === 0) return null @@ -105,7 +107,7 @@ export const CardUi4 = (props: Props) => { const maybeIcon = icon == null ? null : ( - {typeof icon === 'string' ? : icon} + {typeof icon === 'string' ? : icon} ) const content = sections ? {children} : children @@ -140,11 +142,11 @@ export const CardUi4 = (props: Props) => { ) return isPressable ? ( - + {allContent} ) : ( - {allContent} + {allContent} ) } @@ -175,7 +177,8 @@ const getStyles = cacheStyles((theme: Theme) => ({ pointerEvents: 'none' }, iconRowContainer: { - flex: 1, + flexGrow: 1, + flexShrink: 1, flexDirection: 'row', alignItems: 'center' }, diff --git a/src/components/ui4/CarouselUi4.tsx b/src/components/ui4/CarouselUi4.tsx index f32e0d95c36..202fbdf44db 100644 --- a/src/components/ui4/CarouselUi4.tsx +++ b/src/components/ui4/CarouselUi4.tsx @@ -58,7 +58,7 @@ export function CarouselUi4(props: Props): JSX.Element { }} dotsLength={data.length} activeDotIndex={activeIndex} - tappableDots + tappableDots={carouselRef.current != null} dotStyle={styles.dotStyle} inactiveDotOpacity={0.4} inactiveDotScale={0.7} diff --git a/src/components/ui4/CryptoIconUi4.tsx b/src/components/ui4/CryptoIconUi4.tsx index 12c7fc645a8..c1132033fa9 100644 --- a/src/components/ui4/CryptoIconUi4.tsx +++ b/src/components/ui4/CryptoIconUi4.tsx @@ -62,13 +62,13 @@ export const CryptoIconUi4 = (props: Props) => { const source = { uri: primaryCurrencyIconUrl } // Return Currency logo from the edge server - return + return source }, [primaryCurrencyIconUrl]) // Secondary (parent) currency icon (if it's a token) const secondaryCurrencyIcon = React.useMemo(() => { if (compromised) { - return + return compromisedIcon } // Skip if this is not a token: @@ -80,9 +80,9 @@ export const CryptoIconUi4 = (props: Props) => { const icon = getCurrencyIconUris(pluginId, null) const source = { uri: mono ? icon.symbolImageDarkMono : icon.symbolImage } + return source // Return Parent logo from the edge server - return - }, [compromised, mono, pluginId, styles.parentIcon, tokenId]) + }, [compromised, mono, pluginId, tokenId]) // Main view styling const spacingStyle = React.useMemo( @@ -94,13 +94,16 @@ export const CryptoIconUi4 = (props: Props) => { [marginRem, size, theme] ) - const shadowStyle = { - height: size, - width: size, - borderRadius: size / 2, - backgroundColor: theme.iconShadow.shadowColor, - ...theme.iconShadow - } + const shadowStyle = React.useMemo( + () => ({ + height: size, + width: size, + borderRadius: size / 2, + backgroundColor: theme.iconShadow.shadowColor, + ...theme.iconShadow + }), + [size, theme] + ) return ( @@ -113,8 +116,8 @@ export const CryptoIconUi4 = (props: Props) => { /> )} - {primaryCurrencyIcon} - {hideSecondary ? null : secondaryCurrencyIcon} + {primaryCurrencyIcon != null ? : null} + {hideSecondary ? null : secondaryCurrencyIcon != null ? : null} ) diff --git a/src/components/ui4/CurrencyViewUi4.tsx b/src/components/ui4/CurrencyViewUi4.tsx index 2f69632c0a7..273a0165d4e 100644 --- a/src/components/ui4/CurrencyViewUi4.tsx +++ b/src/components/ui4/CurrencyViewUi4.tsx @@ -119,7 +119,8 @@ const getStyles = cacheStyles((theme: Theme) => ({ alignItems: 'center', marginHorizontal: theme.rem(0.5), marginVertical: theme.rem(0.25), - flex: 1 + flexGrow: 1, + flexShrink: 1 }, iconContainer: { marginRight: theme.rem(1) @@ -127,7 +128,8 @@ const getStyles = cacheStyles((theme: Theme) => ({ innerContainer: { flexDirection: 'column', justifyContent: 'space-between', - flex: 1 + flexGrow: 1, + flexShrink: 1 }, primaryText: { fontSize: theme.rem(0.75), diff --git a/src/components/ui4/FiatExchangeDetailsCard.tsx b/src/components/ui4/FiatExchangeDetailsCard.tsx new file mode 100644 index 00000000000..313a190e365 --- /dev/null +++ b/src/components/ui4/FiatExchangeDetailsCard.tsx @@ -0,0 +1,159 @@ +import { EdgeAssetAction, EdgeCurrencyWallet, EdgeTransaction, EdgeTxActionFiat } from 'edge-core-js' +import * as React from 'react' +import { Linking, Platform, View } from 'react-native' +import Mailer from 'react-native-mail' +import SafariView from 'react-native-safari-view' +import { sprintf } from 'sprintf-js' + +import { useAsyncEffect } from '../../hooks/useAsyncEffect' +import { useHandler } from '../../hooks/useHandler' +import { lstrings } from '../../locales/strings' +import { useState } from '../../types/reactHooks' +import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' +import { unixToLocaleDateTime } from '../../util/utils' +import { RawTextModal } from '../modals/RawTextModal' +import { Airship, showError } from '../services/AirshipInstance' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' +import { EdgeText } from '../themed/EdgeText' +import { CardUi4 } from './CardUi4' +import { RowUi4 } from './RowUi4' + +interface Props { + action: EdgeTxActionFiat + assetAction: EdgeAssetAction + transaction: EdgeTransaction + wallet: EdgeCurrencyWallet +} + +export function FiatExchangeDetailsCard(props: Props) { + const { action, assetAction, transaction, wallet } = props + const theme = useTheme() + const styles = getStyles(theme) + const [sourceAmount, setSourceAmount] = useState('') + const [destinationAmount, setDestinationAmount] = useState('') + + const { + orderId, + orderUri, + isEstimate, + fiatPlugin: { providerDisplayName, supportEmail }, + payinAddress, + payoutAddress, + fiatAsset, + cryptoAsset + } = action + + const { tokenId, pluginId, nativeAmount } = cryptoAsset + + const { assetActionType: direction } = assetAction + + const createExchangeDataString = (nl: string = '\n') => { + const { dateTime } = unixToLocaleDateTime(transaction.date) + + const exchangeData = [ + [lstrings.fio_date_label, dateTime], + [lstrings.transaction_details_exchange_service, providerDisplayName], + [lstrings.transaction_details_exchange_order_id, orderId], + [isEstimate ? lstrings.estimated_quote : lstrings.fixed_quote, undefined] + ] + if (payinAddress != null) exchangeData.push([lstrings.transaction_details_exchange_exchange_address, payinAddress]) + if (payoutAddress != null) exchangeData.push([lstrings.transaction_details_exchange_payout_address, payoutAddress]) + + return exchangeData.map(([key, value]) => (value == null ? key : `${key}: ${value}`)).join(nl) + } + + const handleExchangeDetails = useHandler(async () => { + await Airship.show(bridge => ) + }) + + const handleEmail = useHandler(() => { + const email = supportEmail + const body = createExchangeDataString('
') + + Mailer.mail( + { + subject: sprintf(lstrings.transaction_details_exchange_support_request, providerDisplayName), + // @ts-expect-error + recipients: [email], + body, + isHTML: true + }, + (error, event) => { + if (error) showError(error) + } + ) + }) + + const handleLink = async () => { + if (orderUri == null) return + + if (Platform.OS === 'ios') { + SafariView.isAvailable() + .then(async available => { + if (available) await SafariView.show({ url: orderUri }) + else await Linking.openURL(orderUri) + }) + .catch(error => { + showError(error) + Linking.openURL(orderUri).catch(err => showError(err)) + }) + } else { + await Linking.openURL(orderUri) + } + } + + const currencyCode = getCurrencyCode(wallet, tokenId) + let sourceCurrencyCode = '' + let destinationCurrencyCode = '' + if (direction === 'buy') { + sourceCurrencyCode = fiatAsset.fiatCurrencyCode + destinationCurrencyCode = currencyCode + } else { + sourceCurrencyCode = currencyCode + destinationCurrencyCode = fiatAsset.fiatCurrencyCode + } + useAsyncEffect( + async () => { + const exchangeAmount = nativeAmount != null ? await wallet.nativeToDenomination(nativeAmount, currencyCode) : '' + if (direction === 'buy') { + setSourceAmount(`${fiatAsset.fiatAmount} `) + setDestinationAmount(`${exchangeAmount} `) + } else { + setDestinationAmount(`${fiatAsset.fiatAmount} `) + setSourceAmount(`${exchangeAmount} `) + } + }, + [], + 'FiatExchangeDetailsCard' + ) + + if (pluginId !== wallet.currencyInfo.pluginId) return null + if (action.actionType !== 'fiat') return null + if (direction !== 'buy' && direction !== 'sell') return null + + return ( + + + + {lstrings.title_exchange + ' ' + sourceAmount + sourceCurrencyCode} + {lstrings.string_to_capitalize + ' ' + destinationAmount + destinationCurrencyCode} + {isEstimate ? lstrings.estimated_quote : lstrings.fixed_quote} + + + + {orderUri == null ? null : ( + + )} + {supportEmail == null ? null : ( + + )} + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + tileColumn: { + flexDirection: 'column', + justifyContent: 'center' + } +})) diff --git a/src/components/ui4/HomeCardUi4.tsx b/src/components/ui4/HomeCardUi4.tsx index 7416720e24e..0cc6258115e 100644 --- a/src/components/ui4/HomeCardUi4.tsx +++ b/src/components/ui4/HomeCardUi4.tsx @@ -32,7 +32,7 @@ export const HomeCardUi4 = (props: Props) => { {title} - + {footer} @@ -42,10 +42,11 @@ export const HomeCardUi4 = (props: Props) => { const getStyles = cacheStyles((theme: Theme) => ({ footerText: { - fontSize: theme.rem(0.75) + fontSize: theme.rem(0.75), + ...theme.cardTextShadow }, verticalSplitContainer: { - flex: 1, // Make sure the card fills the space + flex: 1, // Make sure the card fills the space evenly compared to the other HomeCards justifyContent: 'space-between', // Aligns title to the top, footer to the bottom margin: theme.rem(0.5) } diff --git a/src/components/ui4/MarketsCardUi4.tsx b/src/components/ui4/MarketsCardUi4.tsx index 1832114f94a..698fb5225f5 100644 --- a/src/components/ui4/MarketsCardUi4.tsx +++ b/src/components/ui4/MarketsCardUi4.tsx @@ -46,60 +46,70 @@ interface Props { numRows: number } -/** - * Card that displays balance, deposit/send buttons, and a link to view assets - */ -export const MarketsCardUi4 = (props: Props) => { - const { navigation, numRows } = props +interface CoinRowProps { + coinRow: CoinRankingData + index: number + navigation: NavigationBase +} + +const CoinRow = (props: CoinRowProps) => { + const { coinRow, index, navigation } = props const theme = useTheme() const styles = getStyles(theme) - const defaultIsoFiat = useSelector(state => `iso:${getDefaultFiat(state)}`) const defaultFiat = useSelector(state => getDefaultFiat(state)) const fiatSymbol = React.useMemo(() => getSymbolFromCurrency(defaultFiat), [defaultFiat]) - const [coinRankingDatas, setCoinRankingDatas] = React.useState([]) + const { assetId, currencyCode, price, percentChange, imageUrl } = coinRow + const key = `${index}-${currencyCode}` - const renderCoinRow = (coinRow: CoinRankingData, index: number) => { - const { assetId, currencyCode, price, percentChange, imageUrl } = coinRow - const key = `${index}-${currencyCode}` + // Price & percent change string + const percentChangeRaw = String(percentChange.hours24) + const decimalChangeRaw = div(percentChangeRaw, '100', DECIMAL_PRECISION) - // Price & percent change string - const percentChangeRaw = String(percentChange.hours24) - const decimalChangeRaw = div(percentChangeRaw, '100', DECIMAL_PRECISION) + const percentString = toPercentString(decimalChangeRaw, { plusSign: true, intlOpts: { noGrouping: true } }) + const percentStyle = lt(percentChangeRaw, '0') ? styles.negativeText : styles.positiveText - const percentString = toPercentString(decimalChangeRaw, { plusSign: true, intlOpts: { noGrouping: true } }) - const percentStyle = lt(percentChangeRaw, '0') ? styles.negativeText : styles.positiveText + const priceString = `${fiatSymbol}${formatFiatString({ fiatAmount: price.toString() })} ` - const priceString = `${fiatSymbol}${formatFiatString({ fiatAmount: price.toString() })} ` + // See if we have an Edge custom icons from a small list of top assets - // See if we have an Edge custom icons from a small list of top assets + const imageSrc = React.useMemo(() => { let edgeIconUri const edgeAsset = COINGECKO_TO_EDGE_ASSET[assetId] if (edgeAsset != null) { const icon = getCurrencyIconUris(edgeAsset.pluginId, edgeAsset.tokenId) edgeIconUri = icon.symbolImage } - const iconUrl = edgeIconUri ?? imageUrl - - return ( - } - onPress={() => navigation.navigate('coinRankingDetails', { coinRankingData: coinRow })} - rightButtonType="none" - > - - {currencyCode.toUpperCase() ?? 'N/A'} - - {priceString} - {percentString} - + return { uri: iconUrl } + }, [assetId, imageUrl]) + + return ( + } + onPress={() => navigation.navigate('coinRankingDetails', { coinRankingData: coinRow })} + rightButtonType="none" + > + + {currencyCode.toUpperCase() ?? 'N/A'} + + {priceString} + {percentString} - - ) - } + + + ) +} + +/** + * Card that displays market summary info for top coins + */ +export const MarketsCardUi4 = (props: Props) => { + const { numRows } = props + const defaultIsoFiat = useSelector(state => `iso:${getDefaultFiat(state)}`) + const [coinRankingDatas, setCoinRankingDatas] = React.useState([]) /** * Fetch Markets Data @@ -138,7 +148,13 @@ export const MarketsCardUi4 = (props: Props) => { return () => task.stop() }, [defaultIsoFiat, numRows]) - return {coinRankingDatas.map((coinRow, index) => renderCoinRow(coinRow, index))} + return ( + + {coinRankingDatas.map((coinRow, index) => ( + + ))} + + ) } const getStyles = cacheStyles((theme: Theme) => ({ @@ -151,7 +167,8 @@ const getStyles = cacheStyles((theme: Theme) => ({ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - flex: 1, + flexGrow: 1, + flexShrink: 1, marginHorizontal: theme.rem(0.5) }, rowRight: { diff --git a/src/components/ui4/ModalUi4.tsx b/src/components/ui4/ModalUi4.tsx index fa041eaf1d1..133f78c48ec 100644 --- a/src/components/ui4/ModalUi4.tsx +++ b/src/components/ui4/ModalUi4.tsx @@ -210,15 +210,27 @@ const getStyles = cacheStyles((theme: Theme) => ({ alignSelf: 'flex-start', alignItems: 'flex-end', justifyContent: 'flex-start', - paddingTop: theme.rem(0.15), // Bake in margins to align with 1 line of text, no matter the number of lines - marginRight: theme.rem(0.25) // Less margins because the icon itself comes with whitespace + // Increase tappable area with padding, while net X with negative margin to visually appear as if X padding + paddingTop: theme.rem(1.15), // Bake in margins to align with 1 line of text, no matter the number of lines + paddingRight: theme.rem(1.25), // Less margins because the icon itself comes with whitespace + paddingBottom: theme.rem(0.75), + marginTop: -theme.rem(1), + marginRight: -theme.rem(1), + marginBottom: -theme.rem(0.75) }, closeIconContainerAbsolute: { // Used when the caller passes a special title that may span the entire // width. It's up to the caller to ensure there's no overlap with the close button. position: 'absolute', - top: theme.rem(0.15), // Bake in margins to align with 1 line of text, which is often supplied in custom headers. - right: theme.rem(0.25) + top: 0, + right: 0, + paddingTop: theme.rem(1), // Bake in margins to align with 1 line of text, no matter the number of lines + paddingRight: theme.rem(1.25), // Less margins because the icon itself comes with whitespace + paddingBottom: theme.rem(0.75), + paddingLeft: theme.rem(1), + marginTop: -theme.rem(1), + marginRight: -theme.rem(1), + marginBottom: -theme.rem(0.75) }, titleContainer: { flexDirection: 'row', diff --git a/src/components/ui4/PromoCardUi4.tsx b/src/components/ui4/PromoCardUi4.tsx index aeac78fa49f..4e5887e7366 100644 --- a/src/components/ui4/PromoCardUi4.tsx +++ b/src/components/ui4/PromoCardUi4.tsx @@ -1,4 +1,4 @@ -import { PromoCard2 } from 'edge-info-server/types' +import { PromoCard2 } from 'edge-info-server' import * as React from 'react' import { View } from 'react-native' import FastImage from 'react-native-fast-image' @@ -54,18 +54,14 @@ export function PromoCardUi4(props: Props) { dispatch(linkReferralWithCurrencies(navigation, url)).catch(err => showError(err)) }) + const imageSrc = React.useMemo(() => ({ uri: imageUri }), [imageUri]) + return ( - +
} > diff --git a/src/components/ui4/PromoCardsUi4.tsx b/src/components/ui4/PromoCardsUi4.tsx index f02ece376db..6c559be4252 100644 --- a/src/components/ui4/PromoCardsUi4.tsx +++ b/src/components/ui4/PromoCardsUi4.tsx @@ -1,17 +1,18 @@ -import { asArray, asDate } from 'cleaners' -import { asInfoRollup, asPromoCard2, PromoCard2 } from 'edge-info-server/types' +import { asDate } from 'cleaners' +import { PromoCard2 } from 'edge-info-server' import * as React from 'react' import { ListRenderItem, Platform } from 'react-native' -import DeviceInfo, { getBuildNumber, getVersion } from 'react-native-device-info' +import { getBuildNumber, getVersion } from 'react-native-device-info' import shajs from 'sha.js' import { getCountryCodeByIp, hideMessageTweak } from '../../actions/AccountReferralActions' +import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { useHandler } from '../../hooks/useHandler' -import { config } from '../../theme/appConfig' import { useDispatch, useSelector } from '../../types/reactRedux' import { NavigationBase } from '../../types/routerTypes' -import { fetchInfo } from '../../util/network' -import { EdgeAnim } from '../common/EdgeAnim' +import { infoServerData } from '../../util/network' +import { getOsVersion } from '../../util/utils' +import { EdgeAnim, fadeInUp110 } from '../common/EdgeAnim' import { useTheme } from '../services/ThemeContext' import { CarouselUi4 } from './CarouselUi4' import { FilteredPromoCard, PromoCardUi4 } from './PromoCardUi4' @@ -29,15 +30,16 @@ export const PromoCardsUi4 = (props: Props) => { const [cards, setCards] = React.useState([]) // Check for PromoCard2 from info server: - React.useEffect(() => { - fetchPromoCards() - .then(async cards => { - const countryCode = await getCountryCodeByIp() - const filteredCards = filterPromoCards(cards, countryCode) - setCards(filteredCards) - }) - .catch(e => console.log(e)) - }, []) + useAsyncEffect( + async () => { + const cards = infoServerData.rollup?.promoCards2 ?? [] + const countryCode = await getCountryCodeByIp().catch(() => '') + const filteredCards = filterPromoCards(cards, countryCode) + setCards(filteredCards) + }, + [], + 'PromoCardsUi4' + ) const hiddenAccountMessages = useSelector(state => state.account.accountReferral.hiddenAccountMessages) const activeCards = React.useMemo(() => cards.filter(card => !hiddenAccountMessages[card.messageId]), [cards, hiddenAccountMessages]) @@ -50,41 +52,16 @@ export const PromoCardsUi4 = (props: Props) => { } return }) + const style = React.useMemo(() => ({ height: theme.rem(11.5) }), [theme]) if (activeCards == null || activeCards.length === 0) return null - return ( - + ) } -/** - * Reads and normalizes the OS version. - */ -function getOsVersion(): string { - const osVersionRaw = DeviceInfo.getSystemVersion() - return Array.from({ length: 3 }, (_, i) => osVersionRaw.split('.')[i] || '0').join('.') -} - -/** - * Visits the info server to obtain relevant promotion cards. - */ -async function fetchPromoCards(): Promise { - const osType = Platform.OS.toLowerCase() - const osVersion = getOsVersion() - const version = getVersion() - - // Visit the server: - const res = await fetchInfo(`v1/inforollup/${config.appId ?? 'edge'}?os=${osType}&osVersion=${osVersion}&appVersion=${version}`) - if (!res.ok) { - throw new Error(`Info server error ${res.status}: ${await res.text()}`) - } - const infoData = await res.json() - return asArray(asPromoCard2)(asInfoRollup(infoData).promoCards2) -} - /** * Finds the promo cards that are relevant to our application version & * other factors. diff --git a/src/components/ui4/SectionHeaderUi4.tsx b/src/components/ui4/SectionHeaderUi4.tsx index afc85c447e6..278cf691693 100644 --- a/src/components/ui4/SectionHeaderUi4.tsx +++ b/src/components/ui4/SectionHeaderUi4.tsx @@ -18,7 +18,7 @@ interface Props { /** * A view representing rows of data split on the left and right edges of the - * line. Neither side will exceed 50% of the width of the view. + * line. * * If the right side is a string and onRightPress handler is provided, it will * be rendered as green tappable text, else it's up to the caller to decide. @@ -47,8 +47,12 @@ export const SectionHeaderUi4 = (props: Props) => { const getStyles = cacheStyles((theme: Theme) => ({ rightTappableContainer: { - flex: 1, - justifyContent: 'flex-end' + flexGrow: 1, + flexShrink: 1, + justifyContent: 'flex-end', + // Increase tappable area with padding, while net 0 with negative margin to visually appear as if 0 margins/padding + padding: theme.rem(0.75), + margin: -theme.rem(0.75) }, tappableText: { color: theme.iconTappable, diff --git a/src/components/ui4/SectionView.tsx b/src/components/ui4/SectionView.tsx index b2db9e7952d..d32950af400 100644 --- a/src/components/ui4/SectionView.tsx +++ b/src/components/ui4/SectionView.tsx @@ -72,7 +72,8 @@ export const SectionView = (props: Props): JSX.Element | null => { const getStyles = cacheStyles((theme: Theme) => ({ container: { flexDirection: 'column', - flex: 1 + flexGrow: 1, + flexShrink: 1 }, marginCard: { marginVertical: theme.rem(0) diff --git a/src/components/ui4/SwapDetailsCard.tsx b/src/components/ui4/SwapDetailsCard.tsx index c2e3e38bfa5..c57b4ec24f0 100644 --- a/src/components/ui4/SwapDetailsCard.tsx +++ b/src/components/ui4/SwapDetailsCard.tsx @@ -48,18 +48,21 @@ export function SwapDetailsCard(props: Props) { }) const handleEmail = useHandler(() => { - const email = plugin.supportEmail const body = createExchangeDataString('
') Mailer.mail( { subject: sprintf(lstrings.transaction_details_exchange_support_request, plugin.displayName), - // @ts-expect-error - recipients: [email], + recipients: plugin.supportEmail != null ? [plugin.supportEmail] : undefined, body, isHTML: true }, (error, event) => { + if (String(error) === 'not_available') { + showError(lstrings.error_no_email_account) + return + } + if (error) showError(error) } ) diff --git a/src/components/ui4/scenes/HomeSceneUi4.tsx b/src/components/ui4/scenes/HomeSceneUi4.tsx index 287a44e6674..59a082aeefe 100644 --- a/src/components/ui4/scenes/HomeSceneUi4.tsx +++ b/src/components/ui4/scenes/HomeSceneUi4.tsx @@ -1,9 +1,9 @@ -import { asBlogPosts, BlogPost } from 'edge-info-server/types' +import { asBlogPosts, BlogPost } from 'edge-info-server' import * as React from 'react' import { ListRenderItem, View } from 'react-native' import FastImage from 'react-native-fast-image' import Animated from 'react-native-reanimated' -import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context' +import { useSafeAreaFrame } from 'react-native-safe-area-context' import { showBackupForTransferModal } from '../../../actions/BackupModalActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../../constants/constantSettings' @@ -15,7 +15,7 @@ import { useSelector } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { getUi4ImageUri } from '../../../util/CdnUris' import { fetchInfo } from '../../../util/network' -import { EdgeAnim } from '../../common/EdgeAnim' +import { EdgeAnim, fadeInUp30, fadeInUp60, fadeInUp80, fadeInUp140 } from '../../common/EdgeAnim' import { SceneWrapper } from '../../common/SceneWrapper' import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext' import { WiredProgressBar } from '../../themed/WiredProgressBar' @@ -40,9 +40,6 @@ export const HomeSceneUi4 = (props: Props) => { const { width: screenWidth } = useSafeAreaFrame() - // TODO: Include this fix in the SceneWrapper component - const safeAreaInsets = useSafeAreaInsets() - // Evenly distribute the home cards into 4 quadrants: const cardSize = screenWidth / 2 - theme.rem(TEMP_PADDING_REM) @@ -88,6 +85,12 @@ export const HomeSceneUi4 = (props: Props) => { const renderBlog: ListRenderItem = useHandler(({ item }) => ) + const buyCryptoIcon = React.useMemo(() => ({ uri: getUi4ImageUri(theme, 'cardBackgrounds/bg-buy-crypto') }), [theme]) + const sellCryptoIcon = React.useMemo(() => ({ uri: getUi4ImageUri(theme, 'cardBackgrounds/bg-sell-crypto') }), [theme]) + const fioIcon = React.useMemo(() => ({ uri: getUi4ImageUri(theme, 'cardBackgrounds/bg-fio') }), [theme]) + const tradeCryptoIcon = React.useMemo(() => ({ uri: getUi4ImageUri(theme, 'cardBackgrounds/bg-trade') }), [theme]) + const homeRowStyle = React.useMemo(() => [styles.homeRowContainer, { height: cardSize }], [styles, cardSize]) + return ( {({ insetStyle, undoInsetStyle }) => ( @@ -96,28 +99,24 @@ export const HomeSceneUi4 = (props: Props) => { <> - + {/* Animation inside PromoCardsUi4 component */} - + - +
} onPress={handleBuyPress} @@ -128,24 +127,20 @@ export const HomeSceneUi4 = (props: Props) => { gradientBackground={theme.sellCardGradient} nodeBackground={ - + } onPress={handleSellPress} />
- + - + } onPress={handleFioPress} @@ -156,7 +151,7 @@ export const HomeSceneUi4 = (props: Props) => { gradientBackground={theme.swapCardGradient} nodeBackground={ - + } onPress={handleSwapPress} @@ -165,7 +160,7 @@ export const HomeSceneUi4 = (props: Props) => { <> navigation.navigate('coinRanking', {})} /> - + diff --git a/src/constants/WalletAndCurrencyConstants.ts b/src/constants/WalletAndCurrencyConstants.ts index 8e48e02d713..f5b44353206 100644 --- a/src/constants/WalletAndCurrencyConstants.ts +++ b/src/constants/WalletAndCurrencyConstants.ts @@ -108,9 +108,6 @@ export const WALLET_TYPE_ORDER = [ // Put these in reverse order of preference export const PREFERRED_TOKENS = ['WINGS', 'HERC', 'REPV2', 'RIF'] -// Strip away 'wallet:' prefix and '-bip' suffix, if present -export const getPluginId = (walletType: string): string => walletType.replace('wallet:', '').split('-')[0] - export interface ImportKeyOption { optionName: string displayName: string @@ -332,6 +329,32 @@ export const SPECIAL_CURRENCY_INFO: { reference: '1' } }, + arbitrum: { + initWalletName: lstrings.string_first_arbitrum_wallet_name, + chainCode: 'ETH', + dummyPublicAddress: '0x0d73358506663d484945ba85d0cd435ad610b0a0', + allowZeroTx: true, + isImportKeySupported: true, + isCustomTokensSupported: true, + isPaymentProtocolSupported: false, + walletConnectV2ChainId: { + namespace: 'eip155', + reference: '42161' + } + }, + base: { + initWalletName: lstrings.string_first_base_wallet_name, + chainCode: 'ETH', + dummyPublicAddress: '0x0d73358506663d484945ba85d0cd435ad610b0a0', + allowZeroTx: true, + isImportKeySupported: true, + isCustomTokensSupported: true, + isPaymentProtocolSupported: false, + walletConnectV2ChainId: { + namespace: 'eip155', + reference: '8453' + } + }, filecoin: { initWalletName: lstrings.string_first_filecoin_wallet_name, chainCode: 'FIL', @@ -460,19 +483,49 @@ export const SPECIAL_CURRENCY_INFO: { dummyPublicAddress: 'tz1cVgSd4oY25pDkH7vdvVp5DfPkZwT2hXwX', isImportKeySupported: true }, + axelar: { + initWalletName: lstrings.string_first_axelar_wallet_name, + chainCode: 'AXL', + dummyPublicAddress: 'axelar1hap5ld4fl82wjn67j96unpgee5yxh0njs0eswf', + isStakingSupported: false, + isImportKeySupported: true, + walletConnectV2ChainId: { + namespace: 'cosmos', + reference: 'axelar-dojo-1' + } + }, coreum: { initWalletName: lstrings.string_first_coreum_wallet_name, - chainCode: 'CORE', + chainCode: 'COREUM', dummyPublicAddress: 'core18rv2a6cjkk3lnayy29hez6s2ftpe9llqnce2vu', isStakingSupported: true, - isImportKeySupported: true + isImportKeySupported: true, + walletConnectV2ChainId: { + namespace: 'cosmos', + reference: 'coreum-mainnet-1' + } + }, + cosmoshub: { + initWalletName: lstrings.string_first_cosmoshub_wallet_name, + chainCode: 'ATOM', + dummyPublicAddress: 'cosmos1ucnamh638lpgqraetdmcaxk0gz79t4k2akytvf', + isStakingSupported: false, + isImportKeySupported: true, + walletConnectV2ChainId: { + namespace: 'cosmos', + reference: 'cosmoshub-4' + } }, osmosis: { initWalletName: lstrings.string_first_osmosis_wallet_name, chainCode: 'OSMO', dummyPublicAddress: 'osmo156hdwk3gx4wkq0r5m0s3ag2yj5pawfeudml34a', isCustomTokensSupported: true, - isImportKeySupported: true + isImportKeySupported: true, + walletConnectV2ChainId: { + namespace: 'cosmos', + reference: 'osmosis-1' + } }, thorchainrune: { initWalletName: lstrings.string_first_thorchainrune_wallet_name, diff --git a/src/constants/plugins/GuiPlugins.ts b/src/constants/plugins/GuiPlugins.ts index aba493872a0..983b46f3333 100644 --- a/src/constants/plugins/GuiPlugins.ts +++ b/src/constants/plugins/GuiPlugins.ts @@ -2,6 +2,15 @@ import { amountQuoteFiatPlugin } from '../../plugins/gui/amountQuotePlugin' import { GuiPlugin, GuiPluginRow } from '../../types/GuiPluginTypes' export const guiPlugins: { [pluginId: string]: GuiPlugin } = { + ach: { + pluginId: 'amountquote', + storeId: '', + baseUri: '', + lockUriPath: true, + nativePlugin: amountQuoteFiatPlugin, + forceFiatCurrencyCode: 'iso:USD', + displayName: 'ACH Bank Transfer' + }, libertyx: { pluginId: 'libertyx', storeId: 'com.libertyx', @@ -108,7 +117,8 @@ export const guiPlugins: { [pluginId: string]: GuiPlugin } = { baseUri: '', lockUriPath: true, nativePlugin: amountQuoteFiatPlugin, - displayName: 'ACH Bank Transfer' + forceFiatCurrencyCode: 'iso:USD', + displayName: 'Instant ACH Bank Transfer' }, ideal: { pluginId: 'amountquote', diff --git a/src/constants/plugins/buyPluginList.json b/src/constants/plugins/buyPluginList.json index 5a5a756e718..c60062bde65 100644 --- a/src/constants/plugins/buyPluginList.json +++ b/src/constants/plugins/buyPluginList.json @@ -243,6 +243,18 @@ "cryptoCodes": [], "paymentTypeLogoKey": "credit" }, + { + "id": "iach", + "pluginId": "iach", + "paymentType": "iach", + "paymentTypes": ["iach"], + "title": "Instant ACH Bank Transfer", + "description": "Fee: ~2%\nSettlement: ~5 minutes", + "forCountries": ["US"], + "cryptoCodes": [], + "paymentTypeLogoKey": "bank" + }, + { "id": "sepa", "pluginId": "sepa", diff --git a/src/constants/plugins/sellPluginList.json b/src/constants/plugins/sellPluginList.json index 8c0dfc97b5a..e69fb503cb1 100644 --- a/src/constants/plugins/sellPluginList.json +++ b/src/constants/plugins/sellPluginList.json @@ -1,4 +1,15 @@ [ + { + "id": "ach", + "pluginId": "ach", + "paymentType": "ach", + "paymentTypes": ["ach"], + "title": "ACH Bank Transfer", + "description": "Fee: 1.5%\nSettlement: 2 - 3 days", + "forCountries": ["US"], + "cryptoCodes": [], + "paymentTypeLogoKey": "bank" + }, { "id": "directtobank", "pluginId": "directtobank", @@ -16,8 +27,8 @@ "paymentType": "credit", "paymentTypes": ["credit"], "title": "Bank Transfer via Debit Card", - "description": "Fee: 2.5%\nSettlement: 5 min - 24 hours", - "forCountries": ["US"], + "description": "Fee: 4.0%\nSettlement: 5 min - 24 hours", + "forCountries": ["US", "GB", "AU"], "cryptoCodes": [], "paymentTypeLogoKey": "bank" }, diff --git a/src/controllers/edgeProvider/EdgeProviderServer.tsx b/src/controllers/edgeProvider/EdgeProviderServer.tsx index a492b59c72c..0590c49a37b 100644 --- a/src/controllers/edgeProvider/EdgeProviderServer.tsx +++ b/src/controllers/edgeProvider/EdgeProviderServer.tsx @@ -19,7 +19,6 @@ import SafariView from 'react-native-safari-view' import { sprintf } from 'sprintf-js' import { launchPaymentProto } from '../../actions/PaymentProtoActions' -import { trackConversion } from '../../actions/TrackingActions' import { ButtonsModal } from '../../components/modals/ButtonsModal' import { WalletListModal, WalletListResult } from '../../components/modals/WalletListModal' import { Airship, showError, showToast } from '../../components/services/AirshipInstance' @@ -32,6 +31,7 @@ import { getCurrencyIconUris } from '../../util/CdnUris' import { getTokenIdForced } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { makeCurrencyCodeTable } from '../../util/tokenIdTools' +import { logEvent } from '../../util/tracking' import { CurrencyConfigMap } from '../../util/utils' import { asExtendedCurrencyCode } from './types/edgeProviderCleaners' import { @@ -196,6 +196,11 @@ export class EdgeProviderServer implements EdgeProviderMethods { isHTML: true }, (error, event) => { + if (String(error) === 'not_available') { + showError(lstrings.error_no_email_account) + return + } + if (error) showError(error) } ) @@ -404,11 +409,11 @@ export class EdgeProviderServer implements EdgeProviderMethods { .nativeToDenomination(transaction.nativeAmount, transaction.currencyCode) .then(exchangeAmount => { this._dispatch( - trackConversion('EdgeProvider_Conversion_Success', { + logEvent('EdgeProvider_Conversion_Success', { pluginId: this._plugin.storeId, orderId, currencyCode: transaction.currencyCode, - exchangeAmount: Number(abs(exchangeAmount)) + exchangeAmount: abs(exchangeAmount) }) ) }) diff --git a/src/envConfig.ts b/src/envConfig.ts index 3cf53bfb538..20c97485baa 100644 --- a/src/envConfig.ts +++ b/src/envConfig.ts @@ -57,6 +57,11 @@ export const asEnvConfig = asObject({ }) ), Bitrefill: asOptional(asString), + kado: asOptional( + asObject({ + apiKey: asString + }) + ), moonpay: asOptional(asString), paybis: asOptional( asObject({ @@ -84,6 +89,7 @@ export const asEnvConfig = asObject({ () => ({ banxa: undefined, Bitrefill: undefined, + kado: undefined, moonpay: undefined, paybis: undefined, simplex: undefined, @@ -102,7 +108,9 @@ export const asEnvConfig = asObject({ STAKEKIT_API_KEY: asNullable(asString), // Core plugin options: + ARBITRUM_INIT: asCorePluginInit(asEvmApiKeys), AVALANCHE_INIT: asCorePluginInit(asEvmApiKeys), + BASE_INIT: asCorePluginInit(asEvmApiKeys), BINANCE_SMART_CHAIN_INIT: asCorePluginInit(asEvmApiKeys), CHANGE_NOW_INIT: asCorePluginInit( asObject({ diff --git a/src/experimentConfig.ts b/src/experimentConfig.ts index d4ec2371ead..e768b0d735c 100644 --- a/src/experimentConfig.ts +++ b/src/experimentConfig.ts @@ -6,19 +6,20 @@ import { isMaestro } from 'react-native-is-maestro' import { LOCAL_EXPERIMENT_CONFIG } from './constants/constantSettings' import { ENV } from './env' +export type LandingType = 'A_legacy' | 'B_Usps' | 'C_UspsMinusWGYC' | 'D_UspsAltWGYC' + // Persistent experiment config for A/B testing. Values initialized in this // config persist throughout the liftetime of the app install. export interface ExperimentConfig { - swipeLastUsp: 'true' | 'false' createAccountType: CreateAccountType - legacyLanding: 'legacyLanding' | 'uspLanding' + landingType: LandingType signupCaptcha: 'withCaptcha' | 'withoutCaptcha' } -const DEFAULT_EXPERIMENT_CONFIG: ExperimentConfig = { - swipeLastUsp: 'false', +// Defined default "unchanged" values before experimentation. +export const DEFAULT_EXPERIMENT_CONFIG: ExperimentConfig = { createAccountType: 'full', - legacyLanding: 'uspLanding', + landingType: 'B_Usps', signupCaptcha: 'withoutCaptcha' } @@ -26,9 +27,8 @@ const experimentConfigDisklet = makeReactNativeDisklet() // The probability of an experiment config feature being set for a given key const experimentDistribution = { - swipeLastUsp: [50, 50], createAccountType: [50, 50], - legacyLanding: [50, 50], + landingType: [25, 25, 25, 25], signupCaptcha: [50, 50] } @@ -68,16 +68,13 @@ const generateExperimentConfigVal = (key: keyof typeof experimentDistribution return configVals[configVals.length - 1] } -// It's important to define string literals instead of booleans as values so -// that they are properly captured in the analytics dashboard reports. The first -// values are the variant values that differ from the default feature -// behavior/appearance, while the last value represents unchanged -// behavior/appearance. const asExperimentConfig: Cleaner = asObject({ - swipeLastUsp: asMaybe(asValue('true', 'false'), generateExperimentConfigVal('swipeLastUsp', ['true', 'false'])), createAccountType: asMaybe(asValue('full', 'light'), generateExperimentConfigVal('createAccountType', ['full', 'light'])), - legacyLanding: asMaybe(asValue('uspLanding', 'legacyLanding'), generateExperimentConfigVal('legacyLanding', ['legacyLanding', 'uspLanding'])), - signupCaptcha: asMaybe(asValue('withoutCaptcha'), 'withoutCaptcha') + landingType: asMaybe( + asValue('A_legacy', 'B_Usps', 'C_UspsMinusWGYC', 'D_UspsAltWGYC'), + generateExperimentConfigVal('landingType', ['A_legacy', 'B_Usps', 'C_UspsMinusWGYC', 'D_UspsAltWGYC']) + ), + signupCaptcha: asMaybe(asValue('withoutCaptcha', 'withCaptcha'), generateExperimentConfigVal('signupCaptcha', ['withoutCaptcha', 'withCaptcha'])) }) /** diff --git a/src/hooks/useWalletConnect.tsx b/src/hooks/useWalletConnect.tsx index cc6d29eaf05..482d28afdd3 100644 --- a/src/hooks/useWalletConnect.tsx +++ b/src/hooks/useWalletConnect.tsx @@ -204,6 +204,7 @@ const getSupportedNamespaces = (chainId: WalletConnectChainId, addr: string) => const { namespace, reference } = chainId let methods: string[] + let events = ['chainChanged', 'accountsChanged'] switch (namespace) { case 'eip155': methods = [ @@ -219,13 +220,17 @@ const getSupportedNamespaces = (chainId: WalletConnectChainId, addr: string) => case 'algorand': methods = ['algo_signTxn'] + break + case 'cosmos': + methods = ['cosmos_getAccounts', 'cosmos_signDirect', 'cosmos_signAmino'] + events = [] } return { [namespace]: { chains: [`${namespace}:${reference}`], methods, - events: ['chainChanged', 'accountsChanged'], + events, accounts: [`${namespace}:${reference}:${addr}`] } } diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 5d2b5a48930..cf9f5d8bacf 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -89,6 +89,7 @@ const strings = { // Error messages: + error_no_email_account: 'No email account setup on phone. Please setup an email account on your phone to use this feature.', error_paymentprotocol_empty_output_invoice: 'Received no output in payment request', error_paymentprotocol_empty_verification_hex_req: 'Generated empty transaction hex(es)', error_paymentprotocol_currency_not_supported: 'Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.', @@ -248,7 +249,6 @@ const strings = { wallet_list_referral_link_currency_invalid: 'Currency to create is invalid', wallet_list_referral_link_currency_loading: 'Hang tight. Creating wallet necessary for this promotion', wallet_list_referral_link_ask_wallet_creation: 'You need %s wallet for this promotion, do you want to create one?', - wallet_list_referral_link_cancelled_wallet_creation: 'User did not approve creating wallet for the promotion', wallet_list_wallet_search: 'Search Wallets', compromised_key_label: 'Compromised Key', create_wallet_choice_new_button: 'Create New Wallet', @@ -513,6 +513,8 @@ const strings = { string_first_ethereum_wallet_name: 'My Ether', string_first_ethereum_classic_wallet_name: 'My Ethereum Classic', string_first_ethereum_pow_wallet_name: 'My Ethereum POW', + string_first_arbitrum_wallet_name: 'My Arbitrum', + string_first_base_wallet_name: 'My Base', string_first_filecoin_wallet_name: 'My Filecoin', string_first_filecoin_fevm_wallet_name: 'My Filecoin FEVM', string_first_filecoin_fevm_calibratio_wallet_name: 'My Filecoin FEVM (Calibration)', @@ -540,7 +542,9 @@ const strings = { string_first_zcoin_wallet_name: 'My Firo', string_first_stellar_wallet_name: 'My Stellar', string_first_tezos_wallet_name: 'My Tezos', + string_first_axelar_wallet_name: 'My Axelar', string_first_coreum_wallet_name: 'My Coreum', + string_first_cosmoshub_wallet_name: 'My Cosmos Hub', string_first_osmosis_wallet_name: 'My Osmosis', string_first_thorchainrune_wallet_name: 'My Thorchain', string_first_rsk_wallet_name: 'My Rootstock', @@ -821,6 +825,7 @@ const strings = { cannot_delete_last_wallet_modal_message_part_1: 'At least one wallet required in this account.', cannot_delete_last_wallet_modal_message_part_2: 'If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.', + enter_amount_label: 'Enter Amount', enter_your_password: 'Enter your password', spending_limits: 'Spending Limits', spending_limits_tx_title: 'Transaction Spending Limit', @@ -1076,7 +1081,9 @@ const strings = { error_boundary_title: 'Oops!', error_boundary_message_s: - "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + error_boundary_message2: "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + error_boundary_help_button: 'Help Closing App', error_unexpected_title: 'Unexpected Error', export_transaction_date_range: 'Date Range', @@ -1674,6 +1681,8 @@ const strings = { getting_started_button_sign_in: `Already have an account? Sign in`, getting_started_slide_1_footnote: `Users can access seed phrases within the app at their discretion.`, getting_started_slide_1_message: `Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.`, + getting_started_slide_1_message_alt: `Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.`, + getting_started_slide_1_title: `We've Got You\n*Covered*`, getting_started_slide_2_message: `Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.`, getting_started_slide_2_title: `They're Your Assets\nYou're in *Control*`, diff --git a/src/locales/strings/de.json b/src/locales/strings/de.json index 1769aec2cfb..a1833db96be 100644 --- a/src/locales/strings/de.json +++ b/src/locales/strings/de.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Wählen Sie eine FIO-Domain", "fio_address_choose_domain_label": "Wähle FIO-Domain für deinen Krypto-Handle aus", "scan_qr_label": "QR-Code scannen", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Keine Ausgabe in Zahlungsanforderung erhalten", "error_paymentprotocol_empty_verification_hex_req": "Erstellte leere Transaktionshex(en)", "error_paymentprotocol_currency_not_supported": "Zahlungsprotokoll Rechnungen in %s werden derzeit nicht unterstützt. Bitte wählen Sie eine andere Zahlungswährung.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Meine Ether", "string_first_ethereum_classic_wallet_name": "Mein Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "Mein Firo", "string_first_stellar_wallet_name": "Mein Stellar", "string_first_tezos_wallet_name": "Mein Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Wallet kann nicht archiviert werden", "cannot_delete_last_wallet_modal_message_part_1": "Mindestens eine Wallet ist in diesem Konto erforderlich.", "cannot_delete_last_wallet_modal_message_part_2": "Wenn Sie diese Brieftasche archivieren möchten, müssen Sie diesem Konto eine zusätzliche Wallet hinzufügen.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Geben Sie Ihr Passwort ein", "spending_limits": "Ausgabelimits", "spending_limits_tx_title": "Transaktions-Ausgabelimit", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbuch-Upgrade", "update_notice_deprecate_electrum_servers_message": "%s verwendet keine Electrum Server. Wenn Sie weiterhin CUSTOM NODES verwenden möchten, geben Sie bitte Blockbook kompatible Adressen ein.\n\nHINWEIS: Wenn Sie benutzerdefinierte Knoten aktiviert haben, werden diese Wallets erst synchronisiert, wenn sie korrigiert wurden.", "error_boundary_title": "Hoppla!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Zeitspanne", "export_transaction_export_type": "Export-Typ", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 6e2290364cc..360012e21d1 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Choose a FIO Domain", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -189,7 +190,6 @@ "wallet_list_referral_link_currency_invalid": "Currency to create is invalid", "wallet_list_referral_link_currency_loading": "Hang tight. Creating wallet necessary for this promotion", "wallet_list_referral_link_ask_wallet_creation": "You need %s wallet for this promotion, do you want to create one?", - "wallet_list_referral_link_cancelled_wallet_creation": "User did not approve creating wallet for the promotion", "wallet_list_wallet_search": "Search Wallets", "compromised_key_label": "Compromised Key", "create_wallet_choice_new_button": "Create New Wallet", @@ -445,6 +445,8 @@ "string_first_ethereum_wallet_name": "My Ether", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +474,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "My Stellar", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +735,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Enter your password", "spending_limits": "Spending Limits", "spending_limits_tx_title": "Transaction Spending Limit", @@ -960,7 +965,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Date Range", "export_transaction_export_type": "Export Type", @@ -1468,6 +1475,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/es.json b/src/locales/strings/es.json index db03b6ec348..f624e778d5f 100644 --- a/src/locales/strings/es.json +++ b/src/locales/strings/es.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Elegir un dominio FIO", "fio_address_choose_domain_label": "Elige un dominio FIO para tu ususario", "scan_qr_label": "Escanear código QR", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "No se ha recibido respuesta en la solicitud de pago", "error_paymentprotocol_empty_verification_hex_req": "Generado hex(s) de transacción vacía", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Mi Ether", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "Mis Firo", "string_first_stellar_wallet_name": "Mi Stellar", "string_first_tezos_wallet_name": "Mi Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "No se puede archivar billetera", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "Si desea archivar esta cartera, necesitará añadir una cartera adicional a esta cuenta.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Introduce tu contraseña", "spending_limits": "Límites de gasto", "spending_limits_tx_title": "Límite de gasto por transacción", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "¡Ups!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Rango de fechas", "export_transaction_export_type": "Tipo de exportación", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/esMX.json b/src/locales/strings/esMX.json index fe917932eaa..23bc22a692c 100644 --- a/src/locales/strings/esMX.json +++ b/src/locales/strings/esMX.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Elegir un dominio FIO", "fio_address_choose_domain_label": "Elige un dominio FIO para tu ususario", "scan_qr_label": "Escanear código QR", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "No se ha recibido respuesta en la solicitud de pago", "error_paymentprotocol_empty_verification_hex_req": "Generado hex(s) de transacción vacía", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Mi Ether", "string_first_ethereum_classic_wallet_name": "Mi Ethereum Classic", "string_first_ethereum_pow_wallet_name": "Mi Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "Mis Firo", "string_first_stellar_wallet_name": "Mi Stellar", "string_first_tezos_wallet_name": "Mi Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "Mi Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "No se puede archivar billetera", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "Si desea archivar esta cartera, necesitará añadir una cartera adicional a esta cuenta.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Introduce tu contraseña", "spending_limits": "Límites de gasto", "spending_limits_tx_title": "Límite de gasto por transacción", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "¡Ups!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Rango de fechas", "export_transaction_export_type": "Tipo de exportación", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/fr.json b/src/locales/strings/fr.json index 95b6e62dec7..c13bc5c1822 100644 --- a/src/locales/strings/fr.json +++ b/src/locales/strings/fr.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Choose a FIO Domain", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Mes Ethers", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "Mes Stellars", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Saisissez votre mot de passe", "spending_limits": "Limites de dépenses", "spending_limits_tx_title": "Limite de dépenses des transactions", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Date Range", "export_transaction_export_type": "Export Type", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/it.json b/src/locales/strings/it.json index 4154bf971fb..b32b0f1789e 100644 --- a/src/locales/strings/it.json +++ b/src/locales/strings/it.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Scegli un dominio FIO", "fio_address_choose_domain_label": "Scegli un dominio FIO per il tuo Crypto Handle", "scan_qr_label": "Scansiona il codice QR", + "error_no_email_account": "Nessun account email impostato sul telefono. Si prega di impostare un account email sul tuo telefono per utilizzare questa funzionalità.", "error_paymentprotocol_empty_output_invoice": "Nessun output nella richiesta di pagamento è stato ricevuto", "error_paymentprotocol_empty_verification_hex_req": "Hex di transazione vuota generato", "error_paymentprotocol_currency_not_supported": "I pagamenti per le invoice in %s che seguono lo standard Payment Protocol, attualmente non supportati. Si prega di scegliere un'altra valuta di pagamento.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "I miei Ethereum", "string_first_ethereum_classic_wallet_name": "I miei Ethereum Classic", "string_first_ethereum_pow_wallet_name": "I miei Ethereum POW", + "string_first_arbitrum_wallet_name": "I miei Arbitrum", + "string_first_base_wallet_name": "I miei Base", "string_first_filecoin_wallet_name": "I miei Filecoin", "string_first_filecoin_fevm_wallet_name": "I miei Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "I miei FEVM Filecoin (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "I miei Firo", "string_first_stellar_wallet_name": "I miei Stellar", "string_first_tezos_wallet_name": "I miei Tezos", + "string_first_axelar_wallet_name": "I miei Axelar", "string_first_coreum_wallet_name": "I miei Coreum", + "string_first_cosmoshub_wallet_name": "I miei Cosmos Hub", "string_first_osmosis_wallet_name": "I miei Osmosis", "string_first_thorchainrune_wallet_name": "I miei Thorchain", "string_first_rsk_wallet_name": "I miei Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Impossibile archiviare il portafoglio", "cannot_delete_last_wallet_modal_message_part_1": "Almeno un portafoglio è richiesto in questo account.", "cannot_delete_last_wallet_modal_message_part_2": "Se desideri archiviare questo portafoglio, dovrai aggiungere un portafoglio aggiuntivo a questo account.", + "enter_amount_label": "Inserisci l'importo", "enter_your_password": "Inserisci la tua password", "spending_limits": "Limiti di spesa", "spending_limits_tx_title": "Limite di spesa per transazione", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Aggiornamento Blockbook", "update_notice_deprecate_electrum_servers_message": "%s non utilizza più i server Electrum. Se desideri continuare a usare server personalizzati, inserisci indirizzi compatibili con Blockbook.\n\nNOTA: se hai abilitato dei server personalizzati, questi portafogli non verranno sincronizzati fino a quando non userai Blockbook o disattiverai i server personalizzati.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "Siamo spiacenti ma qualcosa è andato storto. Si prega di forzare la chiusura dell'app e riaprirla per continuare a utilizzare Edge.\n\nSe il problema persiste, contattaci a %1$s e faremo del nostro meglio per risolvere il problema. \n\nNOTA: NON disinstallare l'app prima di contattare Edge.", + "error_boundary_message_s": "Siamo spiacenti ma qualcosa è andato storto\n\n*** NON INSTALLARE %1$s ***\n\nSi prega di forzare la chiusura dell'app e riaprirla per continuare ad utilizzare %1$s.\n\nPer aiuto su come forzare la chiusura dell'app, clicca qui sotto", + "error_boundary_message2": "Se il problema persiste, clicca qui sotto per contattarci e faremo del nostro meglio per risolvere il problema.", + "error_boundary_help_button": "Aiuto Chiusura App", "error_unexpected_title": "Errore Imprevisto", "export_transaction_date_range": "Intervallo Date", "export_transaction_export_type": "Tipo di esportazione", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Hai già un account? Accedi", "getting_started_slide_1_footnote": "Gli utenti possono accedere alle seed phrase all'interno dell'app a loro discrezione.", "getting_started_slide_1_message": "Dì addio alla scrittura di backup non sicuri su carta. Con la crittografia lato client, backup automatico dell'account e 2FA, Edge semplifica la self-custody per tutti.", + "getting_started_slide_1_message_alt": "Recupera facilmente il tuo Edge Wallet e beneficia della protezione aggiuntiva data dall'autenticazione a due fattori. Edge semplifica la self-custody per tutti.", "getting_started_slide_1_title": "Non preoccuparti, \n*ci siamo noi*", "getting_started_slide_2_message": "Edge ha accesso zero ai fondi degli utenti, il che significa nessun blocco degli account o perdita dei fondi. Acquista, vendi e scambia con tranquillità.", "getting_started_slide_2_title": "Sono i tuoi asset\nsei tu in *controllo*", diff --git a/src/locales/strings/ja.json b/src/locales/strings/ja.json index 163f315702a..70309733b57 100644 --- a/src/locales/strings/ja.json +++ b/src/locales/strings/ja.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "FIOドメインを選択してください", "fio_address_choose_domain_label": "クリプトハンドル用のFIOドメインを選択してください", "scan_qr_label": "QR コードをスキャン", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "支払いリクエストで出力を受信しませんでした", "error_paymentprotocol_empty_verification_hex_req": "空のトランザクションhexが生成されました", "error_paymentprotocol_currency_not_supported": "%s でのペイメント・プロトコルの請求書支払いは現在サポートされていません。別の支払い通貨を選択してください。", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "My Ether", "string_first_ethereum_classic_wallet_name": "イーサリアムクラシック", "string_first_ethereum_pow_wallet_name": "イーサリアムPOW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "My Stellar", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "ウォレットをアーカイブできません", "cannot_delete_last_wallet_modal_message_part_1": "このアカウントには少なくとも1つのウォレットが必要です。", "cannot_delete_last_wallet_modal_message_part_2": "このウォレットをアーカイブしたい場合、このアカウントに追加のウォレットを追加する必要があります。", + "enter_amount_label": "Enter Amount", "enter_your_password": "パスワードを入力", "spending_limits": "限度額", "spending_limits_tx_title": "取引限度額", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "ブロックブックのアップグレード", "update_notice_deprecate_electrum_servers_message": "%sはもうElectrumサーバーを使用しません。引き続きCUSTOM NODESを使用したい場合は、Blockbook互換のアドレスを入力してください。\n\n注意: カスタムノードが有効な場合、これらのウォレットは修正されるまで同期されません。", "error_boundary_title": "エラーが発生しました。", - "error_boundary_message_s": "申し訳ありません。問題が発生しました。アプリを強制終了し、再度開いてEdgeをご利用ください。\n\n問題が続く場合は、%1$s までお問い合わせください。問題の修正に最善を尽くします。\n\n注意: Edgeに連絡する前にアプリをアンインストールしないでください。", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "予期しないエラー", "export_transaction_date_range": "日付範囲", "export_transaction_export_type": "エクスポートの種類", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "すでにアカウントをお持ちですか?サインイン", "getting_started_slide_1_footnote": "ユーザーはアプリ内で自由にシードフレーズにアクセスすることができます。", "getting_started_slide_1_message": "紙に安全でないバックアップを書き留めるのはもうさようならです。クライアント側の暗号化、自動アカウントのバックアップ、二段階認証により、Edgeは自己保管を誰もが簡単に行えるようにします。", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "私たちはあなたを\n*カバー*しています", "getting_started_slide_2_message": "Edgeはユーザーの資金にアクセス権を持っていません、つまり口座の凍結や資産の没収の心配はありません。安心して購入、売却、取引が行えます。", "getting_started_slide_2_title": "これらはあなたの資産です。\nあなたが*コントロール*しています。", diff --git a/src/locales/strings/kaa.json b/src/locales/strings/kaa.json index f4a2eee916b..a8cbae5830d 100644 --- a/src/locales/strings/kaa.json +++ b/src/locales/strings/kaa.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Choose a FIO Domain", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "My Ether", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "My Stellar", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Enter your password", "spending_limits": "Spending Limits", "spending_limits_tx_title": "Transaction Spending Limit", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Date Range", "export_transaction_export_type": "Export Type", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/ko.json b/src/locales/strings/ko.json index eaaae0faab0..88f511d5074 100644 --- a/src/locales/strings/ko.json +++ b/src/locales/strings/ko.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Choose a FIO Domain", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "나의 이더리움", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "My Stellar (스텔라)", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "암호 입력", "spending_limits": "지출 한도", "spending_limits_tx_title": "거래 지출 한도", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Date Range", "export_transaction_export_type": "Export Type", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/pt.json b/src/locales/strings/pt.json index 74e55936eff..633209c224a 100644 --- a/src/locales/strings/pt.json +++ b/src/locales/strings/pt.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Escolha um Domínio FIO", "fio_address_choose_domain_label": "Escolha o domínio FIO para o seu Crypto Handle", "scan_qr_label": "Escaneie o QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Não foi recebida nenhuma saída no pedido de pagamento", "error_paymentprotocol_empty_verification_hex_req": "Hex(es) de transação vazia gerados", "error_paymentprotocol_currency_not_supported": "Pagamentos de fatura do Protocolo de Pagamento em %s não são suportados no momento. Por favor, escolha outra moeda de pagamento.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Meu Ether", "string_first_ethereum_classic_wallet_name": "Meu Ethereum Classic", "string_first_ethereum_pow_wallet_name": "Meu Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "Minha Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "Meu Firo", "string_first_stellar_wallet_name": "Minha Stellar", "string_first_tezos_wallet_name": "Meu Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "Meu Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Não é Possível Arquivar a Carteira", "cannot_delete_last_wallet_modal_message_part_1": "Pelo menos uma carteira é necessária nesta conta.", "cannot_delete_last_wallet_modal_message_part_2": "Se você deseja arquivar esta carteira, precisará adicionar uma carteira adicional a esta conta.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Entre sua senha", "spending_limits": "Limites de Gasto", "spending_limits_tx_title": "Limite de despesa de transação", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Atualização do Blockbook", "update_notice_deprecate_electrum_servers_message": "%s não utiliza mais Servidores Electrum. Se você deseja continuar usando NODES PERSONALIZADOS, por favor insira endereços compatíveis com o Blockbook.\n\nNOTA: Se você tinha NODES PERSONALIZADOS habilitados, essas carteiras não serão sincronizadas até que sejam corrigidas.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "Lamentamos, mas algo deu errado. Force o fechamento do aplicativo e reabra-o para continuar usando o Edge.\n\nSe o problema persistir, entre em contato conosco em %1$s e faremos o possível para resolver o problema.\n\nNOTA: Não desinstale o aplicativo antes de entrar em contato com o Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Erro Inesperado", "export_transaction_date_range": "Intervalo de Datas", "export_transaction_export_type": "Tipo de exportação", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Já tem uma conta? Entrar", "getting_started_slide_1_footnote": "Os usuários podem acessar frases seed dentro do aplicativo a seu critério.", "getting_started_slide_1_message": "Diga adeus à escrita de ‘backups’ inseguros em papel. Com a criptografia do lado do cliente, backup automático de contas e 2FA, Edge simplifica a auto-custódia para todos.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "Não se preocupe,\n*Estamos aqui*", "getting_started_slide_2_message": "O Edge não tem acesso aos fundos do usuário, o que significa que não há congelamento de conta ou confisco de ativos. Compre, venda e negocie com tranquilidade.", "getting_started_slide_2_title": "São os seus Ativos\nVocê está no *Controle*", diff --git a/src/locales/strings/ru.json b/src/locales/strings/ru.json index c88c5936f56..d88989448c3 100644 --- a/src/locales/strings/ru.json +++ b/src/locales/strings/ru.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Выберите домен FIO", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Мой Ether", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "Мой Stellar", "string_first_tezos_wallet_name": "Мой Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Введите ваш пароль", "spending_limits": "Лимит средств", "spending_limits_tx_title": "Лимит суммы транзакции", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Интервал дат", "export_transaction_export_type": "Тип экспорта", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/vi.json b/src/locales/strings/vi.json index dbe74b7d562..35021769b10 100644 --- a/src/locales/strings/vi.json +++ b/src/locales/strings/vi.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Choose a FIO Domain", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "Ether của tôi", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "Stellar của tôi", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "Nhập mật khẩu của bạn", "spending_limits": "Giới hạn chi tiêu", "spending_limits_tx_title": "Giao dịch chi tiêu giới hạn", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Date Range", "export_transaction_export_type": "Export Type", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/locales/strings/zh.json b/src/locales/strings/zh.json index 68ca5f486ec..ad21ad930cd 100644 --- a/src/locales/strings/zh.json +++ b/src/locales/strings/zh.json @@ -52,6 +52,7 @@ "fio_domain_choose_label": "Choose a FIO Domain", "fio_address_choose_domain_label": "Choose FIO Domain for Your Crypto Handle", "scan_qr_label": "Scan QR Code", + "error_no_email_account": "No email account setup on phone. Please setup an email account on your phone to use this feature.", "error_paymentprotocol_empty_output_invoice": "Received no output in payment request", "error_paymentprotocol_empty_verification_hex_req": "Generated empty transaction hex(es)", "error_paymentprotocol_currency_not_supported": "Payment Protocol invoice payments in %s not currently supported. Please choose another payment currency.", @@ -445,6 +446,8 @@ "string_first_ethereum_wallet_name": "我的以太坊", "string_first_ethereum_classic_wallet_name": "My Ethereum Classic", "string_first_ethereum_pow_wallet_name": "My Ethereum POW", + "string_first_arbitrum_wallet_name": "My Arbitrum", + "string_first_base_wallet_name": "My Base", "string_first_filecoin_wallet_name": "My Filecoin", "string_first_filecoin_fevm_wallet_name": "My Filecoin FEVM", "string_first_filecoin_fevm_calibratio_wallet_name": "My Filecoin FEVM (Calibration)", @@ -472,7 +475,9 @@ "string_first_zcoin_wallet_name": "My Firo", "string_first_stellar_wallet_name": "我的恒星币", "string_first_tezos_wallet_name": "My Tezos", + "string_first_axelar_wallet_name": "My Axelar", "string_first_coreum_wallet_name": "My Coreum", + "string_first_cosmoshub_wallet_name": "My Cosmos Hub", "string_first_osmosis_wallet_name": "My Osmosis", "string_first_thorchainrune_wallet_name": "My Thorchain", "string_first_rsk_wallet_name": "My Rootstock", @@ -731,6 +736,7 @@ "cannot_delete_last_wallet_modal_title": "Cannot Archive Wallet", "cannot_delete_last_wallet_modal_message_part_1": "At least one wallet required in this account.", "cannot_delete_last_wallet_modal_message_part_2": "If you’d like to archive this wallet, you’ll need to add an additional wallet to this account.", + "enter_amount_label": "Enter Amount", "enter_your_password": "输入您的密码", "spending_limits": "支出限额", "spending_limits_tx_title": "交易支出限额", @@ -960,7 +966,9 @@ "update_notice_deprecate_electrum_servers_title": "Blockbook Upgrade", "update_notice_deprecate_electrum_servers_message": "%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.", "error_boundary_title": "Oops!", - "error_boundary_message_s": "We're sorry but something went wrong. Please force close the app and reopen to continue using Edge.\n\nIf the problem persists, contact us at %1$s, and we'll do our best to fix the problem. \n\nNOTE: Do not uninstall the app before contacting Edge.", + "error_boundary_message_s": "We're sorry but something went wrong\n\n*** DO NOT UNINSTALL %1$s ***\n\nPlease force close the app and reopen to continue using %1$s.\n\nFor help on how to force close the app, tap below", + "error_boundary_message2": "If the problem persists, tap below to contact us, and we'll do our best to fix the problem.", + "error_boundary_help_button": "Help Closing App", "error_unexpected_title": "Unexpected Error", "export_transaction_date_range": "Date Range", "export_transaction_export_type": "Export Type", @@ -1468,6 +1476,7 @@ "getting_started_button_sign_in": "Already have an account? Sign in", "getting_started_slide_1_footnote": "Users can access seed phrases within the app at their discretion.", "getting_started_slide_1_message": "Say goodbye to writing down insecure backups on paper. With client-side encryption, automatic account backup and 2FA, Edge simplifies self-custody for everyone.", + "getting_started_slide_1_message_alt": "Easily recover your Edge wallet and benefit from the added protection of two-factor authentication. Edge simplifies self-custody for everyone.", "getting_started_slide_1_title": "We've Got You\n*Covered*", "getting_started_slide_2_message": "Edge has zero access to user funds, which means no account freezes or asset forfeiture. Buy, sell, and trade with peace of mind.", "getting_started_slide_2_title": "They're Your Assets\nYou're in *Control*", diff --git a/src/plugins/gui/amountQuotePlugin.ts b/src/plugins/gui/amountQuotePlugin.ts index e341fd846bd..dd6c9a9e33a 100644 --- a/src/plugins/gui/amountQuotePlugin.ts +++ b/src/plugins/gui/amountQuotePlugin.ts @@ -4,18 +4,17 @@ import { sprintf } from 'sprintf-js' import { formatNumber, isValidInput } from '../../locales/intl' import { lstrings } from '../../locales/strings' -import { config } from '../../theme/appConfig' import { EdgeAsset } from '../../types/types' import { getPartnerIconUri } from '../../util/CdnUris' -import { getTokenIdForced } from '../../util/CurrencyInfoHelpers' -import { fetchInfo } from '../../util/network' +import { infoServerData } from '../../util/network' import { logEvent } from '../../util/tracking' import { fuzzyTimeout } from '../../util/utils' import { FiatPlugin, FiatPluginFactory, FiatPluginFactoryArgs, FiatPluginStartParams } from './fiatPluginTypes' -import { FiatProviderAssetMap, FiatProviderGetQuoteParams, FiatProviderQuote } from './fiatProviderTypes' +import { FiatProvider, FiatProviderAssetMap, FiatProviderGetQuoteParams, FiatProviderQuote } from './fiatProviderTypes' import { getBestError, getRateFromQuote } from './pluginUtils' import { banxaProvider } from './providers/banxaProvider' import { bityProvider } from './providers/bityProvider' +import { kadoProvider } from './providers/kadoProvider' import { moonpayProvider } from './providers/moonpayProvider' import { paybisProvider } from './providers/paybisProvider' import { simplexProvider } from './providers/simplexProvider' @@ -34,7 +33,7 @@ type PaymentTypeProviderPriorityMap = ReturnType -const providerFactories = [banxaProvider, bityProvider, moonpayProvider, paybisProvider, simplexProvider] +const providerFactories = [banxaProvider, bityProvider, kadoProvider, moonpayProvider, paybisProvider, simplexProvider] const DEFAULT_FIAT_AMOUNT = '500' @@ -92,9 +91,10 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi // Fetch provider priorities from the info server based on the payment // type try { - const response = await fetchInfo(`v1/fiatPluginPriority/${config.appId ?? 'edge'}`) - providerPriority = asPaymentTypeProviderPriorityMap(await response.json()) - priorityArray = createPriorityArray(providerPriority[paymentTypes[0]]) + if (infoServerData.rollup?.fiatPluginPriority != null) { + providerPriority = infoServerData.rollup.fiatPluginPriority + priorityArray = createPriorityArray(providerPriority[paymentTypes[0]]) + } } catch (e: any) { console.warn('Failed to fetch provider priorities:', e) // This is ok. We will use all configured providers at equal priority. @@ -103,34 +103,43 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi throw new Error('Multiple paymentTypes not implemented') } + const paymentProviderPriority = providerPriority[paymentTypes[0]] + const priorityProviders = providers.filter(p => paymentProviderPriority[p.providerId] != null && paymentProviderPriority[p.providerId] > 0) + // Fetch supported assets from all providers, based on the given // paymentTypes this plugin was initialized with. - for (const provider of providers) { + for (const provider of priorityProviders) { assetPromises.push(provider.getSupportedAssets({ direction, regionCode, paymentTypes })) } const ps = fuzzyTimeout(assetPromises, 5000).catch(e => { console.error('amountQuotePlugin error fetching assets: ', String(e)) - return [] }) const assetArray = await showUi.showToastSpinner(lstrings.fiat_plugin_fetching_assets, ps) + const requireAmountFiat: { [providerId: string]: boolean } = {} + const requireAmountCrypto: { [providerId: string]: boolean } = {} + const allowedAssets: EdgeAsset[] = [] const allowedFiats: { [fiatCurrencyCode: string]: boolean } = {} - for (const assetMap of assetArray) { + const allowedProviders: { [providerId: string]: boolean } = {} + for (const assetMap of assetArray ?? []) { if (assetMap == null) continue + allowedProviders[assetMap.providerId] = true + requireAmountCrypto[assetMap.providerId] = false + requireAmountFiat[assetMap.providerId] = false + if (assetMap.requiredAmountType === 'fiat') { + requireAmountFiat[assetMap.providerId] = true + } else if (assetMap.requiredAmountType === 'crypto') { + requireAmountCrypto[assetMap.providerId] = true + } + for (const currencyPluginId in assetMap.crypto) { - const currencyCodeMap = assetMap.crypto[currencyPluginId] - for (const currencyCode in currencyCodeMap) { - if (currencyCodeMap[currencyCode]) { - try { - const currencyTokenId = getTokenIdForced(account, currencyPluginId, currencyCode) - allowedAssets.push({ pluginId: currencyPluginId, tokenId: currencyTokenId }) - } catch (e: any) { - // This is ok. We might not support a specific pluginId - } - } + const providerTokens = assetMap.crypto[currencyPluginId] + for (const { tokenId } of providerTokens) { + console.log('Adding asset:', assetMap.providerId, currencyPluginId, tokenId) + allowedAssets.push({ pluginId: currencyPluginId, tokenId }) } for (const fiatCode in assetMap.fiat) { allowedFiats[fiatCode] = true @@ -138,6 +147,33 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi } } + const allowedProvidersArray = priorityProviders.filter(p => allowedProviders[p.providerId]) + const fiatAmountProviders = allowedProvidersArray.filter(p => !requireAmountCrypto[p.providerId]) + const cryptoAmountProviders = allowedProvidersArray.filter(p => !requireAmountFiat[p.providerId]) + const hasProviderWithoutRequire = allowedProvidersArray.some(p => !requireAmountFiat[p.providerId] && !requireAmountCrypto[p.providerId]) + + // Filter out providers that have a required amount type if there are ANY providers + // that do not have a required amount type. + let requireCrypto = false + let requireFiat = false + if (!hasProviderWithoutRequire) { + // We don't have a fully flexible provider that can take fiat or crypto inputs + // Validate if we need to force a specific input type + for (const p of allowedProvidersArray) { + if (requireAmountFiat[p.providerId] && requireAmountCrypto[p.providerId]) { + throw new Error('Provider must not require both fiat and crypto amounts') + } + if (requireAmountFiat[p.providerId]) { + requireFiat = true + } else if (requireAmountCrypto[p.providerId]) { + requireCrypto = true + } + } + // We prefer fiat input so in a pinch where some providers require crypto and + // another requires fiat, we'll force fiat + if (requireFiat) requireCrypto = false + } + // Pop up modal to pick wallet/asset // TODO: Filter wallets according to fiats supported by allowed providers const walletListResult = await showUi.walletPicker({ @@ -164,17 +200,22 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi const fiatCurrencyCode = forceFiatCurrencyCode ?? coreWallet.fiatCurrencyCode const displayFiatCurrencyCode = fiatCurrencyCode.replace('iso:', '') const isBuy = direction === 'buy' + const disableInput = requireCrypto ? 1 : requireFiat ? 2 : undefined logEvent(isBuy ? 'Buy_Quote' : 'Sell_Quote') // Navigate to scene to have user enter amount + const initialValue1 = requireCrypto ? undefined : defaultFiatAmount ?? DEFAULT_FIAT_AMOUNT showUi.enterAmount({ + disableInput, headerTitle: isBuy ? sprintf(lstrings.fiat_plugin_buy_currencycode, currencyCode) : sprintf(lstrings.fiat_plugin_sell_currencycode_s, currencyCode), initState: { - value1: defaultFiatAmount ?? DEFAULT_FIAT_AMOUNT + value1: initialValue1, + statusText: initialValue1 == null ? { content: lstrings.enter_amount_label } : { content: '' } }, label1: sprintf(lstrings.fiat_plugin_amount_currencycode, displayFiatCurrencyCode), label2: sprintf(lstrings.fiat_plugin_amount_currencycode, currencyCode), + swapInputLocations: requireCrypto, async onChangeText() {}, async convertValue(sourceFieldNum, value, stateManager) { if (!isValidInput(value)) { @@ -186,6 +227,10 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi lastSourceFieldNum = sourceFieldNum if (eq(value, '0')) { + stateManager.update({ + statusText: { content: lstrings.enter_amount_label }, + poweredBy: undefined + }) return '' } @@ -195,8 +240,10 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi // TODO: Design UX that supports quoting fiatCurrencyCodes that differ // from the the selected wallet's fiatCurrencyCode + let finalProvidersArray: FiatProvider[] if (sourceFieldNum === 1) { // User entered a fiat value. Convert to crypto + finalProvidersArray = fiatAmountProviders sourceFieldCurrencyCode = displayFiatCurrencyCode quoteParams = { pluginId: currencyPluginId, @@ -211,6 +258,7 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi } } else { // User entered a crypto value. Convert to fiat + finalProvidersArray = cryptoAmountProviders sourceFieldCurrencyCode = currencyCode quoteParams = { pluginId: currencyPluginId, @@ -225,7 +273,9 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi } } - const quotePromises = providers.filter(p => (providerId == null ? true : providerId === p.providerId)).map(async p => await p.getQuote(quoteParams)) + const quotePromises = finalProvidersArray + .filter(p => (providerId == null ? true : providerId === p.providerId)) + .map(async p => await p.getQuote(quoteParams)) let errors: unknown[] = [] const quotes = await fuzzyTimeout(quotePromises, 5000).catch(e => { errors = e @@ -259,7 +309,7 @@ export const amountQuoteFiatPlugin: FiatPluginFactory = async (params: FiatPlugi const exchangeRateText = getRateFromQuote(bestQuote, displayFiatCurrencyCode) stateManager.update({ - statusText: { content: exchangeRateText }, + statusText: { content: exchangeRateText, textType: bestQuote.isEstimate ? 'warning' : undefined }, poweredBy: { poweredByText: bestQuote.pluginDisplayName, poweredByIcon: bestQuote.partnerIcon } }) diff --git a/src/plugins/gui/fiatPlugin.tsx b/src/plugins/gui/fiatPlugin.tsx index 7866e0963cd..d65d2742fc1 100644 --- a/src/plugins/gui/fiatPlugin.tsx +++ b/src/plugins/gui/fiatPlugin.tsx @@ -9,7 +9,6 @@ import SafariView from 'react-native-safari-view' import { DisablePluginMap, NestedDisableMap } from '../../actions/ExchangeInfoActions' import { launchPaymentProto, LaunchPaymentProtoParams } from '../../actions/PaymentProtoActions' import { addressWarnings } from '../../actions/ScanActions' -import { trackConversionWithReferral } from '../../actions/TrackingActions' import { ButtonsModal } from '../../components/modals/ButtonsModal' import { RadioListModal } from '../../components/modals/RadioListModal' import { WalletListModal, WalletListResult } from '../../components/modals/WalletListModal' @@ -18,10 +17,9 @@ import { Airship, showError, showToast, showToastSpinner } from '../../component import { requestPermissionOnSettings } from '../../components/services/PermissionsManager' import { HomeAddress, SepaInfo } from '../../types/FormTypes' import { GuiPlugin } from '../../types/GuiPluginTypes' -import { AccountReferral } from '../../types/ReferralTypes' import { AppParamList, NavigationBase } from '../../types/routerTypes' import { getNavigationAbsolutePath } from '../../util/routerUtils' -import { TrackingEventName } from '../../util/tracking' +import { OnLogEvent, TrackingEventName } from '../../util/tracking' import { FiatPaymentType, FiatPluginAddressFormParams, @@ -42,7 +40,6 @@ export const SendErrorBackPressed = 'SendErrorBackPressed' export const executePlugin = async (params: { account: EdgeAccount - accountReferral: AccountReferral disklet: Disklet deviceId: string direction: 'buy' | 'sell' @@ -53,11 +50,11 @@ export const executePlugin = async (params: { paymentType?: FiatPaymentType providerId?: string regionCode: FiatPluginRegionCode + onLogEvent: OnLogEvent }): Promise => { const { disablePlugins = {}, account, - accountReferral, deviceId, direction, disklet, @@ -66,7 +63,8 @@ export const executePlugin = async (params: { navigation, paymentType, providerId, - regionCode + regionCode, + onLogEvent } = params const { defaultFiatAmount, forceFiatCurrencyCode, pluginId } = guiPlugin @@ -103,7 +101,7 @@ export const executePlugin = async (params: { )) if (result?.type === 'wallet') return result }, - showError: async (e: Error): Promise => showError(e), + showError: async (e: unknown): Promise => showError(e), listModal: async (params: FiatPluginListModalParams): Promise => { const result = await Airship.show(bridge => ( @@ -237,7 +235,7 @@ export const executePlugin = async (params: { orderId?: string } ) => { - await trackConversionWithReferral(event, opts, accountReferral) + onLogEvent(event, opts) }, exitScene: async () => { navigation.pop() diff --git a/src/plugins/gui/fiatPluginTypes.ts b/src/plugins/gui/fiatPluginTypes.ts index d40a21e8b5a..3ba8565077e 100644 --- a/src/plugins/gui/fiatPluginTypes.ts +++ b/src/plugins/gui/fiatPluginTypes.ts @@ -20,6 +20,7 @@ export const asFiatDirection = asValue('buy', 'sell') export type FiatDirection = ReturnType export const asFiatPaymentType = asValue( + 'ach', 'applepay', 'colombiabank', 'credit', @@ -127,7 +128,7 @@ export interface FiatPluginUi { openWebView: (params: FiatPluginOpenWebViewParams) => Promise openExternalWebView: (params: FiatPluginOpenExternalWebViewParams) => Promise walletPicker: (params: { headerTitle: string; allowedAssets?: EdgeAsset[]; showCreateWallet?: boolean }) => Promise - showError: (error: Error) => Promise + showError: (error: unknown) => Promise listModal: (params: FiatPluginListModalParams) => Promise enterAmount: (params: AppParamList['guiPluginEnterAmount']) => void addressForm: (params: FiatPluginAddressFormParams) => Promise diff --git a/src/plugins/gui/fiatProviderTypes.ts b/src/plugins/gui/fiatProviderTypes.ts index 1c8a1f84b78..1e0bfe6deea 100644 --- a/src/plugins/gui/fiatProviderTypes.ts +++ b/src/plugins/gui/fiatProviderTypes.ts @@ -49,10 +49,20 @@ export class FiatProviderError extends Error { } } +export interface ProviderToken { + tokenId: EdgeTokenId + otherInfo?: unknown +} + // Supported fiats and cryptos per provider export interface FiatProviderAssetMap { - crypto: { [pluginId: string]: { [tokenId: string]: boolean | any } } + providerId: string + crypto: { [pluginId: string]: ProviderToken[] } fiat: { [currencyCode: string]: boolean | any } + + // This provider REQUIRES that the user enter the amount + // in the specified currency. + requiredAmountType?: 'fiat' | 'crypto' } export interface FiatProviderGetQuoteParams { @@ -90,9 +100,12 @@ export interface FiatProvider { otherMethods: OtherMethods } +export type FiatProviderGetTokenId = (pluginId: string, currencyCode: string) => EdgeTokenId | undefined + export interface FiatProviderFactoryParams { deviceId: string io: { store: FiatProviderStore } + getTokenId: FiatProviderGetTokenId apiKeys?: unknown // Data specific to the requirements of each provider, // which lets the provider know that these orders were made from within Edge. // Typically an API key, but can be some other information like a client ID. diff --git a/src/plugins/gui/pluginUtils.ts b/src/plugins/gui/pluginUtils.ts index 16f30a1a3ef..568a2a02549 100644 --- a/src/plugins/gui/pluginUtils.ts +++ b/src/plugins/gui/pluginUtils.ts @@ -25,9 +25,15 @@ const ERROR_PRIORITIES: { [errorType in FiatProviderQuoteErrorTypes]: number } = } export const getRateFromQuote = (quote: FiatProviderQuote, fiatCode: string): string => { + const { isEstimate } = quote const bestRate = div(quote.fiatAmount, quote.cryptoAmount, 16) const localeRate = formatNumber(toFixed(bestRate, 0, 2)) - const exchangeRateText = `1 ${quote.displayCurrencyCode} = ${localeRate} ${fiatCode}` + let exchangeRateText + if (isEstimate) { + exchangeRateText = `1 ${quote.displayCurrencyCode} ~= ${localeRate} ${fiatCode}\n${lstrings.estimated_quote}` + } else { + exchangeRateText = `1 ${quote.displayCurrencyCode} = ${localeRate} ${fiatCode}` + } return exchangeRateText } diff --git a/src/plugins/gui/providers/banxaProvider.ts b/src/plugins/gui/providers/banxaProvider.ts index dad1891aab5..cce28e594f6 100644 --- a/src/plugins/gui/providers/banxaProvider.ts +++ b/src/plugins/gui/providers/banxaProvider.ts @@ -1,6 +1,7 @@ // import { div, gt, lt, mul, toFixed } from 'biggystring' import { gt, lt } from 'biggystring' import { asArray, asEither, asMaybe, asNumber, asObject, asString, asValue } from 'cleaners' +import { EdgeTokenId } from 'edge-core-js' import URL from 'url-parse' import { SendScene2Params } from '../../../components/scenes/SendScene2' @@ -18,8 +19,10 @@ import { FiatProviderFactory, FiatProviderFactoryParams, FiatProviderGetQuoteParams, + FiatProviderGetTokenId, FiatProviderQuote } from '../fiatProviderTypes' +import { addTokenToArray } from '../util/providerUtils' import { NOT_SUCCESS_TOAST_HIDE_MS, RETURN_URL_CANCEL, RETURN_URL_FAIL, RETURN_URL_SUCCESS } from './common' const providerId = 'banxa' const storeId = 'banxa' @@ -282,7 +285,10 @@ const COIN_TO_CURRENCY_CODE_MAP: StringMap = { BTC: 'BTC' } const asInfoCreateHmacResponse = asObject({ signature: asString }) -const allowedCurrencyCodes: Record = { buy: { fiat: {}, crypto: {} }, sell: { fiat: {}, crypto: {} } } +const allowedCurrencyCodes: Record = { + buy: { providerId, fiat: {}, crypto: {} }, + sell: { providerId, fiat: {}, crypto: {} } +} const banxaPaymentsMap: Record = { buy: {}, sell: {} } export const banxaProvider: FiatProviderFactory = { @@ -291,6 +297,7 @@ export const banxaProvider: FiatProviderFactory = { makeProvider: async (params: FiatProviderFactoryParams): Promise => { const { apiKeys, + getTokenId, io: { store } } = params const { apiKey, hmacUser, partnerUrl: url } = asBanxaApiKeys(apiKeys) @@ -312,7 +319,8 @@ export const banxaProvider: FiatProviderFactory = { pluginDisplayName, getSupportedAssets: async ({ direction, paymentTypes }): Promise => { // Return nothing if paymentTypes are not supported by this provider - if (!paymentTypes.some(paymentType => allowedPaymentTypes[direction][paymentType] === true)) return { crypto: {}, fiat: {} } + if (!paymentTypes.some(paymentType => allowedPaymentTypes[direction][paymentType] === true)) + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) const fiats = allowedCurrencyCodes[direction].fiat const cryptos = allowedCurrencyCodes[direction].fiat @@ -339,7 +347,7 @@ export const banxaProvider: FiatProviderFactory = { const currencyPluginId = CURRENCY_PLUGINID_MAP[chain.code] if (currencyPluginId != null) { const edgeCurrencyCode = COIN_TO_CURRENCY_CODE_MAP[coin.coin_code] ?? coin.coin_code - addToAllowedCurrencies(currencyPluginId, direction, edgeCurrencyCode, coin) + addToAllowedCurrencies(getTokenId, currencyPluginId, direction, edgeCurrencyCode, coin) } } } @@ -371,7 +379,7 @@ export const banxaProvider: FiatProviderFactory = { let banxaCrypto try { - banxaCrypto = edgeToBanxaCrypto(pluginId, direction, displayCurrencyCode) + banxaCrypto = edgeToBanxaCrypto(pluginId, direction, tokenId) } catch (e: any) { throw new FiatProviderError({ providerId, errorType: 'assetUnsupported' }) } @@ -470,7 +478,7 @@ export const banxaProvider: FiatProviderFactory = { const { showUi, coreWallet } = approveParams const success = await showUi.requestPermission(['camera'], pluginDisplayName, true) if (!success) { - await showUi.showError(new Error(lstrings.fiat_plugin_cannot_continue_camera_permission)) + await showUi.showError(lstrings.fiat_plugin_cannot_continue_camera_permission) } const receiveAddress = await coreWallet.getReceiveAddress({ tokenId: null }) @@ -682,9 +690,18 @@ const banxaFetch = async (params: { return reply } -const addToAllowedCurrencies = (pluginId: string, direction: FiatDirection, currencyCode: string, coin: BanxaCryptoCoin) => { - if (allowedCurrencyCodes[direction].crypto[pluginId] == null) allowedCurrencyCodes[direction].crypto[pluginId] = {} - allowedCurrencyCodes[direction].crypto[pluginId][currencyCode] = coin +const addToAllowedCurrencies = ( + getTokenId: FiatProviderGetTokenId, + pluginId: string, + direction: FiatDirection, + currencyCode: string, + coin: BanxaCryptoCoin +) => { + if (allowedCurrencyCodes[direction].crypto[pluginId] == null) allowedCurrencyCodes[direction].crypto[pluginId] = [] + const tokens = allowedCurrencyCodes[direction].crypto[pluginId] + const tokenId = getTokenId(pluginId, currencyCode) + if (tokenId === undefined) return + addTokenToArray({ tokenId, otherInfo: coin }, tokens) } const typeMap: { [Payment in BanxaPaymentType]: FiatPaymentType } = { @@ -771,11 +788,12 @@ const getPaymentIdLimit = (direction: FiatDirection, fiat: string, banxaCoin: st } // Takes an EdgeAsset and returns the corresponding Banxa chain code and coin code -const edgeToBanxaCrypto = (pluginId: string, direction: FiatDirection, displayCurrencyCode: string): { banxaChain: string; banxaCoin: string } => { +const edgeToBanxaCrypto = (pluginId: string, direction: FiatDirection, tokenId: EdgeTokenId): { banxaChain: string; banxaCoin: string } => { const tokens = allowedCurrencyCodes[direction].crypto[pluginId] if (tokens == null) throw new Error(`edgeToBanxaCrypto ${pluginId} not allowed`) - const banxaCoin = asBanxaCryptoCoin(tokens[displayCurrencyCode]) - if (banxaCoin == null) throw new Error(`edgeToBanxaCrypto ${pluginId} ${displayCurrencyCode} not allowed`) + const providerToken = tokens.find(t => t.tokenId === tokenId) + const banxaCoin = asBanxaCryptoCoin(providerToken?.otherInfo) + if (banxaCoin == null) throw new Error(`edgeToBanxaCrypto ${pluginId} ${tokenId} not allowed`) for (const chain of banxaCoin.blockchains) { // @ts-expect-error const edgePluginId = CURRENCY_PLUGINID_MAP[chain.code] diff --git a/src/plugins/gui/providers/bityProvider.ts b/src/plugins/gui/providers/bityProvider.ts index 7d9d08e8703..2dc822afe42 100644 --- a/src/plugins/gui/providers/bityProvider.ts +++ b/src/plugins/gui/providers/bityProvider.ts @@ -15,8 +15,10 @@ import { FiatProviderFactory, FiatProviderFactoryParams, FiatProviderGetQuoteParams, + FiatProviderGetTokenId, FiatProviderQuote } from '../fiatProviderTypes' +import { addTokenToArray } from '../util/providerUtils' const providerId = 'bity' const storeId = 'com.bity' @@ -27,7 +29,7 @@ const supportEmail = 'support@bity.com' const supportedPaymentType: FiatPaymentType = 'sepa' const partnerFee = 0.005 -const allowedCurrencyCodes: FiatProviderAssetMap = { crypto: {}, fiat: {} } +const allowedCurrencyCodes: FiatProviderAssetMap = { providerId, crypto: {}, fiat: {} } const allowedCountryCodes: { readonly [code: string]: boolean } = { AT: true, BE: true, @@ -288,7 +290,7 @@ export const bityProvider: FiatProviderFactory = { providerId, storeId, makeProvider: async (params: FiatProviderFactoryParams): Promise => { - const { apiKeys } = params + const { apiKeys, getTokenId } = params const clientId = asBityApiKeys(apiKeys).clientId const out: FiatProvider = { @@ -297,7 +299,7 @@ export const bityProvider: FiatProviderFactory = { pluginDisplayName, getSupportedAssets: async ({ paymentTypes }): Promise => { // Return nothing if 'sepa' is not included in the props - if (!paymentTypes.includes(supportedPaymentType)) return { crypto: {}, fiat: {} } + if (!paymentTypes.includes(supportedPaymentType)) throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) const response = await fetch(`https://exchange.api.bity.com/v2/currencies`).catch(e => undefined) if (response == null || !response.ok) { @@ -323,11 +325,11 @@ export const bityProvider: FiatProviderFactory = { // overlap, such as USDC being 'crypto', 'ethereum', 'erc20'. if (currency.tags.includes('erc20') && currency.tags.includes('ethereum')) { // ETH tokens - addToAllowedCurrencies('ethereum', currency, currency.code) + addToAllowedCurrencies(getTokenId, 'ethereum', currency, currency.code) isAddCurrencySuccess = true } else if (Object.keys(CURRENCY_PLUGINID_MAP).includes(currency.code)) { // Mainnet currencies - addToAllowedCurrencies(CURRENCY_PLUGINID_MAP[currency.code], currency, currency.code) + addToAllowedCurrencies(getTokenId, CURRENCY_PLUGINID_MAP[currency.code], currency, currency.code) isAddCurrencySuccess = true } } @@ -354,7 +356,8 @@ export const bityProvider: FiatProviderFactory = { if (!allowedCountryCodes[regionCode.countryCode]) throw new FiatProviderError({ providerId, errorType: 'regionRestricted', displayCurrencyCode }) if (!paymentTypes.includes(supportedPaymentType)) throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) - const cryptoCurrencyObj = asBityCurrency(allowedCurrencyCodes.crypto[pluginId][displayCurrencyCode]) + const bityCurrency = allowedCurrencyCodes.crypto[pluginId].find(t => t.tokenId === tokenId) + const cryptoCurrencyObj = asBityCurrency(bityCurrency?.otherInfo) const fiatCurrencyObj = asBityCurrency(allowedCurrencyCodes.fiat[fiatCurrencyCode]) if (cryptoCurrencyObj == null || fiatCurrencyObj == null) throw new Error('Bity: Could not query supported currencies') @@ -471,9 +474,13 @@ export const bityProvider: FiatProviderFactory = { } } -const addToAllowedCurrencies = (pluginId: string, currency: BityCurrency, currencyCode: string) => { - if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = {} - allowedCurrencyCodes.crypto[pluginId][currencyCode] = currency +const addToAllowedCurrencies = (getTokenId: FiatProviderGetTokenId, pluginId: string, currency: BityCurrency, currencyCode: string) => { + if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = [] + const tokenId = getTokenId(pluginId, currencyCode) + if (tokenId === undefined) return + + const tokens = allowedCurrencyCodes.crypto[pluginId] + addTokenToArray({ tokenId, otherInfo: currency }, tokens) } /** diff --git a/src/plugins/gui/providers/dummyProvider.ts b/src/plugins/gui/providers/dummyProvider.ts deleted file mode 100644 index 5d9deeca6d7..00000000000 --- a/src/plugins/gui/providers/dummyProvider.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { div, gt, lt, mul, toFixed } from 'biggystring' - -import { - FiatProvider, - FiatProviderApproveQuoteParams, - FiatProviderAssetMap, - FiatProviderError, - FiatProviderFactory, - FiatProviderFactoryParams, - FiatProviderGetQuoteParams, - FiatProviderQuote -} from '../fiatProviderTypes' -const providerId = 'dummyprovider' -const storeId = 'com.dummyprovider' -const partnerIcon = 'simplex-logo-sm-square.png' -const pluginDisplayName = 'Dummy' - -const SIMPLEX_ID_MAP: { [pluginId: string]: { [currencyCode: string]: string } } = { - avalanche: { AVAX: 'AVAX-C' }, - binance: { AVA: 'AVA', BNB: 'BNB' }, - binancesmartchain: { - BABYDOGE: 'BABYDOGE', - BAKE: 'BAKE', - BNB: 'BNB', - BUSD: 'BUSD-SC', - CAKE: 'CAKE', - EGC: 'EGC', - KMON: 'KMON', - SATT: 'SATT-SC', - TCT: 'TCT', - ULTI: 'ULTI', - USDC: 'USDC-SC', - XVS: 'XVS' - }, - bitcoin: { BTC: 'BTC' }, - bitcoincash: { BCH: 'BCH' }, - bitcoinsv: { BSV: 'BSV' }, - cardano: { ADA: 'ADA' }, - celo: { CELO: 'CELO', CEUR: 'CEUR', CUSD: 'CUSD' }, - digibyte: { DGB: 'DGB' }, - dogecoin: { DOGE: 'DOGE' }, - eos: { EOS: 'EOS' }, - ethereum: { - '1EARTH': '1EARTH', - AAVE: 'AAVE', - AXS: 'AXS-ERC20', - BAT: 'BAT', - BUSD: 'BUSD', - CEL: 'CEL', - CHZ: 'CHZ', - COMP: 'COMP', - COTI: 'COTI-ERC20', - CRO: 'CRO-ERC20', - DAI: 'DAI', - DEP: 'DEP', - DFT: 'DFT', - ELON: 'ELON', - ENJ: 'ENJ', - ETH: 'ETH', - GALA: 'GALA', - GHX: 'GHX', - GMT: 'GMT-ERC20', - GOVI: 'GOVI', - HEDG: 'HEDG', - HGOLD: 'HGOLD', - HUSD: 'HUSD', - KCS: 'KCS', - LINK: 'LINK', - MANA: 'MANA', - MATIC: 'MATIC-ERC20', - MKR: 'MKR', - PRT: 'PRT', - REVV: 'REVV', - RFOX: 'RFOX', - RFUEL: 'RFUEL', - RLY: 'RLY-ERC20', - SAND: 'SAND', - SATT: 'SATT-ERC20', - SHIB: 'SHIB', - SUSHI: 'SUSHI', - TRU: 'TRU', - TUSD: 'TUSD', - UNI: 'UNI', - UOS: 'UOS-ERC20', - USDC: 'USDC', - USDK: 'USDK', - USDP: 'USDP', - USDT: 'USDT', - VNDC: 'VNDC', - WBTC: 'WBTC', - XAUT: 'XAUT', - XYO: 'XYO' - }, - fantom: { FTM: 'FTM' }, - groestlcoin: { GRS: 'GRS' }, - hedera: { HBAR: 'HBAR' }, - litecoin: { LTC: 'LTC' }, - one: { ONE: 'ONE' }, - polkadot: { DOT: 'DOT' }, - polygon: { GMEE: 'GMEE', MATIC: 'MATIC', USDC: 'USDC-MATIC' }, - qtum: { QTUM: 'QTUM' }, - ravencoin: { RVN: 'RVN' }, - ripple: { XRP: 'XRP' }, - solana: { KIN: 'KIN', SOL: 'SOL' }, - stellar: { XLM: 'XLM' }, - tezos: { XTZ: 'XTZ' }, - tron: { - BTT: 'BTT', - KLV: 'KLV', - TRX: 'TRX', - USDC: 'USDC-TRC20', - USDT: 'USDT-TRC20' - }, - wax: { WAX: 'WAXP' } -} - -const allowedCurrencyCodes: FiatProviderAssetMap = { fiat: { 'iso:USD': true }, crypto: {} } - -for (const pluginId in SIMPLEX_ID_MAP) { - const codesObject = SIMPLEX_ID_MAP[pluginId] - for (const currencyCode in codesObject) { - if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = {} - allowedCurrencyCodes.crypto[pluginId][currencyCode] = true - } -} - -export const dummyProvider: FiatProviderFactory = { - providerId, - storeId, - makeProvider: async (params: FiatProviderFactoryParams): Promise => { - const out = { - providerId, - partnerIcon, - pluginDisplayName, - getSupportedAssets: async (): Promise => allowedCurrencyCodes, - getQuote: async (params: FiatProviderGetQuoteParams): Promise => { - const { regionCode, paymentTypes } = params - - const MIN_USD = '50' - const MAX_USD = '20000' - - let pairCodes - const url = 'https://rates2.edge.app/v1/exchangeRate?currency_pair=' - const { displayCurrencyCode = 'BTC' } = params - let fiatAmount, cryptoAmount - if (params.amountType === 'fiat') { - pairCodes = `USD_${displayCurrencyCode}` - } else { - pairCodes = `${displayCurrencyCode}_USD` - } - - const response = await fetch(url + pairCodes).catch(e => undefined) - if (response == null || !response.ok) throw new Error('Dummy failed to fetch') - const result = await response.json().catch(e => undefined) - - let maxLimit: number - let minLimit: number - if (params.amountType === 'fiat') { - const rateShift = (1 - Math.random() * 0.05).toString() // Randomly apply a fee between 0-5% - cryptoAmount = result?.exchangeRate ?? '0' - cryptoAmount = mul(cryptoAmount, params.exchangeAmount) - cryptoAmount = mul(cryptoAmount, rateShift) - fiatAmount = params.exchangeAmount - maxLimit = parseFloat(MAX_USD) - minLimit = parseFloat(MIN_USD) - } else { - const rateShift = (1 + Math.random() * 0.05).toString() // Randomly apply a fee between 0-5% - fiatAmount = result?.exchangeRate ?? '0' - fiatAmount = mul(fiatAmount, params.exchangeAmount) - fiatAmount = mul(fiatAmount, rateShift) - cryptoAmount = params.exchangeAmount - maxLimit = parseFloat(toFixed(mul(MAX_USD, div(cryptoAmount, fiatAmount, 16)), 0, 6)) - minLimit = parseFloat(toFixed(mul(MIN_USD, div(cryptoAmount, fiatAmount, 16)), 0, 6)) - } - if (gt(fiatAmount, MAX_USD)) { - throw new FiatProviderError({ providerId, errorType: 'overLimit', errorAmount: maxLimit }) - } - if (lt(fiatAmount, MIN_USD)) { - throw new FiatProviderError({ providerId, errorType: 'underLimit', errorAmount: minLimit }) - } - - const paymentQuote: FiatProviderQuote = { - providerId, - regionCode, - paymentTypes, - partnerIcon, - pluginDisplayName, - displayCurrencyCode: params.displayCurrencyCode, - isEstimate: false, - fiatCurrencyCode: params.fiatCurrencyCode, - fiatAmount, - cryptoAmount, - direction: params.direction, - expirationDate: new Date(Date.now() + 60000), - approveQuote: async (params: FiatProviderApproveQuoteParams): Promise => { - await params.showUi.openExternalWebView({ url: 'https://edge.app' }) - }, - closeQuote: async (): Promise => {} - } - return paymentQuote - }, - otherMethods: null - } - return out - } -} diff --git a/src/plugins/gui/providers/dummyProvider2.ts b/src/plugins/gui/providers/dummyProvider2.ts deleted file mode 100644 index bd9c3cbffb8..00000000000 --- a/src/plugins/gui/providers/dummyProvider2.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { div, gt, lt, mul, toFixed } from 'biggystring' - -import { - FiatProvider, - FiatProviderApproveQuoteParams, - FiatProviderAssetMap, - FiatProviderError, - FiatProviderFactory, - FiatProviderFactoryParams, - FiatProviderGetQuoteParams, - FiatProviderQuote -} from '../fiatProviderTypes' -const providerId = 'dummyprovider2' -const storeId = 'com.dummyprovider2' -const partnerIcon = 'icon_black_small.png' -const pluginDisplayName = 'Dummy 2' - -const SIMPLEX_ID_MAP: { [pluginId: string]: { [currencyCode: string]: string } } = { - avalanche: { AVAX: 'AVAX-C' }, - binance: { AVA: 'AVA', BNB: 'BNB' }, - binancesmartchain: { - BABYDOGE: 'BABYDOGE', - BAKE: 'BAKE', - BNB: 'BNB', - BUSD: 'BUSD-SC', - CAKE: 'CAKE', - EGC: 'EGC', - KMON: 'KMON', - SATT: 'SATT-SC', - TCT: 'TCT', - ULTI: 'ULTI', - USDC: 'USDC-SC', - XVS: 'XVS' - }, - bitcoin: { BTC: 'BTC' }, - bitcoincash: { BCH: 'BCH' }, - bitcoinsv: { BSV: 'BSV' }, - cardano: { ADA: 'ADA' }, - celo: { CELO: 'CELO', CEUR: 'CEUR', CUSD: 'CUSD' }, - digibyte: { DGB: 'DGB' }, - dogecoin: { DOGE: 'DOGE' }, - eos: { EOS: 'EOS' }, - ethereum: { - '1EARTH': '1EARTH', - AAVE: 'AAVE', - AXS: 'AXS-ERC20', - BAT: 'BAT', - BUSD: 'BUSD', - CEL: 'CEL', - CHZ: 'CHZ', - COMP: 'COMP', - COTI: 'COTI-ERC20', - CRO: 'CRO-ERC20', - DAI: 'DAI', - DEP: 'DEP', - DFT: 'DFT', - ELON: 'ELON', - ENJ: 'ENJ', - ETH: 'ETH', - GALA: 'GALA', - GHX: 'GHX', - GMT: 'GMT-ERC20', - GOVI: 'GOVI', - HEDG: 'HEDG', - HGOLD: 'HGOLD', - HUSD: 'HUSD', - KCS: 'KCS', - LINK: 'LINK', - MANA: 'MANA', - MATIC: 'MATIC-ERC20', - MKR: 'MKR', - PRT: 'PRT', - REVV: 'REVV', - RFOX: 'RFOX', - RFUEL: 'RFUEL', - RLY: 'RLY-ERC20', - SAND: 'SAND', - SATT: 'SATT-ERC20', - SHIB: 'SHIB', - SUSHI: 'SUSHI', - TRU: 'TRU', - TUSD: 'TUSD', - UNI: 'UNI', - UOS: 'UOS-ERC20', - USDC: 'USDC', - USDK: 'USDK', - USDP: 'USDP', - USDT: 'USDT', - VNDC: 'VNDC', - WBTC: 'WBTC', - XAUT: 'XAUT', - XYO: 'XYO' - }, - fantom: { FTM: 'FTM' }, - groestlcoin: { GRS: 'GRS' }, - hedera: { HBAR: 'HBAR' }, - litecoin: { LTC: 'LTC' }, - one: { ONE: 'ONE' }, - polkadot: { DOT: 'DOT' }, - polygon: { GMEE: 'GMEE', MATIC: 'MATIC', USDC: 'USDC-MATIC' }, - qtum: { QTUM: 'QTUM' }, - ravencoin: { RVN: 'RVN' }, - ripple: { XRP: 'XRP' }, - solana: { KIN: 'KIN', SOL: 'SOL' }, - stellar: { XLM: 'XLM' }, - tezos: { XTZ: 'XTZ' }, - tron: { - BTT: 'BTT', - KLV: 'KLV', - TRX: 'TRX', - USDC: 'USDC-TRC20', - USDT: 'USDT-TRC20' - }, - wax: { WAX: 'WAXP' } -} - -const allowedCurrencyCodes: FiatProviderAssetMap = { fiat: { 'iso:USD': true }, crypto: {} } - -for (const pluginId in SIMPLEX_ID_MAP) { - const codesObject = SIMPLEX_ID_MAP[pluginId] - for (const currencyCode in codesObject) { - if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = {} - allowedCurrencyCodes.crypto[pluginId][currencyCode] = true - } -} - -export const dummyProvider2: FiatProviderFactory = { - providerId, - storeId, - makeProvider: async (params: FiatProviderFactoryParams): Promise => { - const out = { - providerId, - partnerIcon, - pluginDisplayName, - getSupportedAssets: async (): Promise => allowedCurrencyCodes, - getQuote: async (params: FiatProviderGetQuoteParams): Promise => { - const { regionCode, paymentTypes } = params - - const MIN_USD = '50' - const MAX_USD = '20000' - - let pairCodes - const url = 'https://rates2.edge.app/v1/exchangeRate?currency_pair=' - const { displayCurrencyCode = 'BTC' } = params - let fiatAmount, cryptoAmount - if (params.amountType === 'fiat') { - pairCodes = `USD_${displayCurrencyCode}` - } else { - pairCodes = `${displayCurrencyCode}_USD` - } - - const response = await fetch(url + pairCodes).catch(e => undefined) - if (response == null || !response.ok) throw new Error('Dummy 2 failed to fetch') - const result = await response.json().catch(e => undefined) - - let maxLimit: number - let minLimit: number - if (params.amountType === 'fiat') { - const rateShift = (1 - Math.random() * 0.05).toString() // Randomly apply a fee between 0-5% - cryptoAmount = result?.exchangeRate ?? '0' - cryptoAmount = mul(cryptoAmount, params.exchangeAmount) - cryptoAmount = mul(cryptoAmount, rateShift) - fiatAmount = params.exchangeAmount - maxLimit = parseFloat(MAX_USD) - minLimit = parseFloat(MIN_USD) - } else { - const rateShift = (1 + Math.random() * 0.05).toString() // Randomly apply a fee between 0-5% - fiatAmount = result?.exchangeRate ?? '0' - fiatAmount = mul(fiatAmount, params.exchangeAmount) - fiatAmount = mul(fiatAmount, rateShift) - cryptoAmount = params.exchangeAmount - maxLimit = parseFloat(toFixed(mul(MAX_USD, div(cryptoAmount, fiatAmount, 16)), 0, 6)) - minLimit = parseFloat(toFixed(mul(MIN_USD, div(cryptoAmount, fiatAmount, 16)), 0, 6)) - } - if (gt(fiatAmount, MAX_USD)) { - throw new FiatProviderError({ providerId, errorType: 'overLimit', errorAmount: maxLimit }) - } - if (lt(fiatAmount, MIN_USD)) { - throw new FiatProviderError({ providerId, errorType: 'underLimit', errorAmount: minLimit }) - } - - const paymentQuote: FiatProviderQuote = { - providerId, - regionCode, - paymentTypes, - partnerIcon, - pluginDisplayName, - displayCurrencyCode: params.displayCurrencyCode, - isEstimate: false, - fiatCurrencyCode: params.fiatCurrencyCode, - fiatAmount, - cryptoAmount, - direction: params.direction, - expirationDate: new Date(Date.now() + 60000), - approveQuote: async (params: FiatProviderApproveQuoteParams): Promise => { - await params.showUi.openExternalWebView({ url: 'https://edge.app' }) - }, - closeQuote: async (): Promise => {} - } - return paymentQuote - }, - otherMethods: null - } - return out - } -} diff --git a/src/plugins/gui/providers/ioniaProvider.ts b/src/plugins/gui/providers/ioniaProvider.ts index 0b479de89fc..7f6e655108e 100644 --- a/src/plugins/gui/providers/ioniaProvider.ts +++ b/src/plugins/gui/providers/ioniaProvider.ts @@ -29,6 +29,8 @@ import { makeUuid } from '../../../util/utils' import { FiatProvider, FiatProviderAssetMap, FiatProviderFactory, FiatProviderGetQuoteParams, FiatProviderQuote } from '../fiatProviderTypes' import { RewardsCardItem, UserRewardsCards } from '../RewardsCardPlugin' +const providerId = 'ionia' + // JWT 24 hour access token for Edge let ACCESS_TOKEN: string @@ -398,12 +400,13 @@ export const makeIoniaProvider: FiatProviderFactory = { async getSupportedAssets() { const fiatProviderAssetMap: FiatProviderAssetMap = { + providerId, crypto: { - bitcoin: { BTC: true }, - bitcoincash: { BCH: true }, - dash: { DASH: true }, - dogecoin: { DOGE: true }, - litecoin: { LTC: true } + bitcoin: [{ tokenId: null }], + bitcoincash: [{ tokenId: null }], + dash: [{ tokenId: null }], + dogecoin: [{ tokenId: null }], + litecoin: [{ tokenId: null }] }, fiat: { 'iso:USD': true diff --git a/src/plugins/gui/providers/kadoProvider.ts b/src/plugins/gui/providers/kadoProvider.ts new file mode 100644 index 00000000000..6182ae22c92 --- /dev/null +++ b/src/plugins/gui/providers/kadoProvider.ts @@ -0,0 +1,798 @@ +import { gt, lt } from 'biggystring' +import { asArray, asBoolean, asNumber, asObject, asOptional, asString, asValue } from 'cleaners' +import { EdgeAssetAction, EdgeSpendInfo, EdgeTokenId, EdgeTxActionFiat } from 'edge-core-js' +import URL from 'url-parse' + +import { SendScene2Params } from '../../../components/scenes/SendScene2' +import { lstrings } from '../../../locales/strings' +import { isHex } from '../../../util/utils' +import { SendErrorBackPressed, SendErrorNoTransaction } from '../fiatPlugin' +import { FiatDirection, FiatPaymentType, SaveTxActionParams } from '../fiatPluginTypes' +import { + FiatProvider, + FiatProviderApproveQuoteParams, + FiatProviderAssetMap, + FiatProviderError, + FiatProviderFactory, + FiatProviderFactoryParams, + FiatProviderGetQuoteParams, + FiatProviderQuote +} from '../fiatProviderTypes' +const providerId = 'kado' +const storeId = 'money.kado' +const partnerIcon = 'kado.png' +const pluginDisplayName = 'Kado' +const providerDisplayName = 'Kado' +const supportEmail = 'support@kado.money' + +const urls = { + api: { + prod: 'https://api.kado.money', + test: 'https://test-api.kado.money' + }, + widget: { + prod: 'https://app.kado.money', + test: 'https://sandbox--kado.netlify.app' + } +} + +const MODE = 'prod' + +// https://api.kado.money/v1/ramp/blockchains + +// Maps Edge pluginIds to Kado blockchain.origin values +const PLUGIN_TO_CHAIN_ID_MAP: { [pluginId: string]: string } = { + // stellar: 'stellar', // Needs destination tag support + solana: 'solana', + // ripple: 'ripple', // Needs destination tag support + polygon: 'polygon', + osmosis: 'osmosis', + optimism: 'optimism', + litecoin: 'litecoin', + ethereum: 'ethereum', + avalanche: 'avalanche', + // cosmos: 'cosmos hub', + bitcoin: 'bitcoin' +} + +const CHAIN_ID_TO_PLUGIN_MAP: { [chainId: string]: string } = Object.entries(PLUGIN_TO_CHAIN_ID_MAP).reduce( + (out: { [chainId: string]: string }, [pluginId, chainId]) => { + out[chainId] = pluginId + return out + }, + {} +) + +type AllowedPaymentTypes = Record + +const allowedPaymentTypes: AllowedPaymentTypes = { + buy: { + iach: true + }, + sell: { + ach: true + } +} + +const allowedBuyCurrencyCodes: FiatProviderAssetMap = { providerId, requiredAmountType: 'fiat', crypto: {}, fiat: {} } +const allowedSellCurrencyCodes: FiatProviderAssetMap = { providerId, requiredAmountType: 'crypto', crypto: {}, fiat: {} } +const allowedCountryCodes: { [code: string]: boolean } = { US: true } + +/** + * Cleaner for /v1/ramp/blockchains + */ + +// Define cleaners for the nested structures first +// const asPrimeTrust = asObject({ +// assetId: asString, +// assetTransferType: asString, +// assetType: asString +// }) + +const asAssociatedAsset = asObject({ + // _id: asString, + // name: asString, + // description: asString, + // label: asString, + symbol: asString, + // supportedProviders: asArray(asString), + // primeTrust: asPrimeTrust, + // stablecoin: asBoolean, + liveOnRamp: asOptional(asBoolean), + // usesOsmoRouter: asOptional(asBoolean), + // usesLifiRouter: asOptional(asBoolean), + // usesAvaxRouter: asOptional(asBoolean), + // usesInjectiveRouter: asOptional(asBoolean), + // supportedOperations: asOptional(asArray(asUnknown)), + // ibcDenom: asOptional(asString), + // ibcChannelIdOnRamp: asOptional(asNumber), + // ibcChannelIdOffRamp: asOptional(asNumber), + // coingeckoId: asString, + // createdAt: asDate, + // updatedAt: asDate, + // __v: asOptional(asNumber), + // priority: asOptional(asNumber), + address: asOptional(asString), + isNative: asBoolean, + // blockExplorerURI: asOptional(asString), + // decimals: asOptional(asNumber), + // officialChainId: asOptional(asString), + // precision: asOptional(asNumber), + rampProducts: asOptional(asArray(asValue('buy', 'sell'))) + // wallets: asOptional(asArray(asString)), + // fortressSymbol: asOptional(asString), + // fortressChainId: asOptional(asString), + // squidChainId: asOptional(asString), + // squidWalletExample: asOptional(asString), + // squidAssetId: asOptional(asString), + // canBeUsedForLiquidityFulfillment: asOptional(asBoolean), + // rpcURI: asOptional(asString), + // usesPolygonFulfillment: asOptional(asBoolean), + // contractAddress: asOptional(asString) + // fallbackAddress: asOptional(asString), + // fallbackAddressCoinType: asOptional(asNumber), + // lifiSymbol: asOptional(asString), + // displayPrecision: asOptional(asNumber), + // osmoPfmChannel: asOptional(asNumber), + // osmoPfmReceiver: asOptional(asString) +}) + +const asBlockchain = asObject({ + // _id: asString, + // supportedEnvironment: asMaybe(asValue('production')), + // network: asString, + origin: asString, + // label: asString, + associatedAssets: asArray(asAssociatedAsset), + // avgTransactionTimeSeconds: asNumber, + liveOnRamp: asBoolean + // createdAt: asDate, + // updatedAt: asDate, + // __v: asOptional(asNumber), + // priority: asOptional(asNumber), + // ecosystem: asOptional(asString), + // officialId: asString + // usesAvaxRouter: asOptional(asBoolean), + // usesInjectiveRouter: asOptional(asBoolean), + // usesLifiRouter: asOptional(asBoolean), + // usesOsmoRouter: asOptional(asBoolean), + // fortressId: asOptional(asString), + // networkFee: asOptional( + // asObject({ + // unit: asString, + // amount: asNumber + // }) + // ) +}) + +// Define the main cleaner +const asBlockchains = asObject({ + success: asBoolean, + // message: asString, + data: asObject({ + blockchains: asArray(asBlockchain) + }) +}) + +/** + * Cleaner for /v2/ramp/quote + */ + +// Define cleaners for nested objects and properties +// const asAmountCurrency = asObject({ +// amount: asNumber, +// currency: asString +// }) + +// const asRequest = asObject({ +// transactionType: asValue('buy', 'sell'), +// fiatMethod: asString, +// amount: asNumber, +// blockchain: asString, +// currency: asString, +// asset: asString, +// partner: asString +// }) + +// const asPrice = asObject({ +// amount: asNumber, +// price: asNumber, +// symbol: asString, +// unit: asString +// }) + +const asMinMaxValue = asObject({ + amount: asNumber, + unit: asString +}) + +const asQuote = asObject({ + // asset: asString, + // baseAmount: asAmountCurrency, + // pricePerUnit: asNumber, + // price: asPrice, + // processingFee: asAmountCurrency, + // bridgeFee: asAmountCurrency, + // networkFee: asAmountCurrency, + // smartContractFee: asAmountCurrency, + // totalFee: asAmountCurrency, + // receiveAmountAfterFees: asAmountCurrency, + // receiveUnitCountAfterFees: asAmountCurrency, + receive: asObject({ + // originalAmount: asNumber, + amount: asNumber, + // unit: asString, + unitCount: asNumber + // symbol: asString + }), + // feeType: asString, + minValue: asMinMaxValue, + maxValue: asMinMaxValue +}) + +// Main cleaner for the JSON structure +const asQuoteResponse = asObject({ + success: asBoolean, + message: asString, + data: asObject({ + // request: asRequest, + quote: asQuote + }) +}) + +const asApiKeys = asObject({ + apiKey: asString +}) + +const asTokenOtherInfo = asObject({ + symbol: asString +}) + +/** + * Cleaner for /v2/public/orders/{orderId} + */ + +// const asFeeItem = asObject({ +// amount: asNumber, +// name: asString, +// unit: asString, +// // discount: asOptional(asEither(asString, asNull)), +// createdAt: asString, +// updatedAt: asString +// }) + +// const asProcessingFee = asObject({ +// amount: asNumber, +// unit: asString, +// name: asString, +// originalAmount: asNumber, +// promotionModifier: asNumber +// }) + +const asCryptoCurrency = asObject({ + // usesOsmoRouter: asBoolean, + // usesLifiRouter: asBoolean, + // usesAvaxRouter: asBoolean, + // usesPolygonFulfillment: asBoolean, + // usesAxelarBridge: asBoolean, + // usesInjectiveRouter: asBoolean, + // supportedOperations: asArray(asString), + // canBeUsedForLiquidityFulfillment: asBoolean, + // canBeUsedWithFireblocks: asBoolean, + // logoURI: asString, + // fallbackAddress: asString, + // fallbackAddressCoinType: asNumber, + // rpcUrl: asString, + // osmoPfmChannel: asOptional(asString), + // osmoPfmReceiver: asString, + // _id: asString, + // name: asString, + // description: asString, + // label: asString, + // supportedProviders: asArray(asString), + // stablecoin: asBoolean, + // liveOnRamp: asBoolean, + // priority: asNumber, + // createdAt: asString, + // updatedAt: asString, + symbol: asString, + // fortressSymbol: asString, + // fortressChainId: asString, + // coingeckoId: asString, + address: asOptional(asString), + isNative: asBoolean + // blockExplorerURI: asString, + // decimals: asNumber, + // officialChainId: asString, + // precision: asNumber, + // rampProducts: asArray(asString), + // wallets: asArray(asString) +}) + +// const asChain = asObject({ +// _id: asString, +// supportedEnvironment: asString, +// network: asString, +// origin: asString +// label: asString, +// associatedAssets: asArray(asString), +// avgTransactionTimeSeconds: asNumber, +// liveOnRamp: asBoolean, +// priority: asNumber, +// createdAt: asString, +// updatedAt: asString, +// ecosystem: asString, +// fortressId: asString, +// officialId: asString +// }) + +const asAmountUnitPair = asObject({ + amount: asOptional(asNumber), + unit: asString +}) + +const asOrderData = asObject({ + // to: asString, + // from: asString, + // exchangeRate: asNumber, + // currencyType: asString, + // walletAddress: asString, + depositAddress: asString, + // sendFrom: asString, + // sendTo: asString, + // processingFee: asProcessingFee, + // bridgeFee: asArray(asFeeItem), + // smartContractFees: asArray(asFeeItem), + // gasFee: asOptional(asEither(asNumber, asNull)), + // asset: asString, + // assetSymbol: asString, + blockchain: asString, + // txHash: asString, + // humanStatusField: asString, + // machineStatusField: asString, + providerDisbursementStatus: asString, + // manualDepositWalletAddress: asOptional(asString), + // manualDepositMemo: asOptional(asString), + // createdAt: asString, + // paymentStatus: asOptional(asString), + // transferStatus: asString, + // allPossibleTxHashes: asObject({}), + // method: asString, + // orderType: asString, + // id: asString, + // originOfOrder: asString, + // userRef: asString, + // anchorTransactionId: asString, + // quote: asAmountUnitPair, + payAmount: asAmountUnitPair, + // recvAmount: asAmountUnitPair, + // totalFee: asAmountUnitPair, + cryptoCurrency: asCryptoCurrency + // chain: asChain + // settlementTimeInSeconds: asNumber +}) + +const asOrderInfo = asObject({ + success: asBoolean, + message: asString, + data: asOrderData +}) + +interface GetQuoteParams { + transactionType: 'buy' | 'sell' + fiatMethod: 'ach' | 'card' + amount: number + blockchain: string + currency: string + asset: string +} + +interface WidgetParams { + apiKey: string + mode: 'minimal' + network: string + networkList: string +} + +interface WidgetParamsBuy extends WidgetParams { + onPayAmount: string + onPayCurrency: 'USD' + onRevCurrency: string + onToAddress: string + product: 'BUY' + productList: 'BUY' +} + +interface WidgetParamsSell extends WidgetParams { + offFromAddress: string + offPayAmount: string + offPayCurrency: string + offRevCurrency: 'USD' + product: 'SELL' + productList: 'SELL' +} + +export const kadoProvider: FiatProviderFactory = { + providerId, + storeId, + makeProvider: async (params: FiatProviderFactoryParams): Promise => { + const { apiKeys } = params + const { apiKey } = asApiKeys(apiKeys) + const out: FiatProvider = { + providerId, + partnerIcon, + pluginDisplayName, + getSupportedAssets: async ({ direction, paymentTypes }): Promise => { + // Return nothing if paymentTypes are not supported by this provider + if (!paymentTypes.some(paymentType => allowedPaymentTypes[direction][paymentType] === true)) + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) + const allowedCurrencyCodes = direction === 'buy' ? allowedBuyCurrencyCodes : allowedSellCurrencyCodes + + if (Object.keys(allowedCurrencyCodes.crypto).length > 0) { + return allowedCurrencyCodes + } + + const response = await fetch(`${urls.api[MODE]}/v1/ramp/blockchains`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Widget-Id': apiKey + } + }) + if (!response.ok) { + const text = await response.text() + throw new Error(`Error fetching kado blockchains: ${text}`) + } + const result = await response.json() + + const blockchains = asBlockchains(result) + if (!blockchains.success) { + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) + } + + for (const blockchain of blockchains.data.blockchains) { + const { liveOnRamp } = blockchain + if (!liveOnRamp) continue + const pluginId = CHAIN_ID_TO_PLUGIN_MAP[blockchain.origin] + if (pluginId == null) continue + allowedCurrencyCodes.crypto[pluginId] = [] + const tokens = allowedCurrencyCodes.crypto[pluginId] + + for (const asset of blockchain.associatedAssets) { + const { isNative, address } = asset + + if (asset.rampProducts == null || !asset.rampProducts.includes(direction)) continue + if (isNative) { + tokens.push({ tokenId: null, otherInfo: { symbol: asset.symbol } }) + continue + } + + if (address != null && address !== '0x0000000000000000000000000000000000000000') { + let tokenId: string + if (address.startsWith('0x')) { + // For EVM tokens only, lowercase and remove 0x + tokenId = address.toLowerCase().replace('0x', '') + } else { + tokenId = address + } + tokens.push({ tokenId, otherInfo: { symbol: asset.symbol } }) + } + } + } + + return allowedCurrencyCodes + }, + getQuote: async (params: FiatProviderGetQuoteParams): Promise => { + const { direction, regionCode, exchangeAmount, amountType, paymentTypes, pluginId, displayCurrencyCode, tokenId } = params + + const allowedCurrencyCodes = direction === 'buy' ? allowedBuyCurrencyCodes : allowedSellCurrencyCodes + + if (!allowedCountryCodes[regionCode.countryCode]) throw new FiatProviderError({ providerId, errorType: 'regionRestricted', displayCurrencyCode }) + if (direction === 'buy' && amountType !== 'fiat') { + throw new FiatProviderError({ providerId, errorType: 'assetUnsupported' }) + } + if (direction === 'sell' && amountType !== 'crypto') { + throw new FiatProviderError({ providerId, errorType: 'assetUnsupported' }) + } + if (!paymentTypes.some(paymentType => allowedPaymentTypes[direction][paymentType] === true)) + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) + + const tokens = allowedCurrencyCodes.crypto[pluginId] + const token = tokens.find(t => t.tokenId === tokenId) + if (token == null) throw new FiatProviderError({ providerId, errorType: 'assetUnsupported' }) + const { symbol: asset } = asTokenOtherInfo(token.otherInfo) + const blockchain = PLUGIN_TO_CHAIN_ID_MAP[pluginId] + + // Query for a quote + const queryParams: GetQuoteParams = { + transactionType: direction, + fiatMethod: 'ach', + amount: Number(exchangeAmount), + blockchain, + currency: 'USD', + asset + } + + const urlObj = new URL(`${urls.api[MODE]}/v2/ramp/quote`, true) + urlObj.set('query', queryParams) + + const response = await fetch(urlObj.href, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Widget-Id': apiKey + } + }) + if (!response.ok) { + const text = await response.text() + throw new Error(`Error fetching kado quote: ${text}`) + } + const result = await response.json() + const quote = asQuoteResponse(result) + + const { amount: minAmount, unit: minUnit } = quote.data.quote.minValue + const { amount: maxAmount, unit: maxUnit } = quote.data.quote.maxValue + + let fiatAmount: string + let cryptoAmount: string + if (direction === 'buy') { + const { unitCount } = quote.data.quote.receive + cryptoAmount = unitCount.toString() + fiatAmount = exchangeAmount + } else { + const { amount } = quote.data.quote.receive + cryptoAmount = exchangeAmount + fiatAmount = amount.toString() + } + if (lt(fiatAmount, minAmount.toString())) + throw new FiatProviderError({ providerId, errorType: 'underLimit', errorAmount: minAmount, displayCurrencyCode: minUnit }) + if (gt(fiatAmount, maxAmount.toString())) + throw new FiatProviderError({ providerId, errorType: 'overLimit', errorAmount: maxAmount, displayCurrencyCode: maxUnit }) + + const paymentQuote: FiatProviderQuote = { + providerId, + partnerIcon, + regionCode, + paymentTypes, + pluginDisplayName, + displayCurrencyCode: params.displayCurrencyCode, + isEstimate: true, + fiatCurrencyCode: params.fiatCurrencyCode, + fiatAmount, + cryptoAmount, + direction: params.direction, + expirationDate: new Date(Date.now() + 60000), + approveQuote: async (approveParams: FiatProviderApproveQuoteParams): Promise => { + const { showUi, coreWallet } = approveParams + const receiveAddress = await coreWallet.getReceiveAddress({ tokenId }) + + const url = new URL(`${urls.widget[MODE]}/`, true) + if (direction === 'buy') { + const urlParams: WidgetParamsBuy = { + apiKey: apiKey, + network: blockchain, + networkList: blockchain, + onPayAmount: fiatAmount, + onPayCurrency: 'USD', + onRevCurrency: asset, + onToAddress: receiveAddress.publicAddress, + product: 'BUY', + productList: 'BUY', + mode: 'minimal' + } + url.set('query', urlParams) + } else { + const urlParams: WidgetParamsSell = { + apiKey: apiKey, + network: blockchain, + networkList: blockchain, + offPayAmount: cryptoAmount, + offRevCurrency: 'USD', + product: 'SELL', + productList: 'SELL', + mode: 'minimal', + offPayCurrency: asset, + offFromAddress: receiveAddress.publicAddress + } + url.set('query', urlParams) + } + console.log('Launching Kado webview url=' + url.href) + + // Only open standard webview of ACH and sells. + // Use showUi.openExternalWebView for Apple Pay/Google Pay + if (direction === 'buy') { + await showUi.openWebView({ url: url.href }) + return + } + + let inPayment = false + + const openWebView = async () => { + await showUi.openWebView({ + url: url.href, + onUrlChange: async newUrl => { + console.log(`*** onUrlChange: ${newUrl}`) + + if (!newUrl.startsWith(`${urls.widget[MODE]}/ramp/order`)) { + return + } + const urlObj = new URL(newUrl, true) + const path = urlObj.pathname + const orderId = path.split('/')[3] + + if (isHex(orderId)) { + if (inPayment) return + inPayment = true + try { + const response = await fetch(`${urls.api[MODE]}/v2/public/orders/${orderId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Widget-Id': apiKey + } + }) + if (!response.ok) { + const text = await response.text() + console.warn(`Error fetching kado blockchains: ${text}`) + return allowedCurrencyCodes + } + const result = await response.json() + + const orderInfo = asOrderInfo(result) + if (!orderInfo.success) { + await showUi.showError(lstrings.fiat_plugin_sell_failed_try_again + '\n\norderInfo.success=false') + inPayment = false + return + } + + const { depositAddress, blockchain, cryptoCurrency, payAmount, providerDisbursementStatus } = orderInfo.data + const { amount, unit } = payAmount + const { address, isNative } = cryptoCurrency + + if (amount == null) { + inPayment = false + await showUi.showError(lstrings.fiat_plugin_sell_failed_try_again + '\n\nMissing amount') + return + } + const paymentExchangeAmount = amount.toString() + const paymentPluginId = CHAIN_ID_TO_PLUGIN_MAP[blockchain] + if (paymentPluginId == null || paymentPluginId !== pluginId) { + inPayment = false + await showUi.showError(lstrings.fiat_plugin_sell_failed_try_again + '\n\nMismatched pluginId') + return + } + + let paymentTokenId: EdgeTokenId + if (isNative) { + paymentTokenId = null + } else if (address != null && address !== '0x0000000000000000000000000000000000000000') { + if (address.startsWith('0x')) { + // For EVM tokens only, lowercase and remove 0x + paymentTokenId = address.toLowerCase().replace('0x', '') + } else { + paymentTokenId = address + } + } else { + throw new FiatProviderError({ providerId, errorType: 'assetUnsupported' }) + } + + if (paymentTokenId !== tokenId) { + inPayment = false + await showUi.showError(lstrings.fiat_plugin_sell_failed_try_again + '\n\nMismatched tokenId') + return + } + + if (providerDisbursementStatus !== 'pending') { + await showUi.showError(lstrings.fiat_plugin_sell_failed_try_again + `\n\nproviderDisbursementStatus=${providerDisbursementStatus}`) + inPayment = false + return + } + + console.log(`Creating Kado payment`) + console.log(` paymentExchangeAmount: ${paymentExchangeAmount}`) + console.log(` unit: ${unit}`) + console.log(` blockchain: ${blockchain}`) + console.log(` pluginId: ${pluginId}`) + console.log(` tokenId: ${tokenId}`) + const nativeAmount = await coreWallet.denominationToNative(paymentExchangeAmount, displayCurrencyCode) + + const assetAction: EdgeAssetAction = { + assetActionType: 'sell' + } + const savedAction: EdgeTxActionFiat = { + actionType: 'fiat', + orderId, + orderUri: `${urls.widget[MODE]}/ramp/order/${orderId}`, + isEstimate: true, + fiatPlugin: { + providerId, + providerDisplayName, + supportEmail + }, + payinAddress: depositAddress, + cryptoAsset: { + pluginId, + tokenId, + nativeAmount + }, + fiatAsset: { + fiatCurrencyCode: 'USD', + fiatAmount + } + } + + // Launch the SendScene to make payment + const spendInfo: EdgeSpendInfo = { + tokenId, + assetAction, + savedAction, + spendTargets: [ + { + nativeAmount, + publicAddress: depositAddress + } + ] + } + + const sendParams: SendScene2Params = { + walletId: coreWallet.id, + tokenId, + spendInfo, + lockTilesMap: { + address: true, + amount: true, + wallet: true + }, + hiddenFeaturesMap: { + address: true + } + } + const tx = await showUi.send(sendParams) + await showUi.trackConversion('Sell_Success', { + destCurrencyCode: 'USD', + destExchangeAmount: fiatAmount, + sourceCurrencyCode: displayCurrencyCode, + sourceExchangeAmount: paymentExchangeAmount, + sourcePluginId: coreWallet.currencyInfo.pluginId, + pluginId: providerId, + orderId + }) + + // Save separate metadata/action for token transaction fee + if (tokenId != null) { + const params: SaveTxActionParams = { + walletId: coreWallet.id, + tokenId, + txid: tx.txid, + savedAction, + assetAction: { ...assetAction, assetActionType: 'sell' } + } + await showUi.saveTxAction(params) + } + } catch (e: any) { + if (e.message === SendErrorNoTransaction) { + await showUi.showToast(lstrings.fiat_plugin_sell_failed_to_send_try_again) + } else if (e.message === SendErrorBackPressed) { + await showUi.showToast(lstrings.fiat_plugin_sell_cancelled) + await showUi.exitScene() + } else { + await showUi.showError(e) + } + } finally { + inPayment = false + } + } + } + }) + } + await openWebView() + }, + closeQuote: async (): Promise => {} + } + return paymentQuote + }, + otherMethods: null + } + return out + } +} diff --git a/src/plugins/gui/providers/moonpayProvider.ts b/src/plugins/gui/providers/moonpayProvider.ts index 029adb8b586..d8346303d66 100644 --- a/src/plugins/gui/providers/moonpayProvider.ts +++ b/src/plugins/gui/providers/moonpayProvider.ts @@ -12,14 +12,16 @@ import { FiatProviderFactory, FiatProviderFactoryParams, FiatProviderGetQuoteParams, + FiatProviderGetTokenId, FiatProviderQuote } from '../fiatProviderTypes' +import { addTokenToArray } from '../util/providerUtils' const providerId = 'moonpay' const storeId = 'com.moonpay' const partnerIcon = 'moonpay_symbol_prp.png' const pluginDisplayName = 'Moonpay' -const allowedCurrencyCodes: FiatProviderAssetMap = { crypto: {}, fiat: {} } +const allowedCurrencyCodes: FiatProviderAssetMap = { providerId, crypto: {}, fiat: {} } const allowedCountryCodes: { [code: string]: boolean } = {} const allowedPaymentTypes: { [Payment in FiatPaymentType]?: boolean } = { applepay: true, credit: true, googlepay: true, iach: true } @@ -58,6 +60,8 @@ const asMoonpayCountry = asObject({ isSellAllowed: asBoolean }) +const asApiKeys = asString + const asMoonpayCountries = asArray(asMoonpayCountry) interface MoonpayWidgetQueryParams { @@ -111,7 +115,8 @@ export const moonpayProvider: FiatProviderFactory = { providerId, storeId, makeProvider: async (params: FiatProviderFactoryParams): Promise => { - const apiKey: string | null = typeof params.apiKeys === 'string' ? params.apiKeys : null + const { apiKeys, getTokenId } = params + const apiKey = asApiKeys(apiKeys) if (apiKey == null) throw new Error('Moonpay missing apiKey') const out: FiatProvider = { providerId, @@ -119,10 +124,11 @@ export const moonpayProvider: FiatProviderFactory = { pluginDisplayName, getSupportedAssets: async ({ direction, paymentTypes, regionCode }): Promise => { if (direction !== 'buy') { - return { crypto: {}, fiat: {} } + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) } // Return nothing if paymentTypes are not supported by this provider - if (!paymentTypes.some(paymentType => allowedPaymentTypes[paymentType] === true)) return { crypto: {}, fiat: {} } + if (!paymentTypes.some(paymentType => allowedPaymentTypes[paymentType] === true)) + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) const response = await fetch(`https://api.moonpay.com/v3/currencies?apiKey=${apiKey}`).catch(e => undefined) if (response == null || !response.ok) return allowedCurrencyCodes @@ -142,18 +148,18 @@ export const moonpayProvider: FiatProviderFactory = { continue } if (currency.name.includes('(ERC-20)')) { - addToAllowedCurrencies('ethereum', currency, currency.code) + addToAllowedCurrencies(getTokenId, 'ethereum', currency, currency.code) } else { if (currency.isSuspended) continue if (CURRENCY_CODE_TRANSLATE[currency.code] != null) { const currencyCode = CURRENCY_CODE_TRANSLATE[currency.code] - addToAllowedCurrencies(CURRENCY_PLUGINID_MAP[currencyCode], currency, currencyCode) + addToAllowedCurrencies(getTokenId, CURRENCY_PLUGINID_MAP[currencyCode], currency, currencyCode) currency.code = CURRENCY_CODE_TRANSLATE[currency.code] } else if (TOKEN_MAP[currency.code] != null) { - addToAllowedCurrencies(TOKEN_MAP[currency.code], currency, currency.code) + addToAllowedCurrencies(getTokenId, TOKEN_MAP[currency.code], currency, currency.code) } if (CURRENCY_PLUGINID_MAP[currency.code] != null) { - addToAllowedCurrencies(CURRENCY_PLUGINID_MAP[currency.code], currency, currency.code) + addToAllowedCurrencies(getTokenId, CURRENCY_PLUGINID_MAP[currency.code], currency, currency.code) } } } else { @@ -196,7 +202,9 @@ export const moonpayProvider: FiatProviderFactory = { if (!foundPaymentType) throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) let amountParam = '' - const cryptoCurrencyObj = asMoonpayCurrency(allowedCurrencyCodes.crypto[params.pluginId][params.displayCurrencyCode]) + const tokens = allowedCurrencyCodes.crypto[params.pluginId] + const moonpayCurrency = tokens.find(token => token.tokenId === params.tokenId) + const cryptoCurrencyObj = asMoonpayCurrency(moonpayCurrency?.otherInfo) const fiatCurrencyObj = asMoonpayCurrency(allowedCurrencyCodes.fiat[params.fiatCurrencyCode]) if (cryptoCurrencyObj == null || fiatCurrencyObj == null) throw new Error('Moonpay could not query supported currencies') @@ -282,7 +290,9 @@ export const moonpayProvider: FiatProviderFactory = { } } -const addToAllowedCurrencies = (pluginId: string, currency: MoonpayCurrency, currencyCode: string) => { - if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = {} - allowedCurrencyCodes.crypto[pluginId][currencyCode.toUpperCase()] = currency +const addToAllowedCurrencies = (getTokenId: FiatProviderGetTokenId, pluginId: string, currency: MoonpayCurrency, currencyCode: string) => { + if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = [] + const tokenId = getTokenId(pluginId, currencyCode.toUpperCase()) + if (tokenId === undefined) return + addTokenToArray({ tokenId, otherInfo: currency }, allowedCurrencyCodes.crypto[pluginId]) } diff --git a/src/plugins/gui/providers/paybisProvider.ts b/src/plugins/gui/providers/paybisProvider.ts index 62a7acbfc3d..e2704240784 100644 --- a/src/plugins/gui/providers/paybisProvider.ts +++ b/src/plugins/gui/providers/paybisProvider.ts @@ -21,6 +21,7 @@ import { FiatProviderQuote } from '../fiatProviderTypes' import { assert, isWalletTestnet } from '../pluginUtils' +import { addTokenToArray } from '../util/providerUtils' import { NOT_SUCCESS_TOAST_HIDE_MS, RETURN_URL_FAIL, RETURN_URL_PAYMENT, RETURN_URL_SUCCESS } from './common' const providerId = 'paybis' const storeId = 'paybis' @@ -284,8 +285,8 @@ const SELL_REVERSE_PAYMENT_METHOD_MAP: Partial<{ [Payment in FiatPaymentType]: P } const allowedCurrencyCodes: Record = { - buy: { credit: { fiat: {}, crypto: {} } }, - sell: { credit: { fiat: {}, crypto: {} } } + buy: { credit: { providerId, fiat: {}, crypto: {} } }, + sell: { credit: { providerId, fiat: {}, crypto: {} } } } export const paybisProvider: FiatProviderFactory = { providerId, @@ -310,7 +311,7 @@ export const paybisProvider: FiatProviderFactory = { getSupportedAssets: async ({ direction, paymentTypes }): Promise => { // Return nothing if paymentTypes are not supported by this provider const paymentType = paymentTypes.find(paymentType => allowedPaymentTypes[direction][paymentType] === true) - if (paymentType == null) return { crypto: {}, fiat: {} } + if (paymentType == null) throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) const fiats = allowedCurrencyCodes[direction][paymentType]?.fiat const cryptos = allowedCurrencyCodes[direction][paymentType]?.crypto @@ -330,7 +331,8 @@ export const paybisProvider: FiatProviderFactory = { await initializeSellPairs({ url, apiKey }) } - const out = allowedCurrencyCodes[direction][paymentType] ?? { fiat: {}, crypto: {} } + const out = allowedCurrencyCodes[direction][paymentType] + if (out == null) throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) return out }, getQuote: async (params: FiatProviderGetQuoteParams): Promise => { @@ -460,7 +462,7 @@ export const paybisProvider: FiatProviderFactory = { const { coreWallet, showUi } = approveParams const success = await showUi.requestPermission(['camera'], pluginDisplayName, true) if (!success) { - await showUi.showError(new Error(lstrings.fiat_plugin_cannot_continue_camera_permission)) + await showUi.showError(lstrings.fiat_plugin_cannot_continue_camera_permission) } const receiveAddress = await coreWallet.getReceiveAddress({ tokenId: null }) @@ -709,7 +711,7 @@ const initializeBuyPairs = async ({ url, apiKey }: InitializePairs): Promise => { const { apiKeys, + getTokenId, io: { store } } = params + + for (const pluginId in SIMPLEX_ID_MAP) { + const codesObject = SIMPLEX_ID_MAP[pluginId] + for (const currencyCode in codesObject) { + if (allowedCurrencyCodes.crypto[pluginId] == null) allowedCurrencyCodes.crypto[pluginId] = [] + const tokens = allowedCurrencyCodes.crypto[pluginId] + const tokenId = getTokenId(pluginId, currencyCode) + if (tokenId === undefined) continue + addTokenToArray({ tokenId }, tokens) + } + } + let simplexUserId = await store.getItem('simplex_user_id').catch(e => undefined) if (simplexUserId == null || simplexUserId === '') { simplexUserId = makeUuid() @@ -193,11 +199,12 @@ export const simplexProvider: FiatProviderFactory = { pluginDisplayName, getSupportedAssets: async ({ direction, paymentTypes }): Promise => { if (direction !== 'buy') { - return { crypto: {}, fiat: {} } + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) } // Return nothing if paymentTypes are not supported by this provider - if (!paymentTypes.some(paymentType => allowedPaymentTypes[paymentType] === true)) return { crypto: {}, fiat: {} } + if (!paymentTypes.some(paymentType => allowedPaymentTypes[paymentType] === true)) + throw new FiatProviderError({ providerId, errorType: 'paymentUnsupported' }) const response = await fetch(`https://api.simplexcc.com/v2/supported_fiat_currencies?public_key=${publicKey}`).catch(e => undefined) if (response == null || !response.ok) return allowedCurrencyCodes diff --git a/src/plugins/gui/scenes/AddressFormScene.tsx b/src/plugins/gui/scenes/AddressFormScene.tsx index 152bd758135..98afec2504f 100644 --- a/src/plugins/gui/scenes/AddressFormScene.tsx +++ b/src/plugins/gui/scenes/AddressFormScene.tsx @@ -323,13 +323,7 @@ export const AddressFormScene = React.memo((props: Props) => { onChangeText={handleChangePostalCode} onBlur={handleHideAddressHints} /> - + diff --git a/src/plugins/gui/scenes/FiatPluginEnterAmountScene.tsx b/src/plugins/gui/scenes/FiatPluginEnterAmountScene.tsx index 17ac26c3d51..723d9bf444b 100644 --- a/src/plugins/gui/scenes/FiatPluginEnterAmountScene.tsx +++ b/src/plugins/gui/scenes/FiatPluginEnterAmountScene.tsx @@ -3,6 +3,7 @@ import { useEffect } from 'react' import { Image, Text, TextStyle, View } from 'react-native' import { PoweredByCard } from '../../../components/cards/PoweredByCard' +import { EdgeAnim, fadeInDown30, fadeInDown60, fadeInDown90, fadeInUp30, fadeInUp60, fadeInUp90 } from '../../../components/common/EdgeAnim' import { SceneWrapper } from '../../../components/common/SceneWrapper' import { showError } from '../../../components/services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../../../components/services/ThemeContext' @@ -27,6 +28,8 @@ export interface FiatPluginEnterAmountParams { onPoweredByClick: (stateManager: StateManager) => Promise onSubmit: (event: { response: FiatPluginEnterAmountResponse }, stateManager: StateManager) => Promise headerIconUri?: string + swapInputLocations?: boolean + disableInput?: 1 | 2 } export interface EnterAmountState { @@ -62,7 +65,22 @@ export const FiatPluginEnterAmountScene = React.memo((props: Props) => { const theme = useTheme() const styles = getStyles(theme) const { route } = props - const { initState, headerIconUri, headerTitle, onSubmit, convertValue, onPoweredByClick, onChangeText = () => {}, label1, label2 } = route.params + const { + disableInput, + initState, + headerIconUri, + headerTitle, + onSubmit, + convertValue, + onPoweredByClick, + onChangeText = () => {}, + label1, + label2, + swapInputLocations = false + } = route.params + if (disableInput != null && (disableInput < 1 || disableInput > 2)) { + throw new Error('disableInput must be 1 or 2') + } const lastUsed = React.useRef(1) const stateManager = useStateManager({ ...defaultEnterAmountState, ...initState }) @@ -129,48 +147,110 @@ export const FiatPluginEnterAmountScene = React.memo((props: Props) => { } const poweredByIconPath = poweredBy != null ? getPartnerIconUri(poweredBy.poweredByIcon) : undefined + return ( - - {headerIcon} - + + + {headerIcon} + + - - - - - {statusText != null ? {statusText.content} : null} - {poweredBy != null ? : null} - + {swapInputLocations ? ( + + + + + + + + + ) : ( + + + + + + + + + )} + <> + + {statusText.content} + + + + + + + + @@ -181,6 +261,7 @@ const getStyles = cacheStyles((theme: Theme) => { const textCommon: TextStyle = { fontFamily: theme.fontFaceMedium, fontSize: theme.rem(1), + textAlign: 'center', includeFontPadding: false } return { diff --git a/src/plugins/gui/scenes/InfoDisplayScene.tsx b/src/plugins/gui/scenes/InfoDisplayScene.tsx index cc62df14d52..da89bdb0cdb 100644 --- a/src/plugins/gui/scenes/InfoDisplayScene.tsx +++ b/src/plugins/gui/scenes/InfoDisplayScene.tsx @@ -128,7 +128,7 @@ export const InfoDisplayScene = React.memo((props: Props) => { {promptMessage} {renderGroups()} - + ) }) diff --git a/src/plugins/gui/util/initializeProviders.ts b/src/plugins/gui/util/initializeProviders.ts index d77c5490b0a..a49c8b5e0c5 100644 --- a/src/plugins/gui/util/initializeProviders.ts +++ b/src/plugins/gui/util/initializeProviders.ts @@ -1,4 +1,5 @@ import { ENV } from '../../../env' +import { getTokenId } from '../../../util/CurrencyInfoHelpers' import { FiatPluginFactoryArgs } from '../fiatPluginTypes' import { FiatProvider, FiatProviderFactory } from '../fiatProviderTypes' import { createStore } from '../pluginUtils' @@ -11,6 +12,8 @@ export async function initializeProviders(providerFactories: Array>> = [] + const getTokenIdProvider = (pluginId: string, currencyCode: string) => getTokenId(account, pluginId, currencyCode) + for (const providerFactory of providerFactories) { if (disablePlugins[providerFactory.providerId]) continue @@ -18,7 +21,7 @@ export async function initializeProviders(providerFactories: Array { + const index = tokens.findIndex(token => token.tokenId === providerToken.tokenId) + if (index === -1) { + tokens.push(providerToken) + } else { + tokens[index] = providerToken + } +} diff --git a/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts b/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts index d24c980ae12..a4c03a02429 100644 --- a/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts +++ b/src/plugins/stake-plugins/thorchainSavers/tcSaversPlugin.ts @@ -1,6 +1,6 @@ import { add, div, eq, gt, lt, max, mul, sub, toFixed } from 'biggystring' import { asArray, asBoolean, asEither, asNumber, asObject, asOptional, asString } from 'cleaners' -import { EdgeAccount, EdgeCurrencyWallet, EdgeMemo, EdgeSpendInfo, EdgeTransaction, InsufficientFundsError } from 'edge-core-js' +import { asMaybeInsufficientFundsError, EdgeAccount, EdgeCurrencyWallet, EdgeMemo, EdgeSpendInfo, EdgeTransaction, InsufficientFundsError } from 'edge-core-js' import { asMaybeContractLocation } from '../../../components/scenes/EditTokenScene' import { StringMap } from '../../../types/types' @@ -589,7 +589,7 @@ const stakeRequest = async (opts: EdgeGuiPluginOptions, request: ChangeQuoteRequ const estimateTx = await wallet.makeSpend(spendInfo) networkFee = estimateTx.parentNetworkFee ?? estimateTx.networkFee } catch (e: unknown) { - if (e instanceof InsufficientFundsError && !isToken) { + if (asMaybeInsufficientFundsError(e) != null && !isToken) { needsFundingPrimary = true } else { throw e @@ -919,8 +919,10 @@ const unstakeRequestInner = async (opts: EdgeGuiPluginOptions, request: ChangeQu const estimateTx = await wallet.makeSpend(spendInfo) networkFee = estimateTx.networkFee } catch (e: unknown) { - if (e instanceof InsufficientFundsError) { + if (asMaybeInsufficientFundsError(e) != null) { needsFundingPrimary = true + } else { + throw e } } } diff --git a/src/selectors/WalletSelectors.ts b/src/selectors/WalletSelectors.ts index 9693e8624c0..e8ed744de95 100644 --- a/src/selectors/WalletSelectors.ts +++ b/src/selectors/WalletSelectors.ts @@ -4,7 +4,7 @@ import { EdgeCurrencyInfo, EdgeCurrencyWallet, EdgeDenomination } from 'edge-cor import { RootState, ThunkAction } from '../types/reduxTypes' import { getWalletTokenId } from '../util/CurrencyInfoHelpers' import { getWalletFiat } from '../util/CurrencyWalletHelpers' -import { convertNativeToExchange, zeroString } from '../util/utils' +import { convertCurrencyFromExchangeRates, convertNativeToExchange, zeroString } from '../util/utils' export function getSelectedCurrencyWallet(state: RootState): EdgeCurrencyWallet { return state.core.account.currencyWallets[state.ui.wallets.selectedWalletId] @@ -44,18 +44,6 @@ export function convertCurrencyFromState(fromCurrencyCode: string, toCurrencyCod } } -export const convertCurrencyFromExchangeRates = ( - exchangeRates: { [pair: string]: string }, - fromCurrencyCode: string, - toCurrencyCode: string, - amount: string -): string => { - const rateKey = `${fromCurrencyCode}_${toCurrencyCode}` - const rate = exchangeRates[rateKey] ?? '0' - const convertedAmount = mul(amount, rate) - return convertedAmount -} - export const calculateFiatBalance = (wallet: EdgeCurrencyWallet, exchangeDenomination: EdgeDenomination, exchangeRates: { [pair: string]: string }): string => { const currencyCode = exchangeDenomination.name const tokenId = getWalletTokenId(wallet, currencyCode) diff --git a/src/selectors/getCreateWalletList.ts b/src/selectors/getCreateWalletList.ts new file mode 100644 index 00000000000..4b39cb1e4dc --- /dev/null +++ b/src/selectors/getCreateWalletList.ts @@ -0,0 +1,184 @@ +import { EdgeAccount, EdgeTokenId, JsonObject } from 'edge-core-js' + +import { SPECIAL_CURRENCY_INFO, WALLET_TYPE_ORDER } from '../constants/WalletAndCurrencyConstants' +import { EdgeAsset, WalletListItem } from '../types/types' +import { checkAssetFilter, hasAsset, isKeysOnlyPlugin } from '../util/CurrencyInfoHelpers' +import { assetOverrides } from '../util/serverState' +import { normalizeForSearch } from '../util/utils' + +export interface WalletCreateItem { + key: string + currencyCode: string + displayName: string + pluginId: string + + // Used for creating tokens: + tokenId: EdgeTokenId + createWalletIds?: string[] + + // Used for creating wallets: + keyOptions?: JsonObject + walletType?: string +} + +export interface MainWalletCreateItem extends WalletCreateItem { + keyOptions: JsonObject + walletType: string +} + +export interface TokenWalletCreateItem extends WalletCreateItem { + tokenId: string + createWalletIds: string[] +} + +export const splitCreateWalletItems = ( + createItems: WalletCreateItem[] +): { + newWalletItems: MainWalletCreateItem[] + newTokenItems: TokenWalletCreateItem[] +} => { + const newWalletItems: MainWalletCreateItem[] = [] + const newTokenItems: TokenWalletCreateItem[] = [] + createItems.forEach(item => { + if (item.walletType != null) { + newWalletItems.push(item as MainWalletCreateItem) + } else if (item.tokenId != null) { + if (item.createWalletIds == null) item.createWalletIds = [] + newTokenItems.push(item as TokenWalletCreateItem) + } + }) + return { newWalletItems, newTokenItems } +} + +interface CreateWalletListOpts { + filteredWalletList?: WalletListItem[] + filterActivation?: boolean + allowedAssets?: EdgeAsset[] + excludeAssets?: EdgeAsset[] +} + +export const getCreateWalletList = (account: EdgeAccount, opts: CreateWalletListOpts = {}): WalletCreateItem[] => { + const { filteredWalletList = [], filterActivation, allowedAssets, excludeAssets } = opts + + // Add top-level wallet types: + const newWallets: MainWalletCreateItem[] = [] + for (const pluginId of Object.keys(account.currencyConfig)) { + const currencyConfig = account.currencyConfig[pluginId] + const { currencyCode, displayName, walletType } = currencyConfig.currencyInfo + + // Prevent plugins that are "watch only" from being allowed to create new wallets + if (isKeysOnlyPlugin(pluginId)) continue + + // Prevent currencies that needs activation from being created from a modal + if (filterActivation && requiresActivation(pluginId)) continue + + if (['bitcoin', 'litecoin', 'digibyte'].includes(pluginId)) { + newWallets.push({ + key: `create-${walletType}-bip49-${pluginId}`, + currencyCode, + displayName: `${displayName} (Segwit)`, + keyOptions: { format: 'bip49' }, + pluginId, + tokenId: null, + walletType + }) + newWallets.push({ + key: `create-${walletType}-bip44-${pluginId}`, + currencyCode, + displayName: `${displayName} (no Segwit)`, + keyOptions: { format: 'bip44' }, + pluginId, + tokenId: null, + walletType + }) + } else { + newWallets.push({ + key: `create-${walletType}-${pluginId}`, + currencyCode, + displayName, + keyOptions: {}, + pluginId, + tokenId: null, + walletType + }) + } + } + + // Sort what we have so far: + const walletList: WalletCreateItem[] = newWallets.sort((a, b) => { + // Use the table first: + const aIndex = walletOrderTable[a.walletType] + const bIndex = walletOrderTable[b.walletType] + if (aIndex != null && bIndex != null) return aIndex - bIndex + if (aIndex != null) return -1 + if (bIndex != null) return 1 + + // Otherwise, sort display names alphabetically: + return a.displayName.localeCompare(b.displayName) + }) + + // Add token types: + for (const pluginId of Object.keys(account.currencyConfig)) { + const currencyConfig = account.currencyConfig[pluginId] + const { builtinTokens, currencyInfo } = currencyConfig + + // Identify which wallets could add the token + const createWalletIds = Object.keys(account.currencyWallets).filter(walletId => account.currencyWallets[walletId].currencyInfo.pluginId === pluginId) + + for (const tokenId of Object.keys(builtinTokens)) { + const { currencyCode, displayName } = builtinTokens[tokenId] + + // Fix for when the token code and chain code are the same (like EOS/TLOS) + if (currencyCode === currencyInfo.currencyCode) continue + + const item: TokenWalletCreateItem = { + key: `create-${currencyInfo.pluginId}-${tokenId}`, + currencyCode, + displayName, + pluginId, + tokenId, + createWalletIds + } + walletList.push(item) + } + } + + // Filter this list: + const existingWallets: EdgeAsset[] = [] + for (const { wallet, tokenId } of filteredWalletList) { + if (wallet == null) continue + existingWallets.push({ + pluginId: wallet.currencyInfo.pluginId, + tokenId + }) + } + const out = walletList.filter(item => !hasAsset(existingWallets, item) && checkAssetFilter(item, allowedAssets, excludeAssets)) + return out.filter(item => !assetOverrides.disable[item.pluginId]) +} + +export const filterWalletCreateItemListBySearchText = (createWalletList: WalletCreateItem[], searchText: string): WalletCreateItem[] => { + const out: WalletCreateItem[] = [] + const searchTarget = normalizeForSearch(searchText) + for (const item of createWalletList) { + const { currencyCode, displayName, pluginId, walletType } = item + if (normalizeForSearch(currencyCode).includes(searchTarget) || normalizeForSearch(displayName).includes(searchTarget)) { + out.push(item) + continue + } + // Do an additional search for pluginId for mainnet create items + if (walletType != null && normalizeForSearch(pluginId).includes(searchTarget)) { + out.push(item) + } + } + return out +} + +function requiresActivation(pluginId: string) { + const { isAccountActivationRequired = false } = SPECIAL_CURRENCY_INFO[pluginId] ?? {} + return isAccountActivationRequired +} + +const walletOrderTable: { [walletType: string]: number } = {} +for (let i = 0; i < WALLET_TYPE_ORDER.length; ++i) { + walletOrderTable[WALLET_TYPE_ORDER[i]] = i +} diff --git a/src/theme/edgeConfig.ts b/src/theme/edgeConfig.ts index 6bd7cb4fc5a..9ed7966bb43 100644 --- a/src/theme/edgeConfig.ts +++ b/src/theme/edgeConfig.ts @@ -10,7 +10,15 @@ export const edgeConfig: AppConfig = { backupAccountSite: 'https://edge.app/light-account-creation/', configName: 'edge', darkTheme: edgeDark, - defaultWallets: ['BTC', 'ETH', 'LTC', 'BCH', 'DASH'], + defaultWallets: [ + { pluginId: 'bitcoin', tokenId: null }, + { pluginId: 'ethereum', tokenId: null }, + { pluginId: 'litecoin', tokenId: null }, + { pluginId: 'bitcoincash', tokenId: null }, + { pluginId: 'dash', tokenId: null } + ], + forceCloseUrlIos: 'https://support.apple.com/en-us/HT201330 ', + forceCloseUrlAndroid: 'https://support.google.com/android/answer/9079646?hl=en', knowledgeBase: 'https://help.edge.app/support/home', lightTheme: edgeLight, notificationServers: ['https://notif1.edge.app'], diff --git a/src/theme/testConfig.ts b/src/theme/testConfig.ts index 41c1559a9f1..5aab219f88a 100644 --- a/src/theme/testConfig.ts +++ b/src/theme/testConfig.ts @@ -10,7 +10,13 @@ export const testConfig: AppConfig = { backupAccountSite: 'https://support.testy.com/accountbackupinfo', configName: 'test', darkTheme: testDark, - defaultWallets: ['BTC', 'FTM:TOMB', 'ETH:USDC'], + defaultWallets: [ + { pluginId: 'bitcoin', tokenId: null }, + { pluginId: 'fantom', tokenId: '6c021ae822bea943b2e66552bde1d2696a53fbb7' }, + { pluginId: 'ethereum', tokenId: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' } + ], + forceCloseUrlIos: 'https://support.apple.com/en-us/HT201330 ', + forceCloseUrlAndroid: 'https://support.google.com/android/answer/9079646?hl=en', knowledgeBase: 'https://support.testy.com/knowledge', lightTheme: testLight, notificationServers: ['https://notif1.edge.app'], diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx index c23826be2e0..0be337ccfc4 100644 --- a/src/types/routerTypes.tsx +++ b/src/types/routerTypes.tsx @@ -1,54 +1,69 @@ import * as NavigationCore from '@react-navigation/core' -import { StackActionHelpers } from '@react-navigation/native' -import { EdgeCurrencyInfo, EdgeCurrencyWallet, EdgeSpendInfo, EdgeTokenId, JsonObject, OtpError } from 'edge-core-js' -import { InitialRouteName } from 'edge-login-ui-rn' +import type { StackActionHelpers } from '@react-navigation/native' +import type { EdgeCurrencyWallet, EdgeTokenId } from 'edge-core-js' -import { CoinRankingDetailsParams } from '../components/scenes/CoinRankingDetailsScene' -import { ConfirmSceneParams } from '../components/scenes/ConfirmScene' -import { CreateWalletCompletionParams } from '../components/scenes/CreateWalletCompletionScene' -import { CreateWalletImportOptionsParams } from '../components/scenes/CreateWalletImportOptionsScene' -import { CreateWalletImportParams } from '../components/scenes/CreateWalletImportScene' -import { CreateWalletSelectCryptoParams } from '../components/scenes/CreateWalletSelectCryptoScene' -import { CreateWalletSelectFiatParams } from '../components/scenes/CreateWalletSelectFiatScene' -import { ExchangeQuoteProcessingParams } from '../components/scenes/CryptoExchangeQuoteProcessingScene' -import { CryptoExchangeQuoteParams } from '../components/scenes/CryptoExchangeQuoteScene' -import { FioCreateHandleParams } from '../components/scenes/Fio/FioCreateHandleScene' -import { PluginViewParams } from '../components/scenes/GuiPluginViewScene' -import { LoanManageType } from '../components/scenes/Loans/LoanManageScene' -import { MigrateWalletItem } from '../components/scenes/MigrateWalletSelectCryptoScene' -import { SendScene2Params } from '../components/scenes/SendScene2' -import { StakeOptionsParams } from '../components/scenes/Staking/StakeOptionsScene' -import { StakeOverviewParams } from '../components/scenes/Staking/StakeOverviewScene' -import { TransactionDetailsParams } from '../components/scenes/TransactionDetailsScene' -import { TransactionListParams } from '../components/scenes/TransactionListScene' -import { WcConnectionsParams } from '../components/scenes/WcConnectionsScene' -import { WcConnectParams } from '../components/scenes/WcConnectScene' -import { WcDisconnectParams } from '../components/scenes/WcDisconnectScene' -import { WebViewSceneParams } from '../components/scenes/WebViewScene' -import { ExchangedFlipInputAmounts } from '../components/themed/ExchangedFlipInput2' -import { PaymentMethod } from '../controllers/action-queue/PaymentMethod' -import { BorrowEngine, BorrowPlugin } from '../plugins/borrow-plugins/types' -import { FiatPluginAddressFormParams, FiatPluginSepaFormParams, FiatPluginSepaTransferParams } from '../plugins/gui/fiatPluginTypes' -import { FiatPluginEnterAmountParams } from '../plugins/gui/scenes/FiatPluginEnterAmountScene' -import { FiatPluginOpenWebViewParams } from '../plugins/gui/scenes/FiatPluginWebView' -import { RewardsCardDashboardParams } from '../plugins/gui/scenes/RewardsCardDashboardScene' -import { RewardsCardWelcomeParams } from '../plugins/gui/scenes/RewardsCardWelcomeScene' -import { ChangeQuoteRequest, StakePlugin, StakePolicy, StakePosition } from '../plugins/stake-plugins/types' -import { CreateWalletType, FeeOption, FioConnectionWalletItem, FioDomain, FioRequest } from './types' +import type { ChangeMiningFeeParams } from '../components/scenes/ChangeMiningFeeScene' +import type { CoinRankingDetailsParams } from '../components/scenes/CoinRankingDetailsScene' +import type { ConfirmSceneParams } from '../components/scenes/ConfirmScene' +import type { CreateWalletAccountSelectParams } from '../components/scenes/CreateWalletAccountSelectScene' +import type { CreateWalletAccountSetupParams } from '../components/scenes/CreateWalletAccountSetupScene' +import type { CreateWalletCompletionParams } from '../components/scenes/CreateWalletCompletionScene' +import type { CreateWalletImportOptionsParams } from '../components/scenes/CreateWalletImportOptionsScene' +import type { CreateWalletImportParams } from '../components/scenes/CreateWalletImportScene' +import type { CreateWalletSelectCryptoParams } from '../components/scenes/CreateWalletSelectCryptoScene' +import type { CreateWalletSelectFiatParams } from '../components/scenes/CreateWalletSelectFiatScene' +import type { ExchangeQuoteProcessingParams } from '../components/scenes/CryptoExchangeQuoteProcessingScene' +import type { CryptoExchangeQuoteParams } from '../components/scenes/CryptoExchangeQuoteScene' +import type { CurrencyNotificationParams } from '../components/scenes/CurrencyNotificationScene' +import type { CurrencySettingsParams } from '../components/scenes/CurrencySettingsScene' +import type { EdgeLoginParams } from '../components/scenes/EdgeLoginScene' +import type { EditTokenParams } from '../components/scenes/EditTokenScene' +import type { FioCreateHandleParams } from '../components/scenes/Fio/FioCreateHandleScene' +import type { GettingStartedParams } from '../components/scenes/GettingStartedScene' +import type { GuiPluginListParams } from '../components/scenes/GuiPluginListScene' +import type { PluginViewParams } from '../components/scenes/GuiPluginViewScene' +import type { LoanCloseParams } from '../components/scenes/Loans/LoanCloseScene' +import type { LoanCreateConfirmationParams } from '../components/scenes/Loans/LoanCreateConfirmationScene' +import type { LoanCreateParams } from '../components/scenes/Loans/LoanCreateScene' +import type { LoanDetailsParams } from '../components/scenes/Loans/LoanDetailsScene' +import type { LoanManageParams } from '../components/scenes/Loans/LoanManageScene' +import type { LoanStatusParams } from '../components/scenes/Loans/LoanStatusScene' +import type { LoginParams } from '../components/scenes/LoginScene' +import type { ManageTokensParams } from '../components/scenes/ManageTokensScene' +import type { MigrateWalletCalculateFeeParams } from '../components/scenes/MigrateWalletCalculateFeeScene' +import type { MigrateWalletCompletionParams } from '../components/scenes/MigrateWalletCompletionScene' +import type { MigrateWalletSelectCryptoParams } from '../components/scenes/MigrateWalletSelectCryptoScene' +import type { OtpRepairParams } from '../components/scenes/OtpRepairScene' +import type { RequestParams } from '../components/scenes/RequestScene' +import type { SendScene2Params } from '../components/scenes/SendScene2' +import type { StakeModifyParams } from '../components/scenes/Staking/StakeModifyScene' +import type { StakeOptionsParams } from '../components/scenes/Staking/StakeOptionsScene' +import type { StakeOverviewParams } from '../components/scenes/Staking/StakeOverviewScene' +import type { TransactionDetailsParams } from '../components/scenes/TransactionDetailsScene' +import type { TransactionListParams } from '../components/scenes/TransactionListScene' +import type { TransactionsExportParams } from '../components/scenes/TransactionsExportScene' +import type { WcConnectionsParams } from '../components/scenes/WcConnectionsScene' +import type { WcConnectParams } from '../components/scenes/WcConnectScene' +import type { WcDisconnectParams } from '../components/scenes/WcDisconnectScene' +import type { WebViewSceneParams } from '../components/scenes/WebViewScene' +import type { ExchangedFlipInputAmounts } from '../components/themed/ExchangedFlipInput2' +import type { FiatPluginAddressFormParams, FiatPluginSepaFormParams, FiatPluginSepaTransferParams } from '../plugins/gui/fiatPluginTypes' +import type { FiatPluginEnterAmountParams } from '../plugins/gui/scenes/FiatPluginEnterAmountScene' +import type { FiatPluginOpenWebViewParams } from '../plugins/gui/scenes/FiatPluginWebView' +import type { RewardsCardDashboardParams } from '../plugins/gui/scenes/RewardsCardDashboardScene' +import type { RewardsCardWelcomeParams } from '../plugins/gui/scenes/RewardsCardWelcomeScene' +import type { FioConnectionWalletItem, FioDomain, FioRequest } from './types' /** * Defines the acceptable route parameters for each scene key. */ export interface RouteParamList { // Top-level router: - login: { - loginUiInitialRoute?: InitialRouteName - } + login: LoginParams edgeApp: {} edgeAppStack: {} edgeTabs: {} - controlPanel: {} - gettingStarted: {} + gettingStarted: GettingStartedParams // Tabs homeTab: {} @@ -70,52 +85,25 @@ export interface RouteParamList { // Logged-in scenes: assetSettings: {} - changeMiningFee2: { - spendInfo: EdgeSpendInfo - maxSpendSet: boolean - onSubmit: (networkFeeOption: FeeOption, customNetworkFee: JsonObject) => void - wallet: EdgeCurrencyWallet - } + changeMiningFee2: ChangeMiningFeeParams changePassword: {} changePin: {} coinRanking: {} coinRankingDetails: CoinRankingDetailsParams confirmScene: ConfirmSceneParams - createWalletAccountSelect: { - accountName: string - existingWalletId: string - selectedWalletType: CreateWalletType - } - createWalletAccountSetup: { - accountHandle?: string - existingWalletId: string - isReactivation?: boolean - selectedWalletType: CreateWalletType - } + createWalletAccountSelect: CreateWalletAccountSelectParams + createWalletAccountSetup: CreateWalletAccountSetupParams createWalletCompletion: CreateWalletCompletionParams createWalletImport: CreateWalletImportParams createWalletImportOptions: CreateWalletImportOptionsParams createWalletSelectCrypto: CreateWalletSelectCryptoParams createWalletSelectCryptoNewAccount: CreateWalletSelectCryptoParams createWalletSelectFiat: CreateWalletSelectFiatParams - currencyNotificationSettings: { - currencyInfo: EdgeCurrencyInfo - } - currencySettings: { - currencyInfo: EdgeCurrencyInfo - } + currencyNotificationSettings: CurrencyNotificationParams + currencySettings: CurrencySettingsParams defaultFiatSetting: {} - edgeLogin: { - lobbyId: string - } - editToken: { - currencyCode?: string - displayName?: string - multiplier?: string - networkLocation?: JsonObject - tokenId?: EdgeTokenId // Acts like "add token" if this is missing - walletId: string - } + edgeLogin: EdgeLoginParams + editToken: EditTokenParams exchange: {} exchangeQuote: CryptoExchangeQuoteParams exchangeQuoteProcessing: ExchangeQuoteProcessingParams @@ -194,90 +182,40 @@ export interface RouteParamList { } home: {} loanDashboard: {} - loanDetails: { - loanAccountId: string - } - loanCreate: { - borrowEngine: BorrowEngine - borrowPlugin: BorrowPlugin - } - loanCreateConfirmation: { - borrowEngine: BorrowEngine - borrowPlugin: BorrowPlugin - destTokenId: EdgeTokenId - destWallet: EdgeCurrencyWallet - nativeDestAmount: string - nativeSrcAmount: string - paymentMethod?: PaymentMethod - srcTokenId: EdgeTokenId - srcWallet: EdgeCurrencyWallet - } - loanClose: { - loanAccountId: string - } - loanManage: { - loanManageType: LoanManageType - loanAccountId: string - } - loanStatus: { - actionQueueId: string - loanAccountId: string - } - manageTokens: { - walletId: string - } - migrateWalletCompletion: { - migrateWalletList: MigrateWalletItem[] - } - migrateWalletCalculateFee: { - migrateWalletList: MigrateWalletItem[] - } - migrateWalletSelectCrypto: { - preSelectedWalletIds?: string[] - } + loanDetails: LoanDetailsParams + loanCreate: LoanCreateParams + loanCreateConfirmation: LoanCreateConfirmationParams + loanClose: LoanCloseParams + loanManage: LoanManageParams + loanStatus: LoanStatusParams + manageTokens: ManageTokensParams + migrateWalletCompletion: MigrateWalletCompletionParams + migrateWalletCalculateFee: MigrateWalletCalculateFeeParams + migrateWalletSelectCrypto: MigrateWalletSelectCryptoParams notificationSettings: {} - otpRepair: { - otpError: OtpError - } + otpRepair: OtpRepairParams otpSetup: {} passwordRecovery: {} upgradeUsername: {} - pluginListBuy: { - launchPluginId?: string - } - pluginListSell: { - launchPluginId?: string - } + pluginListBuy: GuiPluginListParams + pluginListSell: GuiPluginListParams pluginViewBuy: PluginViewParams pluginViewSell: PluginViewParams pluginView: PluginViewParams promotionSettings: {} - request: {} - scan: { - data?: 'sweepPrivateKey' | 'loginQR' - } // TODO + request: RequestParams securityAlerts: {} send2: SendScene2Params settingsOverview: {} settingsOverviewTab: {} spendingLimits: {} - stakeModify: { - title: string - stakePlugin: StakePlugin - walletId: string - stakePolicy: StakePolicy - stakePosition: StakePosition - modification: ChangeQuoteRequest['action'] - } + stakeModify: StakeModifyParams stakeOptions: StakeOptionsParams stakeOverview: StakeOverviewParams testScene: {} transactionDetails: TransactionDetailsParams transactionList: TransactionListParams - transactionsExport: { - sourceWallet: EdgeCurrencyWallet - currencyCode: string - } + transactionsExport: TransactionsExportParams walletList: {} webView: WebViewSceneParams wcConnections: WcConnectionsParams diff --git a/src/types/types.ts b/src/types/types.ts index 2c99f1b72b7..d99e8459a0f 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -75,13 +75,6 @@ export interface ExchangeData { secondaryCurrencyCode: string } -export interface CreateWalletType { - currencyName: string - walletType: string - pluginId: string - currencyCode: string -} - export interface CustomNodeSetting { isEnabled: boolean nodesList: string[] @@ -167,6 +160,7 @@ const asLocalAccountSettingsInner = asObject({ spendingLimits: asMaybe(asSpendingLimits, () => asSpendingLimits({})) }) const asDeviceSettingsInner = asObject({ + developerPluginUri: asMaybe(asString), disableAnimations: asMaybe(asBoolean, false), hasInteractedWithBackupModal: asMaybe(asBoolean, false) }) @@ -276,7 +270,7 @@ export interface WcConnectionInfo { icon: string } export interface WalletConnectChainId { - namespace: 'algorand' | 'eip155' + namespace: 'algorand' | 'cosmos' | 'eip155' reference: string } export interface wcGetConnection { @@ -301,7 +295,9 @@ export interface AppConfig { backupAccountSite: string configName: string darkTheme: Theme - defaultWallets: string[] + defaultWallets: EdgeAsset[] + forceCloseUrlIos: string + forceCloseUrlAndroid: string knowledgeBase: string lightTheme: Theme notificationServers: string[] diff --git a/src/util/ActionProgramUtils.ts b/src/util/ActionProgramUtils.ts index cc7b90ff643..cfb828a5395 100644 --- a/src/util/ActionProgramUtils.ts +++ b/src/util/ActionProgramUtils.ts @@ -7,12 +7,19 @@ import { makeActionProgram } from '../controllers/action-queue/ActionProgram' import { ActionOp, ActionProgram, ParActionOp, SeqActionOp } from '../controllers/action-queue/types' import { lstrings } from '../locales/strings' import { BorrowCollateral, BorrowDebt, BorrowEngine } from '../plugins/borrow-plugins/types' -import { convertCurrencyFromExchangeRates } from '../selectors/WalletSelectors' import { config } from '../theme/appConfig' import { GuiExchangeRates } from '../types/types' import { getToken } from './CurrencyInfoHelpers' import { enableToken } from './CurrencyWalletHelpers' -import { convertNativeToExchange, DECIMAL_PRECISION, getDenomFromIsoCode, maxPrimaryCurrencyConversionDecimals, precisionAdjust, zeroString } from './utils' +import { + convertCurrencyFromExchangeRates, + convertNativeToExchange, + DECIMAL_PRECISION, + getDenomFromIsoCode, + maxPrimaryCurrencyConversionDecimals, + precisionAdjust, + zeroString +} from './utils' const MINIMUM_PARASWAP_AMOUNT = '100000' // Don't attempt paraswaps under 1 cent const LIQUIDATION_THRESHOLD = '0.74' diff --git a/src/util/CurrencyInfoHelpers.ts b/src/util/CurrencyInfoHelpers.ts index aec1ce95fc9..07a165217a6 100644 --- a/src/util/CurrencyInfoHelpers.ts +++ b/src/util/CurrencyInfoHelpers.ts @@ -1,9 +1,9 @@ import { EdgeAccount, EdgeCurrencyInfo, EdgeCurrencyWallet, EdgeToken, EdgeTokenId } from 'edge-core-js' import { showError } from '../components/services/AirshipInstance' -import { SPECIAL_CURRENCY_INFO, WALLET_TYPE_ORDER } from '../constants/WalletAndCurrencyConstants' +import { SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' import { ENV } from '../env' -import { CreateWalletType } from '../types/types' +import { EdgeAsset } from '../types/types' /** * Returns true if this currency supports existing wallets, @@ -14,11 +14,6 @@ export function isKeysOnlyPlugin(pluginId: string): boolean { return keysOnlyMode || ENV.KEYS_ONLY_PLUGINS[pluginId] } -function requiresActivation(pluginId: string) { - const { isAccountActivationRequired = false } = SPECIAL_CURRENCY_INFO[pluginId] ?? {} - return isAccountActivationRequired -} - /** * Grab all the EdgeCurrencyInfo objects in an account. */ @@ -27,87 +22,6 @@ export function getCurrencyInfos(account: EdgeAccount): EdgeCurrencyInfo[] { return Object.keys(currencyConfig).map(pluginId => currencyConfig[pluginId].currencyInfo) } -const walletOrderTable: { [walletType: string]: number } = {} -for (let i = 0; i < WALLET_TYPE_ORDER.length; ++i) { - walletOrderTable[WALLET_TYPE_ORDER[i]] = i -} - -/** - * Sort an array of EdgeCurrencyInfo objects for display to the user. - */ -export function sortCurrencyInfos(infos: EdgeCurrencyInfo[]): EdgeCurrencyInfo[] { - return infos.sort((a, b) => { - // Use the table first: - const aIndex = walletOrderTable[a.walletType] - const bIndex = walletOrderTable[b.walletType] - if (aIndex != null && bIndex != null) return aIndex - bIndex - if (aIndex != null) return -1 - if (bIndex != null) return 1 - - // Otherwise, sort display names alphabetically: - return a.displayName.localeCompare(b.displayName) - }) -} - -/** - * The wallet creation scenes use a truncated version of EdgeCurrencyInfo, - * so make that. - */ -export function makeCreateWalletType(currencyInfo: EdgeCurrencyInfo): CreateWalletType { - const { currencyCode, walletType, displayName: currencyName, pluginId } = currencyInfo - return { - currencyName, - walletType, - pluginId, - currencyCode - } -} - -/** - * Grab a list of wallet types for the wallet creation scenes. - */ -export function getCreateWalletTypes(account: EdgeAccount, filterActivation: boolean = false): CreateWalletType[] { - const infos = sortCurrencyInfos(getCurrencyInfos(account)) - - const out: CreateWalletType[] = [] - for (const currencyInfo of infos) { - const { currencyCode, displayName, pluginId, walletType } = currencyInfo - // Prevent plugins that are "watch only" from being allowed to create new wallets - if (isKeysOnlyPlugin(pluginId)) continue - // Prevent currencies that needs activation from being created from a modal - if (filterActivation && requiresActivation(pluginId)) continue - // FIO disable changes - if (['bitcoin', 'litecoin', 'digibyte'].includes(pluginId)) { - out.push({ - currencyName: `${displayName} (Segwit)`, - walletType: `${walletType}-bip49`, - pluginId, - currencyCode - }) - out.push({ - currencyName: `${displayName} (no Segwit)`, - walletType: `${walletType}-bip44`, - pluginId, - currencyCode - }) - } else { - out.push(makeCreateWalletType(currencyInfo)) - } - } - - return out -} - -/** - * Get specific wallet for the wallet creation scenes. BTC will always result in segwit - */ -export function getCreateWalletType(account: EdgeAccount, currencyCode: string): CreateWalletType | null { - const infos = getCurrencyInfos(account) - const currencyCodeFormatted = currencyCode.toUpperCase() - const currencyInfo = infos.find(info => info.currencyCode === currencyCodeFormatted) - return currencyInfo ? makeCreateWalletType(currencyInfo) : null -} - export const getTokenId = (account: EdgeAccount, pluginId: string, currencyCode: string): EdgeTokenId | undefined => { const currencyConfig = account.currencyConfig[pluginId] if (currencyConfig == null) return @@ -145,7 +59,10 @@ export const getCurrencyCode = (wallet: EdgeCurrencyWallet, tokenId: EdgeTokenId /** * Get the currencyCode associated with a tokenId */ -export const getCurrencyCodeWithAccount = (account: EdgeAccount, pluginId: string, tokenId: EdgeTokenId): string => { +export const getCurrencyCodeWithAccount = (account: EdgeAccount, pluginId: string, tokenId: EdgeTokenId): string | undefined => { + if (account.currencyConfig[pluginId] == null) { + return + } const { currencyCode } = tokenId != null ? account.currencyConfig[pluginId].allTokens[tokenId] : account.currencyConfig[pluginId].currencyInfo return currencyCode } @@ -162,3 +79,61 @@ export const getToken = (wallet: EdgeCurrencyWallet, tokenId: EdgeTokenId): Edge return allTokens[tokenId] } } + +export function checkAssetFilter(details: EdgeAsset, allowedAssets?: EdgeAsset[], excludeAssets?: EdgeAsset[]): boolean { + if (allowedAssets != null && !hasAsset(allowedAssets, details)) { + return false + } + if (excludeAssets != null && hasAsset(excludeAssets, details)) { + return false + } + return true +} + +/** + * Returns true if the asset array includes the given asset. + */ +export function hasAsset(assets: EdgeAsset[], target: EdgeAsset): boolean { + for (const asset of assets) { + if (asset.pluginId === target.pluginId && asset.tokenId === target.tokenId) { + return true + } + } + return false +} + +/** + * The `currencyCodes` are in the format "ETH:DAI", + */ +export const currencyCodesToEdgeAssets = (account: EdgeAccount, currencyCodes: string[]): EdgeAsset[] => { + const chainCodePluginIdMap = Object.keys(account.currencyConfig).reduce( + (map: { [chainCode: string]: string }, pluginId) => { + const chainCode = account.currencyConfig[pluginId].currencyInfo.currencyCode + if (map[chainCode] == null) map[chainCode] = pluginId + return map + }, + { BNB: 'binancesmartchain', ETH: 'ethereum' } // HACK: Prefer BNB Smart Chain over Beacon Chain if provided a BNB currency code) and Ethereum over L2s + ) + + const edgeTokenIds: EdgeAsset[] = [] + + for (const code of currencyCodes) { + const [parent, child] = code.split(':') + const pluginId = chainCodePluginIdMap[parent] + const currencyConfig = account.currencyConfig[pluginId] + if (currencyConfig == null) continue + + // Add the mainnet EdgeAsset if we haven't yet + if (edgeTokenIds.find(edgeTokenId => edgeTokenId.tokenId == null && edgeTokenId.pluginId === pluginId) == null) { + edgeTokenIds.push({ pluginId, tokenId: null }) + } + + // Add tokens + if (child != null) { + const tokenId = Object.keys(currencyConfig.builtinTokens).find(tokenId => currencyConfig.builtinTokens[tokenId].currencyCode === child) + if (tokenId != null) edgeTokenIds.push({ pluginId, tokenId }) + } + } + + return edgeTokenIds +} diff --git a/src/util/CurrencyWalletHelpers.ts b/src/util/CurrencyWalletHelpers.ts index 87a3a32dcda..19a3a1ef84b 100644 --- a/src/util/CurrencyWalletHelpers.ts +++ b/src/util/CurrencyWalletHelpers.ts @@ -5,7 +5,6 @@ import { sprintf } from 'sprintf-js' import { showFullScreenSpinner } from '../components/modals/AirshipFullScreenSpinner' import { SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' import { lstrings } from '../locales/strings' -import { getWalletTokenId } from './CurrencyInfoHelpers' import { getFioStakingBalances } from './stakeUtils' /** @@ -36,14 +35,9 @@ export function cleanFiatCurrencyCode(fiatCurrencyCode: string): { fiatCurrencyC } } -export const getAvailableBalance = (wallet: EdgeCurrencyWallet, tokenCode?: string): string => { - const { currencyCode, pluginId } = wallet.currencyInfo - let tokenId: EdgeTokenId - if (tokenCode == null || tokenCode === currencyCode) { - tokenId = null - } else { - tokenId = getWalletTokenId(wallet, tokenCode) - } +export const getAvailableBalance = (wallet: EdgeCurrencyWallet, tokenId: EdgeTokenId): string => { + const { pluginId } = wallet.currencyInfo + let balance = wallet.balanceMap.get(tokenId) ?? '0' if (SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported && tokenId == null) { // Special case for FIO mainnet (no token) diff --git a/src/util/FioAddressUtils.ts b/src/util/FioAddressUtils.ts index 4be789c3d35..e26478997db 100644 --- a/src/util/FioAddressUtils.ts +++ b/src/util/FioAddressUtils.ts @@ -226,7 +226,7 @@ export const refreshConnectedWalletsForFioAddress = async ( }) for (const wallet of wallets) { const { currencyConfig, enabledTokenIds } = wallet - const enabledCodes = enabledTokenIds.map(tokenId => currencyConfig.allTokens[tokenId].currencyCode) + const enabledCodes = enabledTokenIds.map(tokenId => currencyConfig.allTokens[tokenId]?.currencyCode).filter(t => t != null) enabledCodes.push(wallet.currencyInfo.currencyCode) for (const currencyCode of enabledCodes) { const fullCurrencyCode = `${wallet.currencyInfo.currencyCode}:${currencyCode}` diff --git a/src/util/cleanerFetch2.ts b/src/util/cleanerFetch2.ts deleted file mode 100644 index d3990a21199..00000000000 --- a/src/util/cleanerFetch2.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Cleaner, uncleaner } from 'cleaners' - -interface FetchInit { - asReq?: Cleaner - asRes: Cleaner - method?: 'GET' | 'POST' -} -type FetchInitWithUri = { uri: string } & FetchInit -interface FetchInput { - uri?: string - req?: Req -} -type FetchInputWithUri = { uri: string } & FetchInput - -type FetcherWithUri = (input: FetchInputWithUri) => Promise -type Fetcher = (input?: FetchInput) => Promise - -export function cleanFetch(init: FetchInitWithUri): (input?: FetchInput) => Promise -export function cleanFetch(init: FetchInit): (input: FetchInputWithUri) => Promise -export function cleanFetch(init: FetchInit | FetchInitWithUri): FetcherWithUri | Fetcher { - const { asReq, asRes } = init - const wasReq = asReq ? uncleaner(asReq) : (r: unknown) => r - - const fetcher = async (input: FetchInputWithUri): Promise => { - const { uri, req } = input ?? {} - const request = wasReq(req) - const response = await fetch(uri, { - method: init.method, - body: request - }) - return asRes(await response.text()) - } - - if ('uri' in init) { - return async (input?: FetchInput): Promise => { - return await fetcher({ uri: init.uri, ...input }) - } - } - - return fetcher -} diff --git a/src/util/corePlugins.ts b/src/util/corePlugins.ts index fb7fa84968e..6973cf081eb 100644 --- a/src/util/corePlugins.ts +++ b/src/util/corePlugins.ts @@ -4,40 +4,44 @@ import { ENV } from '../env' export const currencyPlugins: EdgeCorePluginsInit = { // edge-currency-accountbased: + arbitrum: ENV.ARBITRUM_INIT, algorand: true, + avalanche: ENV.AVALANCHE_INIT, + axelar: true, + base: ENV.BASE_INIT, binance: true, binancesmartchain: ENV.BINANCE_SMART_CHAIN_INIT, - hedera: true, + celo: true, + coreum: ENV.COREUM_INIT, + cosmoshub: true, eos: true, - filecoin: true, - filecoinfevm: true, - filecoinfevmcalibration: true, - telos: true, - wax: true, - polkadot: true, - liberland: true, - liberlandtestnet: false, ethereum: ENV.ETHEREUM_INIT, ethereumclassic: true, ethereumpow: ENV.ETHEREUM_POW_INIT, fantom: ENV.FANTOM_INIT, + filecoin: true, + filecoinfevm: true, + filecoinfevmcalibration: true, fio: ENV.FIO_INIT, goerli: ENV.GOERLI_INIT, + hedera: true, kovan: ENV.KOVAN_INIT, + liberland: true, + liberlandtestnet: false, optimism: ENV.OPTIMISM_INIT, - pulsechain: ENV.PULSECHAIN_INIT, + osmosis: ENV.OSMOSIS_INIT, + polkadot: true, polygon: ENV.POLYGON_INIT, - avalanche: ENV.AVALANCHE_INIT, + pulsechain: ENV.PULSECHAIN_INIT, ripple: true, rsk: true, + solana: ENV.SOLANA_INIT, stellar: true, + telos: true, tezos: true, - solana: ENV.SOLANA_INIT, - celo: true, - coreum: ENV.COREUM_INIT, - osmosis: ENV.OSMOSIS_INIT, thorchainrune: ENV.THORCHAIN_INIT, tron: true, + wax: true, zksync: true, // edge-currency-bitcoin: bitcoin: true, diff --git a/src/util/network.ts b/src/util/network.ts index 3545b720527..3a525229068 100644 --- a/src/util/network.ts +++ b/src/util/network.ts @@ -1,11 +1,16 @@ import { Cleaner } from 'cleaners' import { EdgeFetchFunction, EdgeFetchOptions, EdgeFetchResponse } from 'edge-core-js' +import { asInfoRollup, InfoRollup } from 'edge-info-server' +import { Platform } from 'react-native' +import { getVersion } from 'react-native-device-info' import { config } from '../theme/appConfig' -import { asyncWaterfall, shuffleArray } from './utils' +import { asyncWaterfall, getOsVersion, shuffleArray } from './utils' const INFO_SERVERS = ['https://info1.edge.app', 'https://info2.edge.app'] const RATES_SERVERS = ['https://rates1.edge.app', 'https://rates2.edge.app'] +const INFO_FETCH_INTERVAL = 60000 + export async function fetchWaterfall( servers: string[], path: string, @@ -66,3 +71,28 @@ export const fetchReferral = async (path: string, options?: EdgeFetchOptions, ti export const fetchPush = async (path: string, options?: EdgeFetchOptions, timeout?: number, doFetch?: EdgeFetchFunction): Promise => { return await multiFetch(config.notificationServers, path, options, timeout, doFetch) } + +export const infoServerData: { rollup?: InfoRollup } = {} + +export const initInfoServer = async () => { + const osType = Platform.OS.toLowerCase() + const osVersion = getOsVersion() + const version = getVersion() + + const queryInfo = async () => { + try { + const response = await fetchInfo(`v1/inforollup/${config.appId ?? 'edge'}?os=${osType}&osVersion=${osVersion}&appVersion=${version}`) + if (!response.ok) { + console.warn(`initInfoServer error ${response.status}: ${await response.text()}`) + } else { + const infoData = await response.json() + infoServerData.rollup = asInfoRollup(infoData) + } + } catch (e) { + console.warn('initInfoServer: Failed to ping info server') + } + } + + await queryInfo() + setInterval(queryInfo, INFO_FETCH_INTERVAL) +} diff --git a/src/util/tracking.ts b/src/util/tracking.ts index 0aa282c009b..28ee1083b20 100644 --- a/src/util/tracking.ts +++ b/src/util/tracking.ts @@ -1,15 +1,20 @@ import Bugsnag from '@bugsnag/react-native' import analytics from '@react-native-firebase/analytics' +import { div } from 'biggystring' import { TrackingEventName as LoginTrackingEventName, TrackingValues as LoginTrackingValues } from 'edge-login-ui-rn/lib/util/analytics' import PostHog from 'posthog-react-native' -import { getUniqueId, getVersion } from 'react-native-device-info' +import { getBuildNumber, getUniqueId, getVersion } from 'react-native-device-info' import { getFirstOpenInfo } from '../actions/FirstOpenActions' import { ENV } from '../env' import { ExperimentConfig, getExperimentConfig } from '../experimentConfig' +import { getExchangeDenomination } from '../selectors/DenominationSelectors' +import { convertCurrency } from '../selectors/WalletSelectors' +import { ThunkAction } from '../types/reduxTypes' +import { asBiggystring } from './cleaners' import { fetchReferral } from './network' import { makeErrorLog } from './translateError' -import { consify } from './utils' +import { consify, mulToPrecision } from './utils' export type TrackingEventName = | 'Activate_Wallet_Cancel' | 'Activate_Wallet_Done' @@ -27,6 +32,10 @@ export type TrackingEventName = | 'Exchange_Shift_Quote' | 'Exchange_Shift_Start' | 'Exchange_Shift_Success' + | 'Fio_Domain_Register' + | 'Fio_Domain_Renew' + | 'Fio_Handle_Register' + | 'Fio_Handle_Bundled_Tx' | 'Load_Install_Reason_Match' | 'Load_Install_Reason_Fail' | 'Sell_Quote' @@ -44,16 +53,15 @@ export type TrackingEventName = | 'Start_App_With_Accounts' | 'purchase' | 'Visa_Card_Launch' - // No longer used: - | 'Earn_Spend_Launch' + | 'Earn_Spend_Launch' // No longer used | LoginTrackingEventName +export type OnLogEvent = (event: TrackingEventName, values?: TrackingValues) => void + export interface TrackingValues extends LoginTrackingValues { - accountDate?: string // Account creation date currencyCode?: string // Wallet currency code dollarValue?: number // Conversion amount, in USD error?: unknown | string // Any error - installerId?: string // Account installerId, i.e. referralId orderId?: string // Unique order identifier provided by plugin pluginId?: string // Plugin that provided the conversion numSelectedWallets?: number // Number of wallets to be created @@ -64,6 +72,8 @@ export interface TrackingValues extends LoginTrackingValues { sourceExchangeAmount?: string sourcePluginId?: string // currency pluginId of dest asset numAccounts?: number // Number of full accounts saved on the device + exchangeAmount?: string + nativeAmount?: string } // Set up the global Firebase analytics instance at boot: @@ -128,37 +138,85 @@ export function trackError( /** * Send a raw event to all backends. */ -export function logEvent(event: TrackingEventName, values: TrackingValues = {}) { - const { accountDate, currencyCode, dollarValue, installerId, pluginId, error } = values - getExperimentConfig() - .then(async (experimentConfig: ExperimentConfig) => { - // Persistent & Unchanged params: - const { isFirstOpen, deviceId, firstOpenEpoch } = await getFirstOpenInfo() - const params: any = { edgeVersion: getVersion(), isFirstOpen, deviceId, firstOpenEpoch, ...values } - - // Adjust params: - if (accountDate != null) params.adate = accountDate - if (currencyCode != null) params.currency = currencyCode - if (dollarValue != null) { - params.currency = 'USD' - params.value = Number(dollarValue.toFixed(2)) - params.items = [String(event)] - } - if (installerId != null) params.aid = installerId - if (pluginId != null) params.plugin = pluginId - if (error != null) params.error = makeErrorLog(error) - - // Add all 'sticky' remote config variant values: - for (const key of Object.keys(experimentConfig)) params[`svar_${key}`] = experimentConfig[key as keyof ExperimentConfig] - - // TEMP HACK: Add renamed var for legacyLanding - params.svar_newLegacyLanding = experimentConfig.legacyLanding - - consify({ logEvent: { event, params } }) - - Promise.all([logToPosthog(event, params), logToFirebase(event, params), logToUtilServer(event, params)]).catch(error => console.warn(error)) - }) - .catch(console.error) +export function logEvent(event: TrackingEventName, values: TrackingValues = {}): ThunkAction { + return async (dispatch, getState) => { + const { currencyCode, dollarValue, pluginId, error, exchangeAmount, nativeAmount, sourceExchangeAmount, destExchangeAmount } = values + getExperimentConfig() + .then(async (experimentConfig: ExperimentConfig) => { + // Persistent & Unchanged params: + const { isFirstOpen, deviceId, firstOpenEpoch } = await getFirstOpenInfo() + + const params: any = { edgeVersion: getVersion(), buildNumber: getBuildNumber(), isFirstOpen, deviceId, firstOpenEpoch, ...values } + + // Populate referral params: + const state = getState() + const { deviceReferral, account } = state + const { accountReferral } = account + params.refDeviceInstallerId = deviceReferral.installerId + params.refDeviceCurrencyCodes = deviceReferral.currencyCodes + + const { creationDate, installerId } = accountReferral + params.refAccountDate = installerId == null || creationDate == null ? undefined : creationDate.toISOString().replace(/-\d\dT.*/, '') + params.refAccountInstallerId = accountReferral.installerId + params.refAccountCurrencyCodes = accountReferral.currencyCodes + + // Adjust params: + if (currencyCode != null) params.currency = currencyCode + if (dollarValue != null) { + // If an explicit dollarValue was given, prioritize it + params.currency = 'USD' + params.value = Number(dollarValue.toFixed(2)) + params.items = [String(event)] + } else if (currencyCode != null) { + // Else, calculate the dollar value from crypto amounts, if required props given + if (nativeAmount != null && pluginId != null) { + try { + asBiggystring(nativeAmount) + } catch (e) { + trackError('Error in tracking nativeAmount: ' + JSON.stringify({ event, values })) + } + const { multiplier } = getExchangeDenomination(state, pluginId, currencyCode) + params.value = div(nativeAmount, multiplier, mulToPrecision(multiplier)) + } else if (exchangeAmount != null) { + params.value = parseFloat( + convertCurrency(state, currencyCode, 'iso:USD', typeof destExchangeAmount === 'string' ? destExchangeAmount : String(destExchangeAmount)) + ) + } else if (sourceExchangeAmount != null) { + try { + asBiggystring(sourceExchangeAmount) + } catch (e) { + trackError('Error in tracking sourceExchangeAmount: ' + JSON.stringify({ event, values })) + } + params.sourceExchangeAmount = sourceExchangeAmount + } else if (destExchangeAmount != null) { + try { + asBiggystring(destExchangeAmount) + } catch (e) { + trackError('Error in tracking destExchangeAmount: ' + JSON.stringify({ event, values })) + } + params.destExchangeAmount = destExchangeAmount + try { + asBiggystring(exchangeAmount) + } catch (e) { + trackError('Error in tracking exchangeAmount: ' + JSON.stringify({ event, values })) + } + params.value = convertCurrency(state, currencyCode, 'iso:USD', exchangeAmount) + } else { + console.warn('Unable to calculate dollar value for event:', event, values) + } + } + if (pluginId != null) params.plugin = pluginId + if (error != null) params.error = makeErrorLog(error) + + // Add all 'sticky' remote config variant values: + for (const key of Object.keys(experimentConfig)) params[`svar_${key}`] = experimentConfig[key as keyof ExperimentConfig] + + consify({ logEvent: { event, params } }) + + Promise.all([logToPosthog(event, params), logToFirebase(event, params), logToUtilServer(event, params)]).catch(error => console.warn(error)) + }) + .catch(console.error) + } } /** diff --git a/src/util/utils.ts b/src/util/utils.ts index b4160e89077..41600f83f43 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -10,6 +10,7 @@ import { EdgeTransaction } from 'edge-core-js' import { Linking, Platform } from 'react-native' +import DeviceInfo from 'react-native-device-info' import SafariView from 'react-native-safari-view' import { sprintf } from 'sprintf-js' import { v4 } from 'uuid' @@ -24,7 +25,6 @@ import { } from '../constants/WalletAndCurrencyConstants' import { toLocaleDate, toLocaleDateTime, toLocaleTime, truncateDecimalsPeriod } from '../locales/intl' import { lstrings } from '../locales/strings' -import { convertCurrencyFromExchangeRates } from '../selectors/WalletSelectors' import { RootState } from '../types/reduxTypes' import { GuiExchangeRates, GuiFiatType } from '../types/types' import { getWalletFiat } from '../util/CurrencyWalletHelpers' @@ -148,6 +148,18 @@ export const roundedFee = (nativeAmount: string, decimalPlacesBeyondLeadingZeros return `${truncatedAmount} ` } +export const convertCurrencyFromExchangeRates = ( + exchangeRates: { [pair: string]: string }, + fromCurrencyCode: string, + toCurrencyCode: string, + amount: string +): string => { + const rateKey = `${fromCurrencyCode}_${toCurrencyCode}` + const rate = exchangeRates[rateKey] ?? '0' + const convertedAmount = mul(amount, rate) + return convertedAmount +} + // Used to convert outputs from core into other denominations (exchangeDenomination, displayDenomination) export const convertNativeToDenomination = (nativeToTargetRatio: string) => @@ -632,3 +644,11 @@ export const darkenHexColor = (hexColor: string, scaleFactor: number): string => return scaledHexColor } + +/** + * Reads and normalizes the OS version. + */ +export function getOsVersion(): string { + const osVersionRaw = DeviceInfo.getSystemVersion() + return Array.from({ length: 3 }, (_, i) => osVersionRaw.split('.')[i] || '0').join('.') +} diff --git a/src/wdyr.ts b/src/wdyr.ts new file mode 100644 index 00000000000..fb1e5a6ac8e --- /dev/null +++ b/src/wdyr.ts @@ -0,0 +1,10 @@ +import whyDidYouRender from '@welldone-software/why-did-you-render' +import React from 'react' + +whyDidYouRender(React, { + onlyLogs: true, + titleColor: 'green', + diffNameColor: 'aqua', + logOnDifferentValues: false, + trackAllPureComponents: true +}) diff --git a/yarn.lock b/yarn.lock index 6d2b217ab0a..121bea4ddd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2478,6 +2478,11 @@ dependencies: browser-headers "^0.4.0" +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@ipld/dag-cbor@^9.0.0": version "9.0.4" resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-9.0.4.tgz#5e1bc63aeabf882a750be88bb6879017c5c41930" @@ -3033,6 +3038,98 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== +"@parcel/watcher-android-arm64@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.0.tgz#9c93763794153e4f76920994a423b6ea3257059d" + integrity sha512-+fPtO/GsbYX1LJnCYCaDVT3EOBjvSFdQN9Mrzh9zWAOOfvidPWyScTrHIZHHfJBvlHzNA0Gy0U3NXFA/M7PHUA== + +"@parcel/watcher-darwin-arm64@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.0.tgz#2c79c2abde16aa24cac67e555b60802fd13fe210" + integrity sha512-T/At5pansFuQ8VJLRx0C6C87cgfqIYhW2N/kBfLCUvDhCah0EnLLwaD/6MW3ux+rpgkpQAnMELOCTKlbwncwiA== + +"@parcel/watcher-darwin-x64@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.0.tgz#23d82f198c5d033f047467c68d7c335f3df49b46" + integrity sha512-vZMv9jl+szz5YLsSqEGCMSllBl1gU1snfbRL5ysJU03MEa6gkVy9OMcvXV1j4g0++jHEcvzhs3Z3LpeEbVmY6Q== + +"@parcel/watcher-freebsd-x64@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.0.tgz#7310cc86abc27dacd57624bcdba1f0ba092e76df" + integrity sha512-dHTRMIplPDT1M0+BkXjtMN+qLtqq24sLDUhmU+UxxLP2TEY2k8GIoqIJiVrGWGomdWsy5IO27aDV1vWyQ6gfHA== + +"@parcel/watcher-linux-arm-glibc@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.0.tgz#c31b76e695027eeb1078d3d6f1d641d0b900c335" + integrity sha512-9NQXD+qk46RwATNC3/UB7HWurscY18CnAPMTFcI9Y8CTbtm63/eex1SNt+BHFinEQuLBjaZwR2Lp+n7pmEJPpQ== + +"@parcel/watcher-linux-arm64-glibc@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.0.tgz#56e09b86e9d8a4096f606be118b588da6e965080" + integrity sha512-QuJTAQdsd7PFW9jNGaV9Pw+ZMWV9wKThEzzlY3Lhnnwy7iW23qtQFPql8iEaSFMCVI5StNNmONUopk+MFKpiKg== + +"@parcel/watcher-linux-arm64-musl@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.0.tgz#27ffd5ca5f510ecd638f9ad22e2e813049db54e7" + integrity sha512-oyN+uA9xcTDo/45bwsd6TFHa7Lc7hKujyMlvwrCLvSckvWogndCEoVYFNfZ6JJ2KNL/6fFiGPcbjp8jJmEh5Ng== + +"@parcel/watcher-linux-x64-glibc@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.0.tgz#44cbbb1e5884a1ca900655f47a0775218318f934" + integrity sha512-KphV8awJmxU3q52JQvJot0QMu07CIyEjV+2Tb2ZtbucEgqyRcxOBDMsqp1JNq5nuDXtcCC0uHQICeiEz38dPBQ== + +"@parcel/watcher-linux-x64-musl@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.0.tgz#4c33993618c8d5113722852806239cb80360494b" + integrity sha512-7jzcOonpXNWcSijPpKD5IbC6xC7yTibjJw9jviVzZostYLGxbz8LDJLUnLzLzhASPlPGgpeKLtFUMjAAzM+gSA== + +"@parcel/watcher-wasm@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-wasm/-/watcher-wasm-2.4.0.tgz#84a3959c8ef1cd67b36b9fec487edbc8f27719f6" + integrity sha512-MNgQ4WCbBybqQ97KwR/hqJGYTg3+s8qHpgIyFWB2qJOBvoJWbXuJGmm4ZkPLq2bMaANqCZqrXwmKYagZTkMKZA== + dependencies: + is-glob "^4.0.3" + micromatch "^4.0.5" + napi-wasm "^1.1.0" + +"@parcel/watcher-win32-arm64@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.0.tgz#2a172fd2fda95fe5389298ca3e70b5a96316162a" + integrity sha512-NOej2lqlq8bQNYhUMnOD0nwvNql8ToQF+1Zhi9ULZoG+XTtJ9hNnCFfyICxoZLXor4bBPTOnzs/aVVoefYnjIg== + +"@parcel/watcher-win32-ia32@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.0.tgz#279225b2ebe1fadd3c5137c9b2365ad422656904" + integrity sha512-IO/nM+K2YD/iwjWAfHFMBPz4Zqn6qBDqZxY4j2n9s+4+OuTSRM/y/irksnuqcspom5DjkSeF9d0YbO+qpys+JA== + +"@parcel/watcher-win32-x64@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.0.tgz#93e0bd0ad1bda2c9a688764b9b30b71dc5b72a71" + integrity sha512-pAUyUVjfFjWaf/pShmJpJmNxZhbMvJASUpdes9jL6bTEJ+gDxPRSpXTIemNyNsb9AtbiGXs9XduP1reThmd+dA== + +"@parcel/watcher@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.0.tgz#2d3c4ef8832a5cdfdbb76b914f022489933e664f" + integrity sha512-XJLGVL0DEclX5pcWa2N9SX1jCGTDd8l972biNooLFtjneuGqodupPQh6XseXIBBeVIMaaJ7bTcs3qGvXwsp4vg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.4.0" + "@parcel/watcher-darwin-arm64" "2.4.0" + "@parcel/watcher-darwin-x64" "2.4.0" + "@parcel/watcher-freebsd-x64" "2.4.0" + "@parcel/watcher-linux-arm-glibc" "2.4.0" + "@parcel/watcher-linux-arm64-glibc" "2.4.0" + "@parcel/watcher-linux-arm64-musl" "2.4.0" + "@parcel/watcher-linux-x64-glibc" "2.4.0" + "@parcel/watcher-linux-x64-musl" "2.4.0" + "@parcel/watcher-win32-arm64" "2.4.0" + "@parcel/watcher-win32-ia32" "2.4.0" + "@parcel/watcher-win32-x64" "2.4.0" + "@pnpm/network.ca-file@^1.0.1": version "1.0.2" resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" @@ -3986,7 +4083,7 @@ "@stablelib/constant-time" "^1.0.1" "@stablelib/wipe" "^1.0.1" -"@stablelib/random@1.0.2", "@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": +"@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c" integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== @@ -5000,44 +5097,45 @@ "@urql/core" ">=2.3.1" wonka "^4.0.14" -"@walletconnect/auth-client@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/auth-client/-/auth-client-2.1.0.tgz#47b794cf807d6211fe3a87531f7fca7c6838fd3c" - integrity sha512-k6zZLEdlBpYIvbOL5tBWd+3DUJ2R4VFDyHpdp4TuRzC//njRkIzRSksEnsr8gN8P+IKuoJTLPsDy2sWR4qVTNQ== +"@walletconnect/auth-client@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@walletconnect/auth-client/-/auth-client-2.1.2.tgz#cee304fb0cdca76f6bf4aafac96ef9301862a7e8" + integrity sha512-ubJLn+vGb8sTdBFX6xAh4kjR5idrtS3RBngQWaJJJpEPBQmxMb8pM2q0FIRs8Is4K6jKy+uEhusMV+7ZBmTzjw== dependencies: "@ethersproject/hash" "^5.7.0" "@ethersproject/transactions" "^5.7.0" - "@stablelib/random" "1.0.2" + "@stablelib/random" "^1.0.2" "@stablelib/sha256" "^1.0.1" - "@walletconnect/core" "^2.7.2" + "@walletconnect/core" "^2.10.1" "@walletconnect/events" "^1.0.1" - "@walletconnect/heartbeat" "^1.2.0" - "@walletconnect/jsonrpc-utils" "^1.0.7" + "@walletconnect/heartbeat" "^1.2.1" + "@walletconnect/jsonrpc-utils" "^1.0.8" "@walletconnect/logger" "^2.0.1" "@walletconnect/time" "^1.0.2" - "@walletconnect/utils" "^2.7.2" + "@walletconnect/utils" "^2.10.1" events "^3.3.0" isomorphic-unfetch "^3.1.0" -"@walletconnect/core@2.9.0", "@walletconnect/core@^2.7.2": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.9.0.tgz#7837a5d015a22b48d35b987bcde2aa9ccdf300d8" - integrity sha512-MZYJghS9YCvGe32UOgDj0mCasaOoGHQaYXWeQblXE/xb8HuaM6kAWhjIQN9P+MNp5QP134BHP5olQostcCotXQ== +"@walletconnect/core@2.11.1", "@walletconnect/core@^2.10.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.1.tgz#da2be26b8b6514c74f06dc9a5ffb450bdec3456d" + integrity sha512-T57Vd7YdbHPsy3tthBuwrhaZNafN0+PqjISFRNeJy/bsKdXxpJg2hGSARuOTpCO7V6VcaatqlaSMuG3DrnG5rA== dependencies: "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-provider" "1.0.13" "@walletconnect/jsonrpc-types" "1.0.3" "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/jsonrpc-ws-connection" "1.0.12" - "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "^1.1.1" "@walletconnect/logger" "^2.0.1" "@walletconnect/relay-api" "^1.0.9" "@walletconnect/relay-auth" "^1.0.4" "@walletconnect/safe-json" "^1.0.2" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.9.0" - "@walletconnect/utils" "2.9.0" + "@walletconnect/types" "2.11.1" + "@walletconnect/utils" "2.11.1" events "^3.3.0" + isomorphic-unfetch "3.1.0" lodash.isequal "4.5.0" uint8arrays "^3.1.0" @@ -5056,7 +5154,7 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" -"@walletconnect/heartbeat@1.2.1", "@walletconnect/heartbeat@^1.2.0": +"@walletconnect/heartbeat@1.2.1", "@walletconnect/heartbeat@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@walletconnect/heartbeat/-/heartbeat-1.2.1.tgz#afaa3a53232ae182d7c9cff41c1084472d8f32e9" integrity sha512-yVzws616xsDLJxuG/28FqtZ5rzrTA4gUjdEMTbWB5Y8V1XHRmqq4efAxCw5ie7WjbXFSUyBHaWlMR+2/CpQC5Q== @@ -5082,7 +5180,7 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" -"@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.7", "@walletconnect/jsonrpc-utils@^1.0.8": +"@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz#82d0cc6a5d6ff0ecc277cb35f71402c91ad48d72" integrity sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw== @@ -5091,24 +5189,24 @@ "@walletconnect/jsonrpc-types" "^1.0.3" tslib "1.14.1" -"@walletconnect/jsonrpc-ws-connection@1.0.12": - version "1.0.12" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.12.tgz#2192314884fabdda6d0a9d22e157e5b352025ed8" - integrity sha512-HAcadga3Qjt1Cqy+qXEW6zjaCs8uJGdGQrqltzl3OjiK4epGZRdvSzTe63P+t/3z+D2wG+ffEPn0GVcDozmN1w== +"@walletconnect/jsonrpc-ws-connection@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.14.tgz#eec700e74766c7887de2bd76c91a0206628732aa" + integrity sha512-Jsl6fC55AYcbkNVkwNM6Jo+ufsuCQRqViOQ8ZBPH9pRREHH9welbBiszuTLqEJiQcO/6XfFDl6bzCJIkrEi8XA== dependencies: "@walletconnect/jsonrpc-utils" "^1.0.6" "@walletconnect/safe-json" "^1.0.2" events "^3.3.0" - tslib "1.14.1" ws "^7.5.1" -"@walletconnect/keyvaluestorage@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.0.2.tgz#92f5ca0f54c1a88a093778842ce0c874d86369c8" - integrity sha512-U/nNG+VLWoPFdwwKx0oliT4ziKQCEoQ27L5Hhw8YOFGA2Po9A9pULUYNWhDgHkrb0gYDNt//X7wABcEWWBd3FQ== +"@walletconnect/keyvaluestorage@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" + integrity sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA== dependencies: - safe-json-utils "^1.1.1" - tslib "1.14.1" + "@walletconnect/safe-json" "^1.0.1" + idb-keyval "^6.2.1" + unstorage "^1.9.0" "@walletconnect/logger@2.0.1", "@walletconnect/logger@^2.0.1": version "2.0.1" @@ -5118,13 +5216,14 @@ pino "7.11.0" tslib "1.14.1" -"@walletconnect/react-native-compat@^2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@walletconnect/react-native-compat/-/react-native-compat-2.9.0.tgz#58fe9a1b838218c4cbe45fc80a556282c65c3bf4" - integrity sha512-W1xPdarLuo9ZfJM+x9pDDZ2ZBZLYZOwH09jWnkYGY0B6D1FewS2spWY5/4aVgsBB7zY1M4g40RwwfEMz3ZHeeg== +"@walletconnect/react-native-compat@^2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@walletconnect/react-native-compat/-/react-native-compat-2.11.0.tgz#10716f528e7c50808f15c0b39f07d2c9266a262e" + integrity sha512-AZNWc2+jr1i7EYwiQwqwfKgHaxXT73a1mb7eHN2N6leRxnkYPAYBTApYE07Ya+WV8QqKQADStLc4l9x432xeUw== dependencies: events "3.3.0" fast-text-encoding "^1.0.6" + react-native-url-polyfill "^2.0.0" "@walletconnect/relay-api@^1.0.9": version "1.0.9" @@ -5153,19 +5252,19 @@ dependencies: tslib "1.14.1" -"@walletconnect/sign-client@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.9.0.tgz#fd3b0acb68bc8d56350f01ed70f8c6326e6e89fa" - integrity sha512-mEKc4LlLMebCe45qzqh+MX4ilQK4kOEBzLY6YJpG8EhyT45eX4JMNA7qQoYa9MRMaaVb/7USJcc4e3ZrjZvQmA== +"@walletconnect/sign-client@2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.11.1.tgz#c073b8d2d594e792bb783d36c8b021bd37a9d4f6" + integrity sha512-s3oKSx6/F5X2WmkV1jfJImBFACf9Km5HpTb+n5q+mobJVpUQw/clvoVyIrNNppLhm1V1S/ylHXh0qCrDppDpCA== dependencies: - "@walletconnect/core" "2.9.0" + "@walletconnect/core" "2.11.1" "@walletconnect/events" "^1.0.1" "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "^2.0.1" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.9.0" - "@walletconnect/utils" "2.9.0" + "@walletconnect/types" "2.11.1" + "@walletconnect/utils" "2.11.1" events "^3.3.0" "@walletconnect/time@^1.0.2": @@ -5175,22 +5274,22 @@ dependencies: tslib "1.14.1" -"@walletconnect/types@2.9.0", "@walletconnect/types@^2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.9.0.tgz#6e5dfdc7212c1ec4ab49a1ec409c743e16093f72" - integrity sha512-ORopsMfSRvUYqtjKKd6scfg8o4/aGebipLxx92AuuUgMTERSU6cGmIrK6rdLu7W6FBJkmngPLEGc9mRqAb9Lug== +"@walletconnect/types@2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.11.1.tgz#4f705b43ddc286b69eb9bf91bb6e9496d20de0e3" + integrity sha512-UbdbX+d6MOK0AXKxt5imV3KvAcLVpZUHylaRDIP5ffwVylM/p4DHnKppil1Qq5N+IGDr3RsUwLGFkKjqsQYRKw== dependencies: "@walletconnect/events" "^1.0.1" "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-types" "1.0.3" - "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/keyvaluestorage" "^1.1.1" "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/utils@2.9.0", "@walletconnect/utils@^2.7.2", "@walletconnect/utils@^2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.9.0.tgz#c73925edb9fefe79021bcf028e957028f986b728" - integrity sha512-7Tu3m6dZL84KofrNBcblsgpSqU2vdo9ImLD7zWimLXERVGNQ8smXG+gmhQYblebIBhsPzjy9N38YMC3nPlfQNw== +"@walletconnect/utils@2.11.1", "@walletconnect/utils@^2.10.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.11.1.tgz#56116d9c410c6f2ae8d562017cf6876cccb366f1" + integrity sha512-wRFDHN86dZ05mCET1H3912odIeQa8j7cZKxl1FlWRpV2YsILj9HCYSX6Uq2brwO02Kv2vryke44G1r8XI/LViA== dependencies: "@stablelib/chacha20poly1305" "1.0.1" "@stablelib/hkdf" "1.0.1" @@ -5200,26 +5299,26 @@ "@walletconnect/relay-api" "^1.0.9" "@walletconnect/safe-json" "^1.0.2" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.9.0" + "@walletconnect/types" "2.11.1" "@walletconnect/window-getters" "^1.0.1" "@walletconnect/window-metadata" "^1.0.1" detect-browser "5.3.0" query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/web3wallet@^1.8.6": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.8.6.tgz#445f547111dafb1b673d71f6fef849580a14439b" - integrity sha512-HxE3Jtaxs5cKhZNULEwApeMnsQsh9SEyw4FO+lafoe9KKdc2neQlY/CnPz/S4i345/Dg+bz6BcUNXouimgz3EQ== +"@walletconnect/web3wallet@^1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.10.1.tgz#62271183b72b3e0ec47c683bf021549d47225f39" + integrity sha512-lXyfljLLQ/76F5ftgaKaIoPfj/R2Mi2Tv2JnIXNonlIlAITdghYVby+xYbh4b+0yldf8fr8lqxFuHBuVoWhMjw== dependencies: - "@walletconnect/auth-client" "2.1.0" - "@walletconnect/core" "2.9.0" + "@walletconnect/auth-client" "2.1.2" + "@walletconnect/core" "2.11.1" "@walletconnect/jsonrpc-provider" "1.0.13" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "2.0.1" - "@walletconnect/sign-client" "2.9.0" - "@walletconnect/types" "2.9.0" - "@walletconnect/utils" "2.9.0" + "@walletconnect/sign-client" "2.11.1" + "@walletconnect/types" "2.11.1" + "@walletconnect/utils" "2.11.1" "@walletconnect/window-getters@^1.0.1": version "1.0.1" @@ -5389,6 +5488,13 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== +"@welldone-software/why-did-you-render@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-8.0.1.tgz#e69303ca98424642843f8dd9aa8d4e4f638234b2" + integrity sha512-PtLBjiHNX04gDPheMeAQP16S24JV3SOW6wGDUrm4bFPZmofmmflgvd4Kacf/jhB8zlX6equ8m3t6CS+OxA3Q4g== + dependencies: + lodash "^4" + "@xmldom/xmldom@0.8.6": version "0.8.6" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" @@ -5489,10 +5595,10 @@ acorn-walk@^8.2.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.4.1, acorn@^8.7.0, acorn@^8.8.0, acorn@^8.8.2: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.11.3, acorn@^8.4.1, acorn@^8.7.0, acorn@^8.8.0, acorn@^8.8.2: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== aes-js@3.0.0: version "3.0.0" @@ -5690,10 +5796,10 @@ any-promise@^1.0.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= -anymatch@^3.0.3, anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== +anymatch@^3.0.3, anymatch@^3.1.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -5781,6 +5887,11 @@ application-config-path@^0.1.0: resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.1.tgz#8b5ac64ff6afdd9bd70ce69f6f64b6998f5f756e" integrity sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw== +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + arg@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" @@ -6613,7 +6724,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -7086,7 +7197,7 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== -chokidar@^3.4.2, chokidar@^3.5.2: +chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -7145,6 +7256,13 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +citty@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.5.tgz#fe37ceae5dc764af75eb2fece99d2bf527ea4e50" + integrity sha512-AS7n5NSc0OQVMV9v6wt3ByujNIrne0/cTjiC2MYqhvao57VNfiuVksTSr2p17nVOhEr2KtqiAkGwHcgMC/qUuQ== + dependencies: + consola "^3.2.3" + cjs-module-lexer@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" @@ -7225,6 +7343,15 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +clipboardy@3.0.0, clipboardy@^4.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092" + integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg== + dependencies: + arch "^2.2.0" + execa "^5.1.1" + is-wsl "^2.2.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -7288,6 +7415,11 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -7517,6 +7649,11 @@ consola@^2.15.0: resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== +consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -7550,6 +7687,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-es@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.0.0.tgz#4759684af168dfc54365b2c2dda0a8d7ee1e4865" + integrity sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -7694,6 +7836,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crossws@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.1.1.tgz#3a85a8140568e4828d9747a884171ea7e6a8bbe2" + integrity sha512-c9c/o7bS3OjsdpSkvexpka0JNlesBF2JU9B2V1yNsYGwRbAafxhJQ7VI9b48D5bpONz/oxbPGMzBojy9sXoQIQ== + crypt@0.0.2, crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -8235,6 +8382,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +defu@^6.1.3, defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + degenerator@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-3.0.2.tgz#6a61fcc42a702d6e50ff6023fe17bff435f68235" @@ -8274,6 +8426,11 @@ denodeify@^1.2.1: resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -8301,6 +8458,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destr@^2.0.1, destr@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.2.tgz#8d3c0ee4ec0a76df54bc8b819bca215592a8c218" + integrity sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -8580,10 +8742,10 @@ ed25519@0.0.4: bindings "^1.2.1" nan "^2.0.9" -edge-core-js@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.1.1.tgz#5bc2d26b1d20bc1e8542210d02c9c696e89e117f" - integrity sha512-bDiOlz3iiCTbtnHbzm89sbe4ggvE8+HsE3/U3Wm++OIQopaZs8jJYNN8w/FrIvrO1QX0q+upQryecWx9jIysrg== +edge-core-js@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.2.1.tgz#d29281a8c02ff84d5901ee47210067b87b81f27c" + integrity sha512-1NjbdODCzzt7JFPqcGSUWWPTOwGe1fPPYzp3gKAnTr5okwcxnp4C3pAF4ksYSVhcmg6gQgG8bqW5dYNdsrdJLw== dependencies: aes-js "^3.1.0" base-x "^4.0.0" @@ -8605,10 +8767,10 @@ edge-core-js@^2.1.1: yaob "^0.3.12" yavent "^0.1.3" -edge-currency-accountbased@^3.0.4-1: - version "3.0.4-1" - resolved "https://registry.yarnpkg.com/edge-currency-accountbased/-/edge-currency-accountbased-3.0.4-1.tgz#6a1acfcdb3b9de31c6bfcc5d26849105df619e07" - integrity sha512-MwvxFaaLZwLsQ8jE41nKqSV6wWUfdvPEsiWYSLLCG3grZwS7ngO9bWxi7Hx9EPEolqKBv7czYh9Lm79dQduJ9Q== +edge-currency-accountbased@^3.1.2-3: + version "3.1.2-3" + resolved "https://registry.yarnpkg.com/edge-currency-accountbased/-/edge-currency-accountbased-3.1.2-3.tgz#cfc57f680a32c8589b605ff0b1654a0c05228033" + integrity sha512-n8j0WqJZGEE6DQt8rkcGT8RXD6Dvt3zSTVqitIeWRyG+VoLLPPjA8zQogu4MuBcgFx8H2vFD92F78PhjdcGbIg== dependencies: "@binance-chain/javascript-sdk" "^4.2.0" "@chain-registry/client" "^1.15.0" @@ -8687,10 +8849,10 @@ edge-currency-plugins@^2.5.4: wifgrs "^2.0.6" ws "^7.4.6" -edge-exchange-plugins@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/edge-exchange-plugins/-/edge-exchange-plugins-2.0.2.tgz#c1f13610211a75400699b99fe45132fe80eea6ac" - integrity sha512-fwpm9JmgDBDefNr9fLJ/Ml+IEc7viGWOCm9Xr0GMHSGG+4NQH7+GgYNvyWR22/Qjuhddo+2RJB4pw+Ob3yNdAQ== +edge-exchange-plugins@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/edge-exchange-plugins/-/edge-exchange-plugins-2.1.1.tgz#cf22e66d241672bc2751ba1c18e66929ce3ecef7" + integrity sha512-axEDj/J3qkw2WAzPmR9wTY/o1khdtR8pCkz4cMKYpXAV1+BVBUKiDYXZCOBnLXNbN2QzZ/8IHTG027Gxjg4JnA== dependencies: biggystring "^4.1.3" cleaners "^0.3.13" @@ -8698,17 +8860,17 @@ edge-exchange-plugins@^2.0.2: regenerator-runtime "0.13.11" xrpl "^2.10.0" -edge-info-server@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/edge-info-server/-/edge-info-server-1.4.0.tgz#5da9420c71b1d63a92f74358db1c52fd4f530b5a" - integrity sha512-2Svi1hO6vCwt0I6WnMOd/XrNQNilVTqyvw6i7yVdnnqAwT7eL+efSzMprtr/wWO05DLGE7H8W9saMEX+/EhXLg== +edge-info-server@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/edge-info-server/-/edge-info-server-2.0.0.tgz#0f9b87d166aeb2028dd8a71a1056c3c2b4105c23" + integrity sha512-gYzhpgbnuyfo3T2H62yKEy+0fdfkAiaSYPnBiUPGD90TfNiK0mhGPgaGOiJBApAwv5w/1hm9JJzeaiQmvEgblw== dependencies: cleaners "^0.3.16" -edge-login-ui-rn@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/edge-login-ui-rn/-/edge-login-ui-rn-3.3.2.tgz#3e293385d578abc5c41b69090ae8bc1a13e682a8" - integrity sha512-upu9MNfPPM0O+H/2G3+2YTa/vb6QK5Tp17uTUVzjlg2X4oYF1A1m1T0/NcVtvd3xFWzkTeE+NsEF3BBH8bczcg== +edge-login-ui-rn@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/edge-login-ui-rn/-/edge-login-ui-rn-3.3.4.tgz#8fd9a95fd05a0f8cdea976e8abb94d73a7e0710a" + integrity sha512-R4EqdQiANc4Aeu6WpC5jNEpLcElB+xAUjKLVTARIvWHPT3Vq103Ge5ZW2anhj0TFKFwAZUw1tQLXF5yPHlpz+g== dependencies: base-x "^4.0.0" cleaners "^0.3.12" @@ -9714,7 +9876,7 @@ execa@^4.1.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -10541,6 +10703,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port-please@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.1.2.tgz#502795e56217128e4183025c89a48c71652f4e49" + integrity sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ== + get-port@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" @@ -10837,6 +11004,21 @@ groestl-hash-js@^1.0.0, "groestl-hash-js@https://github.com/Groestlcoin/groestl- version "1.0.0" resolved "https://github.com/Groestlcoin/groestl-hash-js.git#ef6a04f1c4d2f0448f0882b5f213ef7a0659baee" +h3@^1.10.1, h3@^1.8.2: + version "1.10.1" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.10.1.tgz#221634ca9bdb216a6b359bd2915be466a179b8a1" + integrity sha512-UBAUp47hmm4BB5/njB4LrEa9gpuvZj4/Qf/ynSMzO6Ku2RXaouxEfiG2E2IFnv6fxbhAkzjasDxmo6DFdEeXRg== + dependencies: + cookie-es "^1.0.0" + defu "^6.1.4" + destr "^2.0.2" + iron-webcrypto "^1.0.0" + ohash "^1.1.3" + radix3 "^1.1.0" + ufo "^1.3.2" + uncrypto "^0.1.3" + unenv "^1.9.0" + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -11085,6 +11267,11 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-shutdown@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" + integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== + http-signature@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" @@ -11154,6 +11341,11 @@ iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + idna-uts46-hx@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz#a1dc5c4df37eee522bf66d969cc980e00e8711f9" @@ -11335,6 +11527,21 @@ invariant@*, invariant@2.2.4, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7" + integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -11355,6 +11562,11 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== +iron-webcrypto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.0.0.tgz#e3b689c0c61b434a0a4cb82d0aeabbc8b672a867" + integrity sha512-anOK1Mktt8U1Xi7fCM3RELTuYbnFikQY5VtrDj7kPgpejV7d43tWKhzgioO0zpkazLEL/j/iayRqnJhrGfqUsg== + is-arguments@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" @@ -11749,7 +11961,7 @@ isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" -isomorphic-unfetch@^3.1.0: +isomorphic-unfetch@3.1.0, isomorphic-unfetch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== @@ -12265,6 +12477,11 @@ jimp-compact@0.16.1: resolved "https://registry.yarnpkg.com/jimp-compact/-/jimp-compact-0.16.1.tgz#9582aea06548a2c1e04dd148d7c3ab92075aefa3" integrity sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww== +jiti@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + joi@^17.2.1: version "17.4.0" resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.0.tgz#b5c2277c8519e016316e49ababd41a1908d9ef20" @@ -12489,6 +12706,11 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -12720,6 +12942,30 @@ listenercount@~1.0.1: resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= +listhen@^1.5.5: + version "1.6.0" + resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.6.0.tgz#df26c527c59b87557be4d0408d4a09626bd946c8" + integrity sha512-z0RcEXVX5oTpY1bO02SKoTU/kmZSrFSngNNzHRM6KICR17PTq7ANush6AE6ztGJwJD4RLpBrVHd9GnV51J7s3w== + dependencies: + "@parcel/watcher" "^2.4.0" + "@parcel/watcher-wasm" "2.4.0" + citty "^0.1.5" + clipboardy "^4.0.0" + consola "^3.2.3" + crossws "^0.1.0" + defu "^6.1.4" + get-port-please "^3.1.2" + h3 "^1.10.1" + http-shutdown "^1.2.2" + jiti "^1.21.0" + mlly "^1.5.0" + node-forge "^1.3.1" + pathe "^1.1.2" + std-env "^3.7.0" + ufo "^1.3.2" + untun "^0.1.3" + uqr "^0.1.2" + listr2@^3.2.2: version "3.8.2" resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.8.2.tgz#99b138ad1cfb08f1b0aacd422972e49b2d814b99" @@ -12814,11 +13060,21 @@ lodash.debounce@4.0.8, lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.frompairs@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz#bc4e5207fa2757c136e573614e9664506b2b1bd2" integrity sha1-vE5SB/onV8E25XNhTpZkUGsrG9I= +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isequal@4.5.0, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -12864,7 +13120,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= -lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@4.17.21, lodash@^4, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12930,6 +13186,11 @@ lowercase-keys@^3.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== +lru-cache@^10.0.2: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -13390,13 +13651,13 @@ metro@0.73.10: ws "^7.5.1" yargs "^17.5.1" -micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" miller-rabin@^4.0.0: version "4.0.1" @@ -13428,6 +13689,11 @@ mime@^2.4.1, mime@^2.4.4: resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -13582,6 +13848,16 @@ mkdirp@*, mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4: dependencies: minimist "^1.2.6" +mlly@^1.2.0, mlly@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.5.0.tgz#8428a4617d54cc083d3009030ac79739a0e5447a" + integrity sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ== + dependencies: + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.0.3" + ufo "^1.3.2" + mock-fs@^4.1.0: version "4.14.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" @@ -13597,6 +13873,11 @@ moment@^2.21.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +mri@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms-rest@2.5.6: version "2.5.6" resolved "https://registry.yarnpkg.com/ms-rest/-/ms-rest-2.5.6.tgz#14de759426438ea038c3eb278b664740ae60d6d9" @@ -13764,6 +14045,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-wasm@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.0.tgz#bbe617823765ae9c1bc12ff5942370eae7b2ba4e" + integrity sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -13868,6 +14154,11 @@ node-addon-api@^6.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== +node-addon-api@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.0.tgz#71f609369379c08e251c558527a107107b5e0fdb" + integrity sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g== + node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" @@ -13880,6 +14171,11 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== +node-fetch-native@^1.4.0, node-fetch-native@^1.4.1, node-fetch-native@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.1.tgz#f95c74917d3cebc794cdae0cd2a9c7594aad0cb4" + integrity sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw== + node-fetch@2, node-fetch@2.x, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -14113,6 +14409,20 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +ofetch@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.3.tgz#588cb806a28e5c66c2c47dd8994f9059a036d8c0" + integrity sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg== + dependencies: + destr "^2.0.1" + node-fetch-native "^1.4.0" + ufo "^1.3.0" + +ohash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" + integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== + omelette@0.4.17: version "0.4.17" resolved "https://registry.yarnpkg.com/omelette/-/omelette-0.4.17.tgz#24f3ed85cfdd11d8365f16368c7169f8dc422d73" @@ -14567,6 +14877,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.0, pathe@^1.1.1, pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + pbkdf2@^3.0.14, pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -14671,6 +14986,15 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" +pkg-types@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" + integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + dependencies: + jsonc-parser "^3.2.0" + mlly "^1.2.0" + pathe "^1.1.0" + pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -15070,6 +15394,11 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +radix3@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.0.tgz#9745df67a49c522e94a33d0a93cf743f104b6e0d" + integrity sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A== + randombytes@2.1.0, randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.0.6, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -15358,10 +15687,10 @@ react-native-permissions@^3.8.3: picocolors "^1.0.0" pkg-dir "^5.0.0" -react-native-piratechain@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/react-native-piratechain/-/react-native-piratechain-0.4.3.tgz#11e745bab390f4ccb85951c8e4957c3f372f1991" - integrity sha512-2ENCcmop+LCg9tzg+PE14i6W8NpbgnI2A60hyVdyMtu0aVcii7/F6ZHGYZxOTJi+N+s7LZgIxYYQu8w83mtH6g== +react-native-piratechain@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/react-native-piratechain/-/react-native-piratechain-0.4.4.tgz#ad96bffe1eae51d1342782b67cb9afe77703df41" + integrity sha512-kzf5C6dFMMu55QLTR/810jnhyEr1wz8vOTKz21jYyYiBEGqsDGMvxPz1RHOlWlB3+Zdhf5oKIiY9hMNYlcWtDQ== dependencies: rfc4648 "^1.3.0" @@ -15445,6 +15774,13 @@ react-native-url-polyfill@^1.3.0: dependencies: whatwg-url-without-unicode "8.0.0-3" +react-native-url-polyfill@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589" + integrity sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA== + dependencies: + whatwg-url-without-unicode "8.0.0-3" + react-native-vector-icons@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-7.1.0.tgz#145487d617b2a81d395d2cf64e6e065fcab3a454" @@ -15467,10 +15803,10 @@ react-native-webview@^13.2.2: escape-string-regexp "2.0.0" invariant "2.2.4" -react-native-zcash@^0.6.6: - version "0.6.6" - resolved "https://registry.yarnpkg.com/react-native-zcash/-/react-native-zcash-0.6.6.tgz#956c4669b34e08501d4e24c6b37983e76505bf65" - integrity sha512-aY3R3WkJ6ONevMuUhc+BREWgEiH8Yc+yjw1jD6e1QIEICyobsXTln7Fw9xy7Za6EY7XaJ0ZFiLKrLAPttcchEw== +react-native-zcash@^0.6.7: + version "0.6.7" + resolved "https://registry.yarnpkg.com/react-native-zcash/-/react-native-zcash-0.6.7.tgz#3186230664a549fba1ae9d4f0e015b88394ba62b" + integrity sha512-CzHb+BD+xLTxqoAVqDHOSz7oLknB8Q/8WMtr/OkxVdZNQZbQb2MYf3/fiawl7zvfYo/3gtDIosOu5iIiIhva3A== dependencies: biggystring "^4.1.3" rfc4648 "^1.3.0" @@ -15682,6 +16018,18 @@ recyclerlistview@4.2.0: prop-types "15.8.1" ts-object-utils "0.0.5" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + reduce-flatten@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" @@ -16138,11 +16486,6 @@ safe-json-stringify@~1: resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== -safe-json-utils@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/safe-json-utils/-/safe-json-utils-1.1.1.tgz#0e883874467d95ab914c3f511096b89bfb3e63b1" - integrity sha512-SAJWGKDs50tAbiDXLf89PDwt9XYkWyANFWVzn4dTXl5QyI8t2o/bW5/OJl3lvc2WVU4MEpTo9Yz5NVFNsp+OJQ== - safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -16827,6 +17170,11 @@ stacktrace-parser@^0.1.3: dependencies: type-fest "^0.7.1" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -16837,6 +17185,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +std-env@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" + integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + stellar-base@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-0.9.0.tgz#03a4d71c9bfe49f0c54afd4a8f34517ed49c0924" @@ -17848,6 +18201,11 @@ ua-parser-js@^1.0.35: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== +ufo@^1.3.0, ufo@^1.3.1, ufo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.2.tgz#c7d719d0628a1c80c006d2240e0d169f6e3c0496" + integrity sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA== + uglify-es@^3.1.9: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" @@ -17883,11 +18241,27 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +uncrypto@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" + integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== + underscore@^1.12.1: version "1.13.6" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +unenv@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.9.0.tgz#469502ae85be1bd3a6aa60f810972b1a904ca312" + integrity sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g== + dependencies: + consola "^3.2.3" + defu "^6.1.3" + mime "^3.0.0" + node-fetch-native "^1.6.1" + pathe "^1.1.1" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -17971,6 +18345,32 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +unstorage@^1.9.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.10.1.tgz#bf8cc00a406e40a6293e893da9807057d95875b0" + integrity sha512-rWQvLRfZNBpF+x8D3/gda5nUCQL2PgXy2jNG4U7/Rc9BGEv9+CAJd0YyGCROUBKs9v49Hg8huw3aih5Bf5TAVw== + dependencies: + anymatch "^3.1.3" + chokidar "^3.5.3" + destr "^2.0.2" + h3 "^1.8.2" + ioredis "^5.3.2" + listhen "^1.5.5" + lru-cache "^10.0.2" + mri "^1.2.0" + node-fetch-native "^1.4.1" + ofetch "^1.3.3" + ufo "^1.3.1" + +untun@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/untun/-/untun-0.1.3.tgz#5d10dee37a3a5737ff03d158be877dae0a0e58a6" + integrity sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ== + dependencies: + citty "^0.1.5" + consola "^3.2.3" + pathe "^1.1.1" + unzipper@0.10.11: version "0.10.11" resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" @@ -18027,6 +18427,11 @@ upper-case@^1.1.1: resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= +uqr@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/uqr/-/uqr-0.1.2.tgz#5c6cd5dcff9581f9bb35b982cb89e2c483a41d7d" + integrity sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA== + uri-js@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-3.0.2.tgz#f90b858507f81dea4dcfbb3c4c3dbfa2b557faaa"