diff --git a/.env.mock b/.env.mock
index dde3c50f28..af8e5eab8b 100644
--- a/.env.mock
+++ b/.env.mock
@@ -3,3 +3,4 @@ MOCK=1
DISABLE_YELLOW_BOX=1
MOCK_SCAN_RECIPIENT=bitcoin:3HX3Q4wgYi8nKakxv7kmdCgLWJFrFgcqEt?amount=0.001
FORCE_DEBUG_VISIBLE=1
+ADJUST_APP_TOKEN=cbxft2ch7wn4
\ No newline at end of file
diff --git a/.env.production b/.env.production
index d423a454ea..21f5a20ccf 100644
--- a/.env.production
+++ b/.env.production
@@ -1,4 +1,5 @@
APP_NAME="Ledger Live"
SENTRY_DSN=https://beb25fd89630498990fd16bbc5b92fc1@sentry.io/273101
ANALYTICS_TOKEN=jfUZbw28ig8JpEi9DZpTUc21dKUKu1e3
-GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Production"
\ No newline at end of file
+GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Production"
+ADJUST_APP_TOKEN=104p56owfekg
\ No newline at end of file
diff --git a/.env.staging b/.env.staging
index f2c4a9f461..aebc23df71 100644
--- a/.env.staging
+++ b/.env.staging
@@ -1,4 +1,5 @@
APP_NAME="LL [STAGING]"
SENTRY_DSN=https://beb25fd89630498990fd16bbc5b92fc1@sentry.io/273101
ANALYTICS_TOKEN=jfUZbw28ig8JpEi9DZpTUc21dKUKu1e3
-GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Staging"
\ No newline at end of file
+GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Staging"
+ADJUST_APP_TOKEN=v88jjyrsto8w
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d27f57e3a4..e038f95b26 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -88,7 +88,11 @@ import com.android.build.OutputFile
*/
project.ext.react = [
- enableHermes: false, // clean and rebuild if changing
+ /**
+ * Clean and rebuild if changing
+ * The following env var is not read from ../.env, you need to export this var like this: `export HERMES_ENABLED_ANDROID=true`
+ */
+ enableHermes: true,
// bundleInDebug: true, // Uncomment this to debug java without having to deal with JS dev server (metro)
]
project.ext.sentryCli = [
@@ -123,9 +127,9 @@ def enableProguardInReleaseBuilds = false
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
-def jscFlavor = 'org.webkit:android-jsc-intl:+'
+// def jscFlavor = 'org.webkit:android-jsc-intl:+'
-def useIntlJsc = true
+// def useIntlJsc = true
/**
* Whether to enable the Hermes VM.
@@ -152,7 +156,7 @@ android {
multiDexEnabled true
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 36176128
+ versionCode 4195727
versionName "2.40.0"
resValue "string", "build_config_package", "com.ledger.live"
testBuildType System.getProperty('testBuildType', 'debug')
@@ -204,12 +208,6 @@ android {
matchingFallbacks = ['release']
}
}
-
- // As required by https://github.com/react-native-community/jsc-android-buildscripts#for-react-native-version-059
- packagingOptions {
- pickFirst '**/libjsc.so'
- pickFirst '**/libc++_shared.so'
- }
}
dependencies {
@@ -234,16 +232,20 @@ dependencies {
exclude group:'com.facebook.flipper'
}
- debugImplementation project(':flipper-plugin-rn-performance-android')
- if (enableHermes) {
- def hermesPath = "../../node_modules/hermes-engine/android/";
- debugImplementation files(hermesPath + "hermes-debug.aar")
- releaseImplementation files(hermesPath + "hermes-release.aar")
- } else {
- implementation jscFlavor
- }
+ def hermesPath = "../../node_modules/hermes-engine/android/";
+ debugImplementation files(hermesPath + "hermes-debug.aar")
+ stagingReleaseImplementation files(hermesPath + "hermes-release.aar")
+ releaseImplementation files(hermesPath + "hermes-release.aar")
+
androidTestImplementation('com.wix:detox:+')
+
+ compile project(':react-native-video')
+ implementation "androidx.appcompat:appcompat:1.0.0"
+
+ // Adjust
+ compile 'com.google.android.gms:play-services-analytics:10.0.1'
+ compile 'com.android.installreferrer:installreferrer:1.0'
}
// Run this once to be able to run the application with BUCK
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index 11b025724a..4c67e12770 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -8,3 +8,21 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
+
+
+# Hermes config, cf. https://reactnative.dev/docs/hermes#android
+-keep class com.facebook.hermes.unicode.** { *; }
+-keep class com.facebook.jni.** { *; }
+
+-keep class com.adjust.sdk.** { *; }
+-keep class com.google.android.gms.common.ConnectionResult {
+ int SUCCESS;
+}
+-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
+ com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
+}
+-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
+ java.lang.String getId();
+ boolean isLimitAdTrackingEnabled();
+}
+-keep public class com.android.installreferrer.** { *; }
diff --git a/android/app/src/debug/java/com/ledger/live/ReactNativeFlipper.java b/android/app/src/debug/java/com/ledger/live/ReactNativeFlipper.java
index 12f883515b..77024bc260 100644
--- a/android/app/src/debug/java/com/ledger/live/ReactNativeFlipper.java
+++ b/android/app/src/debug/java/com/ledger/live/ReactNativeFlipper.java
@@ -36,6 +36,7 @@ public static void initializeFlipper(Context context, ReactInstanceManager react
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(new RNPerfMonitorPlugin(reactInstanceManager));
client.addPlugin(CrashReporterPlugin.getInstance());
+ client.addPlugin(new RNPerfMonitorPlugin(reactInstanceManager));
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
NetworkingModule.setCustomClientBuilder(
@@ -71,4 +72,4 @@ public void run() {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 704ffd196e..33c5fa42a4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,6 +14,8 @@
+
+
diff --git a/android/app/src/main/java/com/ledger/live/MainApplication.java b/android/app/src/main/java/com/ledger/live/MainApplication.java
index 3dd89f6352..51a3445544 100644
--- a/android/app/src/main/java/com/ledger/live/MainApplication.java
+++ b/android/app/src/main/java/com/ledger/live/MainApplication.java
@@ -15,6 +15,7 @@
import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
+import com.brentvatne.react.ReactVideoPackage;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
@@ -42,6 +43,7 @@ protected List getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List packages = new PackageList(this).getPackages();
packages.add(new BluetoothHelperPackage());
+ packages.add(new ReactVideoPackage());
return packages;
}
diff --git a/android/build.gradle b/android/build.gradle
index 68d9398261..d04621d0b7 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -35,11 +35,13 @@ allprojects {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../node_modules/react-native/android")
}
- maven {
- // Android JSC is installed from npm
- url("$rootDir/../node_modules/jsc-android/dist")
+ mavenCentral {
+ // We don't want to fetch react-native from Maven Central as there are
+ // older versions over there.
+ content {
+ excludeGroup "com.facebook.react"
+ }
}
- mavenCentral()
google()
maven { url 'https://jitpack.io' }
maven {
@@ -48,6 +50,7 @@ allprojects {
maven {
url "$rootDir/../node_modules/expo-camera/android/maven"
}
+ jcenter()
}
configurations.all {
resolutionStrategy {
diff --git a/android/settings.gradle b/android/settings.gradle
index bfbba79456..ed4212a03b 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -4,10 +4,10 @@ project(':react-native-webview').projectDir = new File(rootProject.projectDir, '
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
-include ':flipper-plugin-rn-performance-android'
-project(':flipper-plugin-rn-performance-android').projectDir = new File(rootProject.projectDir, '../node_modules/flipper-plugin-rn-performance-android')
-
include ':app'
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute().text.trim(), "../scripts/autolinking.gradle")
-useExpoModules()
\ No newline at end of file
+useExpoModules()
+
+include ':react-native-video'
+project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
\ No newline at end of file
diff --git a/assets/videos/NanoX_LL0082.png b/assets/videos/NanoX_LL0082.png
new file mode 100644
index 0000000000..a9838b36a7
Binary files /dev/null and b/assets/videos/NanoX_LL0082.png differ
diff --git a/assets/videos/NanoX_LL0140.png b/assets/videos/NanoX_LL0140.png
new file mode 100644
index 0000000000..39c69e0ce6
Binary files /dev/null and b/assets/videos/NanoX_LL0140.png differ
diff --git a/assets/videos/NanoX_LL_White.mp4 b/assets/videos/NanoX_LL_White.mp4
new file mode 100644
index 0000000000..693525c538
Binary files /dev/null and b/assets/videos/NanoX_LL_White.mp4 differ
diff --git a/assets/videos/NanoX_LL_White.webm b/assets/videos/NanoX_LL_White.webm
new file mode 100644
index 0000000000..1f60d0259c
Binary files /dev/null and b/assets/videos/NanoX_LL_White.webm differ
diff --git a/assets/videos/NanoX_LL_black.mp4 b/assets/videos/NanoX_LL_black.mp4
new file mode 100644
index 0000000000..33a624407f
Binary files /dev/null and b/assets/videos/NanoX_LL_black.mp4 differ
diff --git a/assets/videos/NanoX_LL_black.webm b/assets/videos/NanoX_LL_black.webm
new file mode 100644
index 0000000000..bc2a663fc1
Binary files /dev/null and b/assets/videos/NanoX_LL_black.webm differ
diff --git a/assets/videos/ledger-card.webm b/assets/videos/ledger-card.webm
new file mode 100644
index 0000000000..dc69abe527
Binary files /dev/null and b/assets/videos/ledger-card.webm differ
diff --git a/assets/videos/nano-x.mp4 b/assets/videos/nano-x.mp4
new file mode 100644
index 0000000000..2b1b4edc35
Binary files /dev/null and b/assets/videos/nano-x.mp4 differ
diff --git a/assets/videos/onboarding.mp4 b/assets/videos/onboarding.mp4
new file mode 100644
index 0000000000..67e31e065d
Binary files /dev/null and b/assets/videos/onboarding.mp4 differ
diff --git a/babel.config.js b/babel.config.js
index 8a4543d7e8..0345aa9477 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,4 +1,7 @@
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
- plugins: ["react-native-reanimated/plugin"],
+ plugins: [
+ "react-native-reanimated/plugin",
+ "@babel/plugin-transform-named-capturing-groups-regex",
+ ],
};
diff --git a/docs/analytics.md b/docs/analytics.md
new file mode 100644
index 0000000000..7b044a0645
--- /dev/null
+++ b/docs/analytics.md
@@ -0,0 +1,44 @@
+### Analytics
+
+We use a lightweight opt-out analytics layer composed of different api and sdk.
+
+These tools are targetted towards internal contributors only or with
+
+- **_Adjust integration_** 🠒 Installs data analytics
+
+ Several dev environments are available to track installs of apps
+ Debug, Staging and Prod
+
+ In order to log events add this to your target build dot-env file
+
+ ```
+ DEBUG_ADJUST_LOGS=true
+ ```
+
+ For more details on how to work with the SDK check the adjust doc [here](https://github.com/adjust/react_native_sdk)
+
+* **_Segment integration_** 🠒 General use analytics
+
+in order to track events we use segment API with specific react API
+
+```js
+import { Track, TrackScreen } from "../analytics";
+import Button from "./Button";
+
+...
+
+
+
+...
+
+```
+
+`Track` helps track events that can be linked to a component lifecycle.
+
+`TracScreen` tracks mount events on a page with a formatted category (section of the app) and screen name.
+
+`Button` helps track click/press events with event and eventProperties props.
diff --git a/docs/linux_setup.md b/docs/linux_setup.md
new file mode 100644
index 0000000000..55bb00e459
--- /dev/null
+++ b/docs/linux_setup.md
@@ -0,0 +1,182 @@
+# Setup your dev env
+
+## Initial machine
+
+To setup your machine you only need a fresh new Ledger computer configure by IT service
+If you have authorization you can disable sentinelone with :
+```bash
+ sudo systemctl disable sentinelone
+```
+
+## Nano Support
+On Linux, you will have to setup some udev rules as explained here GitHub - LedgerHQ/udev-rules: udev rules to support Ledger devices on Linux
+
+
+## node
+install nvm
+https://github.com/nvm-sh/nvm#installing-and-updating
+```bash
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
+command -v nvm
+```
+
+find the node version used to build apps
+https://github.com/LedgerHQ/ledger-live-desktop/blob/develop/.github/workflows/bundle-app.yml
+```yml
+with:
+ node-version: 14.x
+```
+
+Then find the latest on this version and install it
+```bash
+nvm ls-remote
+nvm install x.x.x
+```
+
+You have npm installed by default with node but Live Team use yarn instead
+```bash
+npm install --global yarn
+```
+
+## Configure Git
+
+1 - ssh key
+```bash
+ssh-keygen -t ed25519 -C "firstname.lastname@ledger.com"
+ssh-add ~/.ssh/id_ed25519
+cat ~/.ssh/id_ed25519.pub
+```
+
+2 - GPG key
+```bash
+gpg --full-generate-key
+gpg --list-secret-keys --keyid-format=long
+gpg --armor --export YOUR_KEY_ID
+
+git config --global commit.gpgsign true
+```
+
+3 - Install gh (Github cli)
+https://github.com/cli/cli
+```bash
+curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
+echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
+sudo apt update
+sudo apt install gh
+```
+
+You will essentially use the command below to create PRs
+```bash
+gh pr create
+```
+
+## VSCODE
+
+install eslint extension
+and add this to your settings.json file
+
+```json
+{
+ ...
+ "editor.formatOnSave": true,
+ "eslint.format.enable": true,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": true
+ }
+}
+```
+
+## LLM
+
+####
+sudo gem install bundler
+
+## install JDK
+sudo apt-get install openjdk-11-jdk
+
+#### Android Studio
+https://developer.android.com/studio/install#linux
+
+Download latest build
+https://developer.android.com/studio
+
+Install the required libraries for 64-bit machines.
+sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386
+
+Use the below command to download from the terminal.
+
+tar -zxvf android-studio-ide-*-linux.tar.gz
+sudo mv android-studio /opt/
+
+Link the executable to /bin directory so that you can quickly start Android Studio using android-studio command.
+
+sudo ln -sf /opt/android-studio/bin/studio.sh /bin/android-studio
+
+Create a .desktop file under /usr/share/applications directory to start Android Studio from Activities menu.
+
+sudo nano /usr/share/applications/android-studio.desktop
+
+Use the following information in the above file.
+
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Android Studio
+Comment=Android Studio
+Exec=bash -i "/opt/android-studio/bin/studio.sh" %f
+Icon=/opt/android-studio/bin/studio.png
+Categories=Development;IDE;
+Terminal=false
+StartupNotify=true
+StartupWMClass=jetbrains-android-studio
+Name[en_GB]=android-studio.desktop
+
+## Before Dev
+Install watchman
+```bash
+apt-get install watchman
+```
+
+add all env vars needed to your ~/.bashrc file :
+```
+JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
+PATH=$PATH:$HOME/bin:$JAVA_HOME/bin
+
+export JAVA_HOME
+export JRE_HOME
+export PATH
+
+ANDROID_SDK_ROOT=~/Android/Sdk
+
+export ANDROID_SDK_ROOT
+export ANDROID_HOME=~/Android/Sdk
+
+export PATH=$PATH:$HOME"/Android/Sdk/platform-tools"
+
+export PATH=$PATH:$HOME"/Android/Sdk/emulator/emulator"
+
+export PATH=$PATH:$HOME"/Android/Sdk/tools"
+
+export PATH="$(yarn global bin):$PATH"
+
+export PATH=$PATH:$HOME"/watchman/linux/bin"
+
+export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"
+```
+
+#### Dev on android
+
+You have to setup your device
+- activate developper mode depending on android version and device
+- accept debug through USB
+- plug your device to you computer
+- set the link as a file transfer
+
+Then start you terminal:
+https://reactnative.dev/docs/running-on-device#method-1-using-adb-reverse-recommended-2
+```bash
+adb devices
+adb -s reverse tcp:8081 tcp:8081
+yarn android
+yarn start
+```
\ No newline at end of file
diff --git a/e2e/setups/1AccountBTC1AccountETH.json b/e2e/setups/1AccountBTC1AccountETH.json
index 70e8c7c8be..d84b7a0654 100644
--- a/e2e/setups/1AccountBTC1AccountETH.json
+++ b/e2e/setups/1AccountBTC1AccountETH.json
@@ -31,7 +31,7 @@
"discreetMode": false,
"preferredDeviceModel": "nanoS",
"hasInstalledApps": true,
- "carouselVisibility": 99999999999,
+ "carouselVisibility": {},
"hasAcceptedSwapKYC": false,
"lastSeenDevice": null,
"blacklistedTokenIds": [],
diff --git a/e2e/setups/onboardingcompleted.json b/e2e/setups/onboardingcompleted.json
index 524fe16696..14c4bdcef8 100644
--- a/e2e/setups/onboardingcompleted.json
+++ b/e2e/setups/onboardingcompleted.json
@@ -31,7 +31,7 @@
"discreetMode": false,
"preferredDeviceModel": "nanoS",
"hasInstalledApps": true,
- "carouselVisibility": 0,
+ "carouselVisibility": {},
"hasAcceptedSwapKYC": false,
"lastSeenDevice": null,
"blacklistedTokenIds": [],
diff --git a/ios/Podfile b/ios/Podfile
index 2e8472447f..59c9ab3225 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -13,7 +13,7 @@ target 'ledgerlivemobile' do
use_react_native!(
:path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods
- :hermes_enabled => false
+ :hermes_enabled => true
)
pod 'djinni_objc', :path => '../node_modules/@ledgerhq/react-native-ledger-core'
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 5f65918cc3..20047d66ee 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,12 +1,15 @@
PODS:
+ - Adjust (4.29.6):
+ - Adjust/Core (= 4.29.6)
+ - Adjust/Core (4.29.6)
- Analytics (4.1.6)
- boost (1.76.0)
- CocoaAsyncSocket (7.6.5)
- djinni_objc (4.19.1)
- DoubleConversion (1.1.6)
- - EXApplication (4.0.1):
+ - EXApplication (4.0.2):
- ExpoModulesCore
- - EXBarCodeScanner (11.1.2):
+ - EXBarCodeScanner (11.2.1):
- EXImageLoader
- ExpoModulesCore
- ZXingObjC/OneD
@@ -15,20 +18,20 @@ PODS:
- ExpoModulesCore
- EXConstants (12.1.3):
- ExpoModulesCore
- - EXErrorRecovery (3.0.4):
+ - EXErrorRecovery (3.0.5):
- ExpoModulesCore
- EXFileSystem (13.0.3):
- ExpoModulesCore
- - EXFont (10.0.4):
+ - EXFont (10.0.5):
- ExpoModulesCore
- - EXImageLoader (3.0.0):
+ - EXImageLoader (3.1.1):
- ExpoModulesCore
- React-Core
- - EXKeepAwake (10.0.1):
+ - EXKeepAwake (10.0.2):
- ExpoModulesCore
- - Expo (43.0.4):
+ - Expo (43.0.5):
- ExpoModulesCore
- - ExpoModulesCore (0.4.9):
+ - ExpoModulesCore (0.4.10):
- React-Core
- FBLazyVector (0.67.3)
- FBReactNativeSpec (0.67.3):
@@ -138,6 +141,7 @@ PODS:
- "GoogleUtilities/NSData+zlib (7.7.0)"
- GoogleUtilities/UserDefaults (7.7.0):
- GoogleUtilities/Logger
+ - hermes-engine (0.9.0)
- InputMask (6.1.0)
- ledger-core-objc (4.19.1):
- djinni_objc
@@ -176,6 +180,12 @@ PODS:
- DoubleConversion
- fmt (~> 6.2.1)
- glog
+ - RCT-Folly/Futures (2021.06.28.00-v2):
+ - boost
+ - DoubleConversion
+ - fmt (~> 6.2.1)
+ - glog
+ - libevent
- RCTRequired (0.67.3)
- RCTTypeSafety (0.67.3):
- FBLazyVector (= 0.67.3)
@@ -342,6 +352,17 @@ PODS:
- React-logger (= 0.67.3)
- React-perflogger (= 0.67.3)
- React-runtimeexecutor (= 0.67.3)
+ - React-hermes (0.67.3):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2021.06.28.00-v2)
+ - RCT-Folly/Futures (= 2021.06.28.00-v2)
+ - React-cxxreact (= 0.67.3)
+ - React-jsi (= 0.67.3)
+ - React-jsiexecutor (= 0.67.3)
+ - React-jsinspector (= 0.67.3)
+ - React-perflogger (= 0.67.3)
- React-jsi (0.67.3):
- boost (= 1.76.0)
- DoubleConversion
@@ -363,6 +384,9 @@ PODS:
- React-jsinspector (0.67.3)
- React-logger (0.67.3):
- glog
+ - react-native-adjust (4.29.6):
+ - Adjust (= 4.29.6)
+ - React
- react-native-ble-plx (2.0.3):
- MultiplatformBleAdapter (= 0.1.9)
- React-Core
@@ -372,10 +396,17 @@ PODS:
- React-Core
- react-native-fingerprint-scanner (6.0.0):
- React
+ - react-native-flipper-performance-plugin (0.2.0):
+ - React-Core
+ - react-native-flipper-performance-plugin/FBDefines (= 0.2.0)
+ - react-native-flipper-performance-plugin/FBDefines (0.2.0):
+ - React-Core
- react-native-locale (0.0.19):
- React
- react-native-netinfo (6.2.1):
- React-Core
+ - react-native-performance (2.1.0):
+ - React-Core
- react-native-randombytes (3.6.1):
- React-Core
- react-native-safe-area-context (3.3.2):
@@ -391,6 +422,11 @@ PODS:
- React-Core
- react-native-version-number (0.3.6):
- React
+ - react-native-video (5.2.0):
+ - React-Core
+ - react-native-video/Video (= 5.2.0)
+ - react-native-video/Video (5.2.0):
+ - React-Core
- react-native-webview (11.17.1):
- React-Core
- React-perflogger (0.67.3)
@@ -588,7 +624,9 @@ DEPENDENCIES:
- FlipperKit/FlipperKitUserDefaultsPlugin (= 0.99.0)
- FlipperKit/SKIOSNetworkPlugin (= 0.99.0)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
+ - hermes-engine (~> 0.9.0)
- "ledger-core-objc (from `../node_modules/@ledgerhq/react-native-ledger-core`)"
+ - libevent (~> 2.1.12)
- lottie-ios (from `../node_modules/lottie-ios`)
- lottie-react-native (from `../node_modules/lottie-react-native`)
- OpenSSL-Universal (= 1.1.180)
@@ -603,21 +641,26 @@ DEPENDENCIES:
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
+ - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
+ - react-native-adjust (from `../node_modules/react-native-adjust`)
- react-native-ble-plx (from `../node_modules/react-native-ble-plx`)
- react-native-config (from `../node_modules/react-native-config`)
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
+ - react-native-flipper-performance-plugin (from `../node_modules/react-native-flipper-performance-plugin`)
- react-native-locale (from `../node_modules/react-native-locale`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
+ - react-native-performance (from `../node_modules/react-native-performance/ios`)
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
- react-native-text-input-mask (from `../node_modules/react-native-text-input-mask`)
- react-native-udp (from `../node_modules/react-native-udp`)
- react-native-version-number (from `../node_modules/react-native-version-number`)
+ - react-native-video (from `../node_modules/react-native-video`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@@ -654,6 +697,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
+ - Adjust
- Analytics
- CocoaAsyncSocket
- Firebase
@@ -674,6 +718,7 @@ SPEC REPOS:
- fmt
- GoogleDataTransport
- GoogleUtilities
+ - hermes-engine
- InputMask
- libevent
- libwebp
@@ -746,6 +791,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/React/CoreModules"
React-cxxreact:
:path: "../node_modules/react-native/ReactCommon/cxxreact"
+ React-hermes:
+ :path: "../node_modules/react-native/ReactCommon/hermes"
React-jsi:
:path: "../node_modules/react-native/ReactCommon/jsi"
React-jsiexecutor:
@@ -754,16 +801,22 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
+ react-native-adjust:
+ :path: "../node_modules/react-native-adjust"
react-native-ble-plx:
:path: "../node_modules/react-native-ble-plx"
react-native-config:
:path: "../node_modules/react-native-config"
react-native-fingerprint-scanner:
:path: "../node_modules/react-native-fingerprint-scanner"
+ react-native-flipper-performance-plugin:
+ :path: "../node_modules/react-native-flipper-performance-plugin"
react-native-locale:
:path: "../node_modules/react-native-locale"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
+ react-native-performance:
+ :path: "../node_modules/react-native-performance/ios"
react-native-randombytes:
:path: "../node_modules/react-native-randombytes"
react-native-safe-area-context:
@@ -776,6 +829,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-udp"
react-native-version-number:
:path: "../node_modules/react-native-version-number"
+ react-native-video:
+ :path: "../node_modules/react-native-video"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-perflogger:
@@ -844,22 +899,23 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
+ Adjust: 94f1c99429fb2a7ebe530294fd66a88d63a54922
Analytics: eefe524436f904b8bb3f8c8c3425280e43b34efc
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
djinni_objc: 54ab066f337b37aceaba6d020b1e6964eead00ba
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
- EXApplication: bdc8dc27713235565da1029a34385229f31b8e08
- EXBarCodeScanner: cc450529b9c5e02dd9e2498cc0dddf153f120dfe
+ EXApplication: 54fe5bd6268d697771645e8f1aef8b806a65247a
+ EXBarCodeScanner: e5ca0062d8ad1c4c1d2e386d6a308d5a32213020
EXCamera: 03d69135ceb6f5f18f37b63eddb63d7643b65c42
EXConstants: 6d585d93723b18d7a8c283591a335609e3bc153e
- EXErrorRecovery: deea88a01d38f8b1c1181b4e1d179b0ba0e4bb5b
+ EXErrorRecovery: b0d7582714a2cc896e94a2308a356f94dbf14ef7
EXFileSystem: 99aac7962c11c680681819dd9cbca24e20e5b1e7
- EXFont: 1fb13af43dc517c01c0ff21a6e32f9f9bf2ea602
- EXImageLoader: 939451be6f7b731aaa6588920b90743f20121a4d
- EXKeepAwake: b571c2ad8323b2fced6e907766e2549f75119471
- Expo: 363a3f3c60d5a1d4f8badb29a869005487f2d9e4
- ExpoModulesCore: e41ed0b72daeac74731816ad7997d639f0115a9d
+ EXFont: 2597c10ac85a69d348d44d7873eccf5a7576ef5e
+ EXImageLoader: 347b72c2ec2df65120ccec40ea65a4c4f24317ff
+ EXKeepAwake: bf48d7f740a5cd2befed6cf9a49911d385c6c47d
+ Expo: d9588796cd19999da4d440d87bf7eb7ae4dbd608
+ ExpoModulesCore: c9438f6add0fb7b04b7c64eb97a833d2752a7834
FBLazyVector: 808f741ddb0896a20e5b98cc665f5b3413b072e2
FBReactNativeSpec: 94473205b8741b61402e8c51716dea34aa3f5b2f
Firebase: 44dd9724c84df18b486639e874f31436eaa9a20c
@@ -881,6 +937,7 @@ SPEC CHECKSUMS:
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
+ hermes-engine: bf7577d12ac6ccf53ab8b5af3c6ccf0dd8458c5c
InputMask: 71d291dc54d2deaeac6512afb6ec2304228c0bb7
ledger-core-objc: cc0290f7cf787ddf1d6a8f8e086acf55f24523a4
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
@@ -900,21 +957,26 @@ SPEC CHECKSUMS:
React-Core: 306cfdc1393bcf9481cc5de9807608db7661817b
React-CoreModules: 2576a88d630899f3fcdf2cb79fcc0454d7b2a8bb
React-cxxreact: a492f0de07d875419dcb9f463c63c22fe51c433b
+ React-hermes: 4321bcd6fce09f8c6d1be355da31e1cdae7bfac6
React-jsi: bca092b0c38d5e3fd60bb491d4994ab4a8ac2ad3
React-jsiexecutor: 15ea57ead631a11fad57634ff69f78e797113a39
React-jsinspector: 1e1e03345cf6d47779e2061d679d0a87d9ae73d8
React-logger: 1e10789cb84f99288479ba5f20822ce43ced6ffe
+ react-native-adjust: 8152efdf7a2e94a85ce427f2096b4167f2824273
react-native-ble-plx: f10240444452dfb2d2a13a0e4f58d7783e92d76e
react-native-config: 72d948053a442779b3178fddd571e37f118ef606
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
+ react-native-flipper-performance-plugin: fccda3ebbcee11cda4e5dee85b335835e9c7e714
react-native-locale: bd8edf0e51706d469af3e2fa568a8102213a3139
react-native-netinfo: 3d3769f0d65de15c83a9bf1346f8be71de5a24bf
+ react-native-performance: f4b6604a9d5a8a7407e34a82fab6c641d9a3ec12
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-text-input-mask: 36a546b378fadd2efe1b7484a859d34bc2c80395
react-native-udp: f0fbf5386c45af5523dc143a86afe2cbf29f764e
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
+ react-native-video: a4c2635d0802f983594b7057e1bce8f442f0ad28
react-native-webview: 162b6453d074e0b1c7025242bb7a939b6f72b9e7
React-perflogger: 93d3f142d6d9a46e635f09ba0518027215a41098
React-RCTActionSheet: 87327c3722203cc79cf79d02fb83e7332aeedd18
@@ -954,6 +1016,6 @@ SPEC CHECKSUMS:
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
-PODFILE CHECKSUM: 60bbf8c5e3f2024a8e68e975181c461b2bf72a8e
+PODFILE CHECKSUM: c09c7e3d7b5d366d20e032f4d54708fe37de1e94
COCOAPODS: 1.11.2
diff --git a/ios/ledgerlivemobile.xcodeproj/project.pbxproj b/ios/ledgerlivemobile.xcodeproj/project.pbxproj
index ab3ce4ae33..a0226c2b60 100644
--- a/ios/ledgerlivemobile.xcodeproj/project.pbxproj
+++ b/ios/ledgerlivemobile.xcodeproj/project.pbxproj
@@ -575,12 +575,14 @@
"${PODS_ROOT}/../../node_modules/@ledgerhq/react-native-ledger-core/ios/Frameworks/universal/ledger-core.framework",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ledger-core.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -902,7 +904,7 @@
DEVELOPMENT_TEAM = 5HK2Q4J4X4;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
- "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -1047,7 +1049,7 @@
DEVELOPMENT_TEAM = 5HK2Q4J4X4;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
- "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -1114,7 +1116,7 @@
DEVELOPMENT_TEAM = 5HK2Q4J4X4;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
- "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
diff --git a/metro.config.js b/metro.config.js
index 2326153df7..9ccb6cf622 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -5,11 +5,12 @@
* @format
*/
-const defaultSourceExts = require("metro-config/src/defaults/defaults")
- .sourceExts;
-const resolve = require("metro-resolver").resolve
+/* eslint-disable no-console */
module.exports = {
+ resolver: {
+ sourceExts: ["tsx", "ts", "jsx", "js", "json", "cjs"],
+ },
transformer: {
getTransformOptions: async () => ({
transform: {
@@ -18,7 +19,64 @@ module.exports = {
},
}),
},
- resolver: {
- sourceExts: [...defaultSourceExts, 'cjs']
- }
};
+
+const getIntlPolyfills = () => {
+ const regions = require("./src/screens/Settings/General/regions.json");
+ const regionsKeys = Object.keys(regions);
+ const polyfills = [
+ "@formatjs/intl-getcanonicallocales/polyfill",
+ "@formatjs/intl-locale/polyfill",
+ ];
+ [
+ {
+ prefix: "@formatjs/intl-pluralrules",
+ supportedLocales: require("@formatjs/intl-pluralrules/supported-locales")
+ .supportedLocales,
+ },
+ {
+ prefix: "@formatjs/intl-numberformat",
+ supportedLocales: require("@formatjs/intl-numberformat/supported-locales")
+ .supportedLocales,
+ },
+ {
+ prefix: "@formatjs/intl-datetimeformat",
+ supportedLocales: require("@formatjs/intl-datetimeformat/supported-locales")
+ .supportedLocales,
+ },
+ ].forEach(({ prefix, supportedLocales }) => {
+ polyfills.push(`${prefix}/polyfill`);
+ supportedLocales
+ .filter(
+ k => k !== "haw",
+ ) /* this locale crashes because the locale data is in the wrong format https://github.com/formatjs/formatjs/issues/3503 */
+ .forEach(supportedLocale => {
+ if (
+ regionsKeys.find(
+ regionLocale =>
+ regionLocale === supportedLocale ||
+ (supportedLocale.split("-").length === 1 &&
+ regionLocale.startsWith(supportedLocale)),
+ )
+ )
+ polyfills.push(`${prefix}/locale-data/${supportedLocale}`);
+ });
+ });
+ polyfills.push("@formatjs/intl-datetimeformat/add-all-tz");
+ return polyfills;
+};
+
+const fs = require("fs");
+const path = require("path");
+
+fs.writeFile(
+ path.resolve("./src/generated/intlPolyfills.js"),
+ `/** file generated in metro.config.js */
+${getIntlPolyfills()
+ .map(str => `import "${str}";`)
+ .join("\n")}\n`,
+ err => {
+ if (err) throw err;
+ else console.log("Intl polyfill imports generated");
+ },
+);
diff --git a/package.json b/package.json
index f2228308f0..e1b428aad2 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"start": "react-native start",
"ios": "react-native run-ios",
"android": "react-native run-android --appIdSuffix=debug",
+ "android:hermes": "export HERMES_ENABLED_ANDROID=true && yarn android",
"detox": "./node_modules/.bin/detox",
"e2e:build": "yarn detox build",
"e2e:test": "yarn detox test",
@@ -39,6 +40,7 @@
"android:ci:playstore": "bundle exec fastlane android ci_playstore --env production",
"preandroid:ci:nightly": "bundle install",
"android:ci:nightly": "bundle exec fastlane android ci_nightly --env production",
+ "android:hermes:staging": "export HERMES_ENABLED_ANDROID=true && yarn android:staging",
"android:staging": "cd android && ./gradlew assembleStagingRelease",
"android:install": "./scripts/install-and-run-apk.sh",
"android:clean": "$ANDROID_HOME/platform-tools/adb shell pm clear com.ledger.live",
@@ -62,13 +64,19 @@
}
},
"dependencies": {
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8",
+ "@formatjs/intl-datetimeformat": "^5.0.0",
+ "@formatjs/intl-getcanonicallocales": "^1.9.1",
+ "@formatjs/intl-locale": "^2.4.46",
+ "@formatjs/intl-numberformat": "^7.4.2",
+ "@formatjs/intl-pluralrules": "^4.3.2",
"@ledgerhq/devices": "6.24.1",
"@ledgerhq/errors": "6.10.0",
"@ledgerhq/hw-transport": "6.24.1",
"@ledgerhq/hw-transport-http": "6.26.0",
- "@ledgerhq/live-common": "https://github.com/LedgerHQ/ledger-live-common.git#develop",
+ "@ledgerhq/live-common": "git+https://github.com/LedgerHQ/ledger-live-common.git#feat/LL-7742-marketprovider-updated",
"@ledgerhq/logs": "6.10.0",
- "@ledgerhq/native-ui": "^0.7.0",
+ "@ledgerhq/native-ui": "^0.7.6",
"@ledgerhq/react-native-hid": "6.24.1",
"@ledgerhq/react-native-hw-transport-ble": "6.25.1",
"@ledgerhq/react-native-ledger-core": "4.19.1",
@@ -108,7 +116,7 @@
"eip55": "^1.0.3",
"events": "^3.2.0",
"expo": "^43.0.1",
- "expo-barcode-scanner": "~11.1.2",
+ "expo-barcode-scanner": "~11.2.0",
"expo-camera": "~12.0.3",
"fuse.js": "^6.4.6",
"hoist-non-react-statics": "3.3.2",
@@ -132,6 +140,7 @@
"react": "17.0.2",
"react-i18next": "11.12.0",
"react-native": "0.67.3",
+ "react-native-adjust": "^4.29.6",
"react-native-android-location-services-dialog-box": "^2.8.2",
"react-native-animatable": "^1.3.3",
"react-native-ble-plx": "2.0.3",
@@ -148,6 +157,7 @@
"react-native-modal": "^13.0.0",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-os": "^1.2.6",
+ "react-native-performance": "^2.1.0",
"react-native-progress": "^4.1.2",
"react-native-qrcode-svg": "6.1.1",
"react-native-randombytes": "^3.6.1",
@@ -168,6 +178,7 @@
"react-native-url-polyfill": "^1.3.0",
"react-native-vector-icons": "^8.1.0",
"react-native-version-number": "^0.3.6",
+ "react-native-video": "^5.2.0",
"react-native-webview": "^11.17.1",
"react-redux": "7.2.6",
"readable-stream": "3.6.0",
@@ -193,12 +204,14 @@
"@babel/core": "^7.12.9",
"@babel/runtime": "^7.12.5",
"@react-native-community/eslint-config": "^2.0.0",
+ "@types/invariant": "^2.2.35",
"@types/jest": "^27.0.2",
"@types/react": "^17.0.30",
"@types/react-native": "^0.65.8",
"@types/react-test-renderer": "^17.0.1",
"babel-jest": "^26.6.3",
- "detox": "^19.4.3",
+ "babel-plugin-module-resolver": "^4.1.0",
+ "detox": "^18.2.1",
"eslint": "7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
@@ -223,6 +236,7 @@
"patch-package": "^6.4.7",
"prettier": "^1.19.1",
"react-native-debugger-open": "^0.3.25",
+ "react-native-flipper-performance-plugin": "^0.2.0",
"rn-nodeify": "10.3.0",
"typescript": "^4.4.4",
"ws": "^7.5.2",
diff --git a/react-native.config.js b/react-native.config.js
index af974dfbe1..5ec597e866 100644
--- a/react-native.config.js
+++ b/react-native.config.js
@@ -4,5 +4,6 @@ module.exports = {
"./assets/fonts/",
"node_modules/@ledgerhq/native-ui/assets/fonts/alpha",
"node_modules/@ledgerhq/native-ui/assets/fonts/inter",
+ "./assets/videos/",
],
};
diff --git a/scripts/sync-families-dispatch.sh b/scripts/sync-families-dispatch.sh
index 395e6a3a2f..8599e1c477 100755
--- a/scripts/sync-families-dispatch.sh
+++ b/scripts/sync-families-dispatch.sh
@@ -4,18 +4,18 @@ set -e
cd $(dirname $0)
targets="\
-operationDetails.js \
-accountActions.js \
-TransactionConfirmFields.js \
-AccountHeader.js \
-AccountBodyHeader.js \
-AccountSubHeader.js \
-SendAmountFields.js \
-screens.js \
-SendRowsCustom.js \
-SendRowsFee.js \
-AccountBalanceSummaryFooter.js \
-SubAccountList.js \
+operationDetails \
+accountActions \
+TransactionConfirmFields \
+AccountHeader \
+AccountBodyHeader \
+AccountSubHeader \
+SendAmountFields \
+screens \
+SendRowsCustom \
+SendRowsFee \
+AccountBalanceSummaryFooter \
+SubAccountList \
"
cd ../src
@@ -27,7 +27,7 @@ genTarget () {
t=$1
echo '// @flow'
for family in $families; do
- if [ -f $family/$t ]; then
+ if [ -f "$family/$t".js ]; then
echo -n 'import '$family' from "'
OIFS=$IFS
IFS="/"
@@ -42,7 +42,7 @@ genTarget () {
echo
echo 'export default {'
for family in $families; do
- if [ -f $family/$t ]; then
+ if [ -f "$family/$t".js ]; then
echo ' '$family','
fi
done
diff --git a/src/actions/settings.js b/src/actions/settings.js
index 16d6284c81..b0ee24afee 100755
--- a/src/actions/settings.js
+++ b/src/actions/settings.js
@@ -133,9 +133,9 @@ export const dismissBanner = (bannerId: string) => ({
payload: bannerId,
});
-export const setCarouselVisibility = (nonce: number) => ({
+export const setCarouselVisibility = (cardsVisibility: any) => ({
type: "SETTINGS_SET_CAROUSEL_VISIBILITY",
- payload: nonce,
+ payload: cardsVisibility,
});
export const setAvailableUpdate = (enabled: boolean) => ({
diff --git a/src/analytics/TrackScreen.tsx b/src/analytics/TrackScreen.tsx
new file mode 100644
index 0000000000..0edb6779cc
--- /dev/null
+++ b/src/analytics/TrackScreen.tsx
@@ -0,0 +1,28 @@
+// @flow
+import { useEffect, useRef } from "react";
+import { useIsFocused } from "@react-navigation/native";
+import { screen } from "./segment";
+
+type Props = Partial<{
+ [key: string]: any;
+}> & {
+ category: string;
+ name?: string;
+};
+
+export default function TrackScreen({ category, name, ...props }: Props) {
+ const isFocused = useIsFocused();
+ const isFocusedRef = useRef();
+
+ useEffect(() => {
+ if (isFocusedRef.current !== isFocused) {
+ isFocusedRef.current = isFocused;
+
+ if (isFocusedRef.current) {
+ screen(category, name, props);
+ }
+ }
+ }, [category, name, props, isFocused]);
+
+ return null;
+}
diff --git a/src/colors.tsx b/src/colors.tsx
new file mode 100644
index 0000000000..e1c1596d3b
--- /dev/null
+++ b/src/colors.tsx
@@ -0,0 +1,153 @@
+import React from "react";
+import color from "color";
+import { useTheme } from "@react-navigation/native";
+
+export const ensureContrast = (color1: string, color2: string) => {
+ const colorL1 = color(color1).luminosity() + 0.05;
+ const colorL2 = color(color2).luminosity() + 0.05;
+
+ const lRatio = colorL1 > colorL2 ? colorL1 / colorL2 : colorL2 / colorL1;
+
+ if (lRatio < 1.5) {
+ return color(color1)
+ .rotate(180)
+ .negate()
+ .string();
+ }
+ return color1;
+};
+
+export const rgba = (c: string, a: number) =>
+ color(c)
+ .alpha(a)
+ .rgb()
+ .toString();
+
+export const darken = (c: string, a: number) =>
+ color(c)
+ .darken(a)
+ .toString();
+
+export const lighten = (c: string, a: number) =>
+ color(c)
+ .lighten(a)
+ .toString();
+
+export function withTheme(Component: React.ElementType) {
+ return (props: any) => {
+ const { colors } = useTheme();
+ return ;
+ };
+}
+
+export const lightTheme = {
+ dark: false,
+ colors: {
+ primary: "rgb(255, 45, 85)",
+ background: "hsla(0, 0%, 100%, 1)",
+ card: "#FFFFFF",
+ text: "rgb(28, 28, 30)",
+ border: "rgb(199, 199, 204)",
+ notification: "rgb(255, 69, 58)",
+ contrastBackground: "#142533",
+ contrastBackgroundText: "#ffffff",
+ /* MAIN */
+ live: "#6490f1",
+ alert: "#ea2e49",
+ success: "#66BE54",
+ darkBlue: "#142533",
+ smoke: "#666666",
+ grey: "#999999",
+ fog: "#D8D8D8",
+ white: "#ffffff",
+ green: "rgb(102, 190, 84)",
+ ledgerGreen: "#41ccb4",
+ black: "#000000",
+ orange: "#ff7701",
+ yellow: "#ffd24a",
+ separator: "#ebebeb",
+ warning: "#ff9900",
+ darkWarning: "#E08700",
+
+ /* DERIVATIVES */
+ lightLive: "#4b84ff19",
+ lightAlert: "#ea2e490c",
+ lightFog: "#EEEEEE",
+ lightGrey: "#F9F9F9",
+ lightOrange: "#FF984F",
+ translucentGreen: "rgba(102, 190, 84, 0.2)",
+ translucentGrey: "rgba(153, 153, 153, 0.2)",
+ lightLiveBg: "#eef0ff",
+
+ errorBg: "#ff0042",
+
+ /* PILLS */
+ pillForeground: "#999999",
+ pillActiveBackground: rgba("#6490f1", 0.1),
+ pillActiveForeground: "#6490f1",
+ pillActiveDisabledForeground: "#999999",
+
+ /** SNACKBAR */
+ snackBarBg: "#142533",
+ snackBarColor: "#FFF",
+
+ /** SKELETON */
+ skeletonBg: "#E9EAEB",
+ },
+};
+
+export const darkTheme = {
+ dark: true,
+ colors: {
+ primary: "rgb(255, 45, 85)",
+ card: "#1C1D1F",
+ background: "hsla(0, 0%, 10%, 1)",
+ text: "#FFFFFF",
+ border: "rgba(255, 255, 255, 0.1)",
+ notification: "rgb(255, 69, 58)",
+ contrastBackground: "#223544",
+ contrastBackgroundText: "#ffffff",
+ /* MAIN */
+ live: "#6490f1",
+ alert: "#ea2e49",
+ success: "#66BE54",
+ darkBlue: "#FAFAFA",
+ smoke: "#aaa",
+ grey: "#aaa",
+ fog: "#A8A8A8",
+ white: "#000000",
+ green: "rgb(102, 190, 84)",
+ ledgerGreen: "#41ccb4",
+ black: "#FFFFFF",
+ orange: "#ff7701",
+ yellow: "#ffd24a",
+ separator: "#ebebeb",
+ warning: "#ff9900",
+ darkWarning: "#E08700",
+
+ /* DERIVATIVES */
+ lightLive: "#4b84ff19",
+ lightAlert: "#ea2e490c",
+ lightFog: "#1c202b",
+ lightGrey: "rgba(255,255,255, 0.05)",
+ lightOrange: "#FF984F",
+ translucentGreen: "rgba(102, 190, 84, 0.2)",
+ translucentGrey: "rgba(153, 153, 153, 0.2)",
+ lightLiveBg: "#222635",
+
+ errorBg: "#ff0042",
+
+ /* PILLS */
+ pillForeground: "#999999",
+ pillActiveBackground: rgba("#6490f1", 0.1),
+ pillActiveForeground: "#6490f1",
+ pillActiveDisabledForeground: "#999999",
+
+ /** SNACKBAR */
+ snackBarBg: "#000000",
+ snackBarColor: "#FFF",
+
+ /** SKELETON */
+ skeletonBg: "#2a2d33",
+ },
+};
diff --git a/src/components/AccountCard.tsx b/src/components/AccountCard.tsx
new file mode 100644
index 0000000000..3c3759b280
--- /dev/null
+++ b/src/components/AccountCard.tsx
@@ -0,0 +1,100 @@
+import React, { ReactNode } from "react";
+import {
+ getAccountName,
+ getAccountSpendableBalance,
+} from "@ledgerhq/live-common/lib/account";
+import {
+ getAccountCurrency,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account/helpers";
+import { getTagDerivationMode } from "@ledgerhq/live-common/lib/derivation";
+import { Account, CryptoCurrency } from "@ledgerhq/live-common/lib/types";
+import { Flex, Tag, Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import { TouchableOpacity } from "react-native-gesture-handler";
+
+import Card, { Props as CardProps } from "./Card";
+import CurrencyIcon from "./CurrencyIcon";
+import CurrencyUnitValue from "./CurrencyUnitValue";
+
+export type Props = CardProps & {
+ account: Account;
+ style?: any;
+ disabled?: boolean;
+ useFullBalance?: boolean;
+ AccountSubTitle?: ReactNode;
+};
+
+const AccountCard = ({
+ onPress,
+ account,
+ style,
+ disabled,
+ useFullBalance,
+ AccountSubTitle,
+ ...props
+}: Props) => {
+ const { colors } = useTheme();
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+ const tag =
+ account.derivationMode !== undefined &&
+ account.derivationMode !== null &&
+ getTagDerivationMode(currency as CryptoCurrency, account.derivationMode);
+
+ return (
+
+
+
+
+
+
+ {getAccountName(account)}
+
+ {AccountSubTitle}
+
+ {tag && {tag}}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AccountCard;
diff --git a/src/components/AccountDelegationInfo.js b/src/components/AccountDelegationInfo.js
index c0dd8d9f01..fc2ae92ef6 100644
--- a/src/components/AccountDelegationInfo.js
+++ b/src/components/AccountDelegationInfo.js
@@ -34,7 +34,7 @@ export default function AccountDelegationInfo({
style={[
styles.container,
{
- backgroundColor: colors.card,
+ backgroundColor: colors.background,
},
]}
>
diff --git a/src/components/AccountGraphCard.tsx b/src/components/AccountGraphCard.tsx
new file mode 100644
index 0000000000..8209c29ed8
--- /dev/null
+++ b/src/components/AccountGraphCard.tsx
@@ -0,0 +1,280 @@
+import React, { useState, useCallback, useMemo, ReactNode, memo } from "react";
+import { useTheme } from "styled-components/native";
+import { Unit, Currency, AccountLike } from "@ledgerhq/live-common/lib/types";
+import {
+ getAccountCurrency,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account";
+import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+import {
+ ValueChange,
+ PortfolioRange,
+ BalanceHistoryWithCountervalue,
+} from "@ledgerhq/live-common/lib/portfolio/v2/types";
+import { Box, Flex, Text, ChartCard } from "@ledgerhq/native-ui";
+
+import { useTranslation } from "react-i18next";
+import { rangeDataTable } from "@ledgerhq/live-common/lib/market/utils/rangeDataTable";
+import { useSelector } from "react-redux";
+import { ensureContrast } from "../colors";
+import { useTimeRange } from "../actions/settings";
+import Delta from "./Delta";
+import FormatDate from "./FormatDate";
+import CurrencyUnitValue from "./CurrencyUnitValue";
+import Placeholder from "./Placeholder";
+import { Item } from "./Graph/types";
+import CurrencyRate from "./CurrencyRate";
+import { useBalanceHistoryWithCountervalue } from "../actions/portfolio";
+import { counterValueFormatter } from "../screens/Market/utils";
+import { localeSelector } from "../reducers/settings";
+
+type HeaderProps = {
+ account: AccountLike;
+ isAvailable: boolean;
+ history: BalanceHistoryWithCountervalue;
+ unit: Unit;
+ counterValueCurrency: Currency;
+ useCounterValue?: boolean;
+ countervalueChange: ValueChange;
+};
+
+const Header = ({
+ account,
+ isAvailable,
+ history,
+ unit,
+ counterValueCurrency,
+ useCounterValue,
+ countervalueChange,
+}: HeaderProps) => (
+
+);
+
+type FooterProps = {
+ renderAccountSummary: () => ReactNode;
+};
+
+const Footer = ({ renderAccountSummary }: FooterProps) => {
+ const accountSummary = renderAccountSummary && renderAccountSummary();
+ return accountSummary ? (
+
+ {accountSummary}
+
+ ) : null;
+};
+
+type Props = {
+ account: AccountLike;
+ range: PortfolioRange;
+ history: BalanceHistoryWithCountervalue;
+ valueChange: ValueChange;
+ countervalueAvailable: boolean;
+ counterValueCurrency: Currency;
+ useCounterValue?: boolean;
+ renderAccountSummary: () => ReactNode;
+};
+
+const timeRangeMapped: any = {
+ "24h": "day",
+ "7d": "week",
+ "30d": "month",
+ "1y": "year",
+};
+
+function AccountGraphCard({
+ account,
+ countervalueAvailable,
+ history,
+ counterValueCurrency,
+ useCounterValue,
+ renderAccountSummary,
+}: Props) {
+ const { colors } = useTheme();
+ const locale = useSelector(localeSelector);
+ const { t } = useTranslation();
+
+ const [rangeRequest, setRangeRequest] = useState("24h");
+ const [timeRange, setTimeRange] = useTimeRange();
+ const { countervalueChange } = useBalanceHistoryWithCountervalue({
+ account,
+ range: timeRange,
+ });
+
+ const ranges = useMemo(
+ () =>
+ Object.keys(rangeDataTable)
+ .filter(key => key !== "1h")
+ .map(r => ({ label: t(`market.range.${r}`), value: r })),
+ [t],
+ );
+
+ const timeFormat = useMemo(() => {
+ switch (rangeRequest) {
+ case "24h":
+ return { hour: "numeric", minute: "numeric" };
+ case "7d":
+ return { weekday: "short" };
+ case "30d":
+ return { month: "short", day: "numeric" };
+ default:
+ return { month: "short" };
+ }
+ }, [rangeRequest]);
+
+ const isAvailable = !useCounterValue || countervalueAvailable;
+
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+ const graphColor = ensureContrast(
+ getCurrencyColor(currency),
+ colors.neutral.c30,
+ );
+
+ const refreshChart = useCallback(
+ request => {
+ if (request && request.range && timeRangeMapped[request.range]) {
+ const { range } = request;
+ setTimeRange(timeRangeMapped[range]);
+ setRangeRequest(range);
+ }
+ },
+ [setTimeRange, setRangeRequest],
+ );
+
+ const dataFormatted = useMemo(() => {
+ const counterValueCurrencyMagnitude =
+ 10 ** counterValueCurrency.units[0].magnitude;
+ return history
+ ? history.map(d => ({
+ date: d.date,
+ value: d.countervalue / counterValueCurrencyMagnitude,
+ }))
+ : [];
+ }, [history, counterValueCurrency]);
+
+ const xAxisFormatter = useCallback(
+ (timestamp: number) =>
+ new Intl.DateTimeFormat(locale, timeFormat).format(timestamp),
+ [locale, timeFormat],
+ );
+
+ return (
+
+ }
+ Footer={}
+ range={rangeRequest}
+ refreshChart={refreshChart}
+ chartData={dataFormatted}
+ currencyColor={graphColor}
+ xAxisFormatter={xAxisFormatter}
+ yAxisFormatter={(value: number) =>
+ counterValueFormatter({
+ value,
+ shorten: true,
+ locale,
+ allowZeroValue: true,
+ t,
+ })
+ }
+ valueFormatter={(value: number) =>
+ counterValueFormatter({
+ value,
+ currency: counterValueCurrency.ticker,
+ locale,
+ allowZeroValue: true,
+ t,
+ })
+ }
+ />
+ );
+}
+
+function GraphCardHeader({
+ counterValueUnit,
+ to,
+ hoveredItem,
+ isLoading,
+ valueChange,
+ account,
+}: {
+ account: AccountLike;
+ isLoading: boolean;
+ cryptoCurrencyUnit: Unit;
+ counterValueUnit: Unit;
+ to: Item;
+ hoveredItem?: Item;
+ useCounterValue?: boolean;
+ valueChange: ValueChange;
+}) {
+ const currency = getAccountCurrency(account);
+ const item = hoveredItem || to;
+
+ return (
+
+
+ {hoveredItem ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {isLoading ? (
+
+ ) : hoveredItem && hoveredItem.date ? (
+
+
+
+ ) : valueChange ? (
+
+ ) : null}
+
+
+ );
+}
+
+export default memo(AccountGraphCard);
diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx
new file mode 100644
index 0000000000..cd4c4b8677
--- /dev/null
+++ b/src/components/AccountList.tsx
@@ -0,0 +1,91 @@
+import React from "react";
+import { FlatList } from "react-native";
+import { useTheme } from "styled-components/native";
+import { AccountLike } from "@ledgerhq/live-common/lib/types";
+import { Flex, Link } from "@ledgerhq/native-ui";
+import { PlusMedium } from "@ledgerhq/native-ui/assets/icons";
+import { Trans } from "react-i18next";
+
+import { SearchResult } from "../helpers/formatAccountSearchResults";
+import AccountCard from "./AccountCard";
+
+type Props = {
+ list: SearchResult[];
+ showAddAccount?: boolean;
+ onPress: (account: AccountLike) => void;
+ onAddAccount?: () => void;
+};
+
+const AccountList = ({
+ list,
+ showAddAccount,
+ onPress,
+ onAddAccount,
+}: Props): JSX.Element => {
+ const keyExtractor = (item: SearchResult) => item.account.id;
+ const { colors } = useTheme();
+
+ const renderItem = ({ item: result }: { item: SearchResult }) => {
+ const { account } = result;
+
+ return (
+
+
+ onPress(account)}
+ py={2}
+ />
+
+ );
+ };
+
+ const renderFooter = () => (
+
+ {showAddAccount && (
+ (
+
+
+
+ )}
+ >
+
+
+ )}
+
+ );
+
+ return (
+
+ );
+};
+
+export default AccountList;
diff --git a/src/components/AccountSectionLabel.tsx b/src/components/AccountSectionLabel.tsx
new file mode 100644
index 0000000000..0d89cbd739
--- /dev/null
+++ b/src/components/AccountSectionLabel.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { View, TouchableOpacity, StyleSheet } from "react-native";
+import { Box, Flex, Text } from "@ledgerhq/native-ui";
+
+type Props = {
+ name: string;
+ icon?: React.ReactNode;
+ onPress?: () => void;
+ RightComponent?: React.ReactNode;
+};
+
+export default function AccountSectionLabel({
+ name,
+ icon,
+ onPress,
+ RightComponent,
+}: Props) {
+ return (
+
+
+
+ {name}
+
+
+ {icon}
+
+
+ {!!RightComponent && (
+ {RightComponent}
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ },
+ rightWrapper: {
+ alignSelf: "flex-end",
+ },
+});
diff --git a/src/components/AccountSelector.tsx b/src/components/AccountSelector.tsx
new file mode 100644
index 0000000000..3420e82218
--- /dev/null
+++ b/src/components/AccountSelector.tsx
@@ -0,0 +1,71 @@
+import React, { useCallback } from "react";
+import { useSelector } from "react-redux";
+import { AccountLike } from "@ledgerhq/live-common/lib/types";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import { Trans } from "react-i18next";
+import { useTheme } from "styled-components/native";
+
+import AccountList from "./AccountList";
+import FilteredSearchBar from "../components/FilteredSearchBar";
+import { formatSearchResults } from "../helpers/formatAccountSearchResults";
+import { accountsSelector } from "../reducers/accounts";
+
+const SEARCH_KEYS = ["name", "unit.code", "token.name", "token.ticker"];
+
+type Props = {
+ list: AccountLike[];
+ onSelectAccount: (account: AccountLike) => void;
+ showAddAccount?: boolean;
+ onAddAccount?: () => void;
+ initialCurrencySelected?: string;
+};
+
+const AccountSelector = ({
+ list,
+ onSelectAccount,
+ showAddAccount,
+ onAddAccount,
+ initialCurrencySelected,
+}: Props) => {
+ const { colors } = useTheme();
+ const accounts = useSelector(accountsSelector);
+
+ const renderList = useCallback(
+ items => {
+ const formatedList = formatSearchResults(items, accounts);
+
+ return (
+
+ );
+ },
+ [showAddAccount, onSelectAccount, onAddAccount, accounts],
+ );
+
+ return (
+ (
+
+
+
+
+
+ )}
+ />
+ );
+};
+
+export default AccountSelector;
diff --git a/src/components/AdjustProvider.tsx b/src/components/AdjustProvider.tsx
new file mode 100644
index 0000000000..4c85ebbb76
--- /dev/null
+++ b/src/components/AdjustProvider.tsx
@@ -0,0 +1,54 @@
+import React, { useEffect } from "react";
+
+import {
+ Adjust,
+ AdjustEventTrackingSuccess,
+ AdjustEventTrackingFailure,
+ AdjustConfig,
+} from "react-native-adjust";
+import Config from "react-native-config";
+import { useSelector } from "react-redux";
+import { analyticsEnabledSelector } from "../reducers/settings";
+
+export default function AdjustProvider() {
+ const analyticsEnabled: boolean = useSelector(analyticsEnabledSelector);
+
+ useEffect(() => {
+ const adjustConfig = new AdjustConfig(
+ Config.ADJUST_APP_TOKEN,
+ __DEV__
+ ? AdjustConfig.EnvironmentSandbox
+ : AdjustConfig.EnvironmentSandbox, // @TODO: Change to Production when ready
+ );
+ adjustConfig.setDelayStart(4.2);
+ if (__DEV__) {
+ adjustConfig.setLogLevel(AdjustConfig.LogLevelDebug);
+ }
+ if (Config.DEBUG_ADJUST_LOGS) {
+ adjustConfig.setEventTrackingSucceededCallbackListener(
+ (eventSuccess: AdjustEventTrackingSuccess) => {
+ // Printing all event success properties.
+ console.warn("Event tracking succeeded!", eventSuccess);
+ },
+ );
+
+ adjustConfig.setEventTrackingFailedCallbackListener(
+ (eventFailure: AdjustEventTrackingFailure) => {
+ // Printing all event failure properties.
+ console.error("Event tracking failed!", eventFailure);
+ },
+ );
+ }
+ Adjust.create(adjustConfig);
+
+ return () => {
+ Adjust.componentWillUnmount();
+ };
+ }, []);
+
+ useEffect(() => {
+ Adjust.setEnabled(analyticsEnabled);
+ }, [analyticsEnabled]);
+
+ return null;
+}
diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx
new file mode 100644
index 0000000000..28f3d481ab
--- /dev/null
+++ b/src/components/Alert.tsx
@@ -0,0 +1,207 @@
+import React, { useCallback, useMemo } from "react";
+import { Trans } from "react-i18next";
+import { Linking, TouchableOpacity } from "react-native";
+import { useSelector } from "react-redux";
+import styled from "styled-components/native";
+import { Icons, Text, Alert as BaseAlert, Flex } from "@ledgerhq/native-ui";
+import { AlertProps as BaseAlertProps } from "@ledgerhq/native-ui/components/message/Alert";
+import { dismissedBannersSelector } from "../reducers/settings";
+
+type AlertType =
+ | "primary"
+ | "secondary"
+ | "success"
+ | "warning"
+ | "error"
+ | "hint"
+ | "security"
+ | "help"
+ | "danger"
+ | "update";
+
+type IconType = React.ComponentType<{ size: number; color: string }>;
+
+type Props = {
+ id?: string;
+ type: AlertType;
+ children: React.ReactNode;
+ title?: string;
+ noIcon?: boolean;
+ onLearnMore?: () => any;
+ learnMoreKey?: string;
+ learnMoreUrl?: string;
+ learnMoreIsInternal?: boolean;
+ learnMoreIcon?: IconType;
+};
+
+const alertPropsByType: Record<
+ AlertType,
+ {
+ type: BaseAlertProps["type"];
+ Icon: BaseAlertProps["Icon"];
+ }
+> = {
+ primary: {
+ type: "info",
+ Icon: Icons.InfoMedium,
+ },
+ secondary: {
+ type: "info",
+ Icon: Icons.InfoMedium,
+ },
+ success: {
+ type: "info",
+ Icon: Icons.CircledCheckMedium,
+ },
+ warning: {
+ type: "warning",
+ Icon: Icons.CircledAlertMedium,
+ },
+ error: {
+ type: "error",
+ Icon: Icons.CircledCrossMedium,
+ },
+ hint: {
+ type: "info",
+ Icon: Icons.LightbulbMedium,
+ },
+ security: {
+ type: "warning",
+ Icon: Icons.ShieldSecurityMedium,
+ },
+ help: {
+ type: "info",
+ Icon: Icons.ShieldSecurityMedium,
+ },
+ danger: {
+ type: "error",
+ Icon: Icons.ShieldSecurityMedium,
+ },
+ update: {
+ type: "warning",
+ Icon: Icons.WarningMedium,
+ },
+};
+
+type LearnMoreLinkProps = {
+ color: string;
+ onPress?: () => void;
+ learnMoreIsInternal?: boolean;
+ learnMoreKey?: string;
+ Icon?: IconType;
+};
+
+const StyledText = styled(Text).attrs({
+ variant: "bodyLineHeight",
+ fontWeight: "medium",
+})``;
+
+const UnderlinedText = styled(StyledText)`
+ text-decoration-line: underline;
+`;
+
+const LinkTouchable = styled(TouchableOpacity).attrs({
+ activeOpacity: 0.5,
+})`
+ flex-direction: row;
+ text-align: center;
+ align-items: center;
+ justify-content: center;
+`;
+
+const LearnMoreLink = ({
+ onPress,
+ learnMoreIsInternal,
+ learnMoreKey,
+ color,
+ Icon,
+}: LearnMoreLinkProps) => {
+ const IconComponent = Icon || Icons.ExternalLinkMedium;
+ return (
+
+
+
+
+ {(Icon || !learnMoreIsInternal) && (
+
+ )}
+
+ );
+};
+
+const Container = styled(Flex).attrs({
+ width: "100%",
+ flexDirection: "column",
+ flex: 1,
+ alignItems: "flex-start",
+})``;
+
+export default function Alert(props: Props) {
+ const {
+ id,
+ type = "secondary",
+ children: description,
+ title,
+ noIcon,
+ onLearnMore,
+ learnMoreUrl,
+ learnMoreKey,
+ learnMoreIsInternal = false,
+ learnMoreIcon,
+ } = props;
+
+ const dismissedBanners = useSelector(dismissedBannersSelector);
+
+ const alertProps = useMemo(
+ () => ({
+ ...alertPropsByType[type],
+ showIcon: !noIcon,
+ }),
+ [type, noIcon],
+ );
+
+ const hasLearnMore = !!onLearnMore || !!learnMoreUrl;
+ const handleLearnMore = useCallback(
+ () =>
+ onLearnMore
+ ? onLearnMore()
+ : learnMoreUrl
+ ? Linking.openURL(learnMoreUrl)
+ : undefined,
+ [onLearnMore, learnMoreUrl],
+ );
+
+ const isDismissed = useMemo(() => dismissedBanners.includes(id), [
+ dismissedBanners,
+ id,
+ ]);
+
+ return !isDismissed ? (
+ (
+
+ {title && {title}}
+ {description && (
+
+ {description}
+
+ )}
+ {hasLearnMore && (
+
+ )}
+
+ )}
+ />
+ ) : null;
+}
diff --git a/src/components/AnimatedHeader.js b/src/components/AnimatedHeader.js
index 2a5ee8cc52..bf63709fa8 100644
--- a/src/components/AnimatedHeader.js
+++ b/src/components/AnimatedHeader.js
@@ -169,7 +169,12 @@ export default function AnimatedHeaderView({
]}
onLayout={onLayoutText}
>
-
+
{title}
@@ -195,7 +200,7 @@ const styles = StyleSheet.create({
root: {
flex: 1,
},
- topHeader: { flexDirection: "row", alignContent: "center", height: 50 },
+ topHeader: { flexDirection: "row", alignContent: "center", height: 55 },
spacer: { flex: 1 },
header: {
...Styles.headerNoShadow,
@@ -212,7 +217,6 @@ const styles = StyleSheet.create({
zIndex: 2,
},
title: {
- fontSize: normalize(34),
lineHeight: 45,
},
buttons: {
diff --git a/src/components/BottomModal.js b/src/components/BottomModal.js
index 7193cbf33a..fc4f985f0f 100644
--- a/src/components/BottomModal.js
+++ b/src/components/BottomModal.js
@@ -104,7 +104,6 @@ const BottomModal = ({
-
);
};
diff --git a/src/components/BottomModal.tsx b/src/components/BottomModal.tsx
new file mode 100644
index 0000000000..8259ea2fc9
--- /dev/null
+++ b/src/components/BottomModal.tsx
@@ -0,0 +1,72 @@
+import React, { useCallback, useEffect, useState } from "react";
+import { StyleProp, ViewStyle } from "react-native";
+import { BottomDrawer } from "@ledgerhq/native-ui";
+import { useSelector } from "react-redux";
+import { isModalLockedSelector } from "../reducers/appstate";
+
+let isModalOpenedref: boolean | undefined = false;
+
+export type Props = {
+ id?: string;
+ isOpened?: boolean;
+ onClose?: () => void;
+ onModalHide?: () => void;
+ children?: React.ReactNode;
+ style?: StyleProp;
+ preventBackdropClick?: boolean;
+ containerStyle?: StyleProp;
+};
+
+const BottomModal = ({
+ isOpened,
+ onClose = () => {},
+ children,
+ style,
+ preventBackdropClick,
+ onModalHide,
+ containerStyle,
+ ...rest
+}: Props) => {
+ const [open, setIsOpen] = useState(false);
+ const modalLock = useSelector(isModalLockedSelector);
+
+ // workarround to make sure no double modal can be opened at same time
+ useEffect(
+ () => () => {
+ isModalOpenedref = false;
+ },
+ [],
+ );
+
+ useEffect(() => {
+ if (!!isModalOpenedref && isOpened) {
+ onClose();
+ } else {
+ setIsOpen(isOpened ?? false);
+ }
+ isModalOpenedref = isOpened;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isOpened, modalLock]); // do not add onClose it might cause some issues on modals ie: filter manager modal
+
+ const handleClose = useCallback(() => {
+ if (modalLock) return;
+ onClose && onClose();
+ onModalHide && onModalHide();
+ }, [modalLock, onClose, onModalHide]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default BottomModal;
diff --git a/src/components/BottomModalChoice.tsx b/src/components/BottomModalChoice.tsx
new file mode 100644
index 0000000000..568faf9819
--- /dev/null
+++ b/src/components/BottomModalChoice.tsx
@@ -0,0 +1,67 @@
+// @flow
+
+import React, { memo } from "react";
+import styled from "styled-components/native";
+import { Text, Icon, Flex } from "@ledgerhq/native-ui";
+import Touchable from "./Touchable";
+
+const hitSlop = {
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+};
+
+type Props = {
+ onPress: (e?: any) => any;
+ iconName: string;
+ title: string;
+ description?: string;
+ event: string;
+ eventProperties?: Object;
+};
+
+const Container = styled(Touchable)<{ opacity: number }>`
+ flex-direction: row;
+ padding-vertical: 15px;
+ margin-vertical: 5px;
+ align-items: center;
+ opacity: ${p => p.opacity};
+`;
+
+function BottomModalChoice({
+ iconName,
+ title,
+ description,
+ onPress,
+ event,
+ eventProperties,
+}: Props) {
+ return (
+
+
+ {iconName ? (
+
+ ) : null}
+
+
+
+ {title}
+
+ {!!description && (
+
+ {description}
+
+ )}
+
+
+ );
+}
+
+export default memo(BottomModalChoice);
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
new file mode 100644
index 0000000000..188ada04b5
--- /dev/null
+++ b/src/components/Button.tsx
@@ -0,0 +1,132 @@
+import React, { useCallback, memo, useContext, useMemo } from "react";
+import { ViewStyle } from "react-native";
+import { useIsFocused } from "@react-navigation/native";
+import { Button } from "@ledgerhq/native-ui";
+import { ButtonProps } from "@ledgerhq/native-ui/components/cta/Button";
+import ButtonUseTouchable from "../context/ButtonUseTouchable";
+import { track } from "../analytics";
+
+const inferType = (type?: string): ButtonProps["type"] => {
+ switch (type) {
+ case "primary":
+ case "lightPrimary":
+ return "shade";
+ case "alert":
+ return "error";
+ case "negativePrimary":
+ case "secondary":
+ case "lightSecondary":
+ case "darkSecondary":
+ case "greySecondary":
+ case "tertiary":
+ return "main";
+ default:
+ return type;
+ }
+};
+
+export interface BaseButtonProps extends Omit {
+ title?: React.ReactNode | string;
+ // when on press returns a promise,
+ // the button will toggle in a pending state and
+ // will wait the promise to complete before enabling the button again
+ // it also displays a spinner if it takes more than WAIT_TIME_BEFORE_SPINNER
+ onPress?: () => any;
+ pending?: boolean;
+ disabled?: boolean;
+ IconLeft?: React.ComponentType<{ size: number; color: string }>;
+ IconRight?: React.ComponentType<{ size: number; color: string }>;
+ containerStyle?: ViewStyle;
+ type?: string;
+ // for analytics
+ event?: string;
+ eventProperties?: Object;
+ // for testing
+ testID?: string;
+}
+
+type Props = BaseButtonProps & {
+ useTouchable: boolean;
+ isFocused: boolean;
+};
+
+function ButtonWrapped(props: BaseButtonProps) {
+ const isFocused = useIsFocused(); // @Warning be careful not to import the wrapped button outside of navigation context
+ const useTouchable = useContext(ButtonUseTouchable);
+ return (
+
+ );
+}
+
+export function BaseButton({
+ // required props
+ title,
+ onPress,
+ Icon,
+ IconLeft,
+ IconRight,
+ iconPosition,
+ disabled,
+ useTouchable,
+ event,
+ eventProperties,
+ type,
+ outline = true,
+ containerStyle,
+ children,
+ ...otherProps
+}: Props) {
+ const onPressHandler = useCallback(async () => {
+ if (!onPress) return;
+ if (event) {
+ track(event, eventProperties || null);
+ }
+ onPress();
+ }, [event, eventProperties, onPress]);
+
+ const isDisabled = disabled || !onPress;
+
+ const containerSpecificProps = useTouchable ? {} : { enabled: !isDisabled };
+
+ function getTestID() {
+ if (isDisabled || !otherProps.isFocused) return undefined;
+ if (otherProps.testID) return otherProps.testID;
+
+ switch (type) {
+ case "primary":
+ return "Proceed";
+ default:
+ return event;
+ }
+ }
+ const testID = useMemo(getTestID, [
+ isDisabled,
+ otherProps.isFocused,
+ otherProps.testID,
+ event,
+ type,
+ ]);
+
+ const ButtonIcon = Icon ?? IconRight ?? IconLeft;
+ const buttonIconPosition =
+ iconPosition ?? (IconRight && "right") ?? (IconLeft && "left");
+
+ return (
+
+ );
+}
+
+export default memo(ButtonWrapped);
diff --git a/src/components/CameraScreen/QRCodeBottomLayer.tsx b/src/components/CameraScreen/QRCodeBottomLayer.tsx
new file mode 100644
index 0000000000..9c1c1374a9
--- /dev/null
+++ b/src/components/CameraScreen/QRCodeBottomLayer.tsx
@@ -0,0 +1,88 @@
+import React, { memo } from "react";
+import { StyleSheet } from "react-native";
+import { Trans } from "react-i18next";
+
+import { Flex, Text, ProgressBar, Alert } from "@ledgerhq/native-ui";
+import { rgba } from "../../colors";
+
+import { softMenuBarHeight } from "../../logic/getWindowDimensions";
+
+type Props = {
+ progress?: number;
+ liveQrCode?: boolean;
+};
+
+function QrCodeBottomLayer({ progress, liveQrCode }: Props) {
+ return (
+
+
+
+
+
+ {progress !== undefined && progress > 0 && (
+
+
+
+ {Math.floor((progress || 0) * 100)}%
+
+
+ )}
+
+ {liveQrCode ? (
+
+
+
+
+
+
+
+
+
+
+ ) : null}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ darken: {
+ flexGrow: 1,
+ paddingBottom: softMenuBarHeight(),
+ },
+ text: {
+ fontSize: 16,
+ lineHeight: 24,
+ textAlign: "center",
+ color: "#fff",
+ },
+ centered: {
+ flex: 1,
+ alignItems: "center",
+ paddingTop: 8,
+ paddingHorizontal: 16,
+ },
+});
+
+export default memo(QrCodeBottomLayer);
diff --git a/src/components/CameraScreen/QRCodeTopLayer.tsx b/src/components/CameraScreen/QRCodeTopLayer.tsx
new file mode 100644
index 0000000000..59d87a6c84
--- /dev/null
+++ b/src/components/CameraScreen/QRCodeTopLayer.tsx
@@ -0,0 +1,3 @@
+const QRCodeTopLayer = () => null;
+
+export default QRCodeTopLayer;
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
new file mode 100644
index 0000000000..2968a506d0
--- /dev/null
+++ b/src/components/Card.tsx
@@ -0,0 +1,26 @@
+import React, { ReactNode } from "react";
+import { RectButton } from "react-native-gesture-handler";
+import { Flex } from "@ledgerhq/native-ui";
+import { FlexBoxProps } from "@ledgerhq/native-ui/components/Layout/Flex";
+
+export type Props = FlexBoxProps & {
+ children?: ReactNode;
+ style?: any;
+ onPress?: () => void;
+};
+
+const Card = ({ children, onPress, ...props }: Props) => {
+ const content = () => (
+
+ {children}
+
+ );
+
+ if (onPress) {
+ return {content()};
+ }
+
+ return content();
+};
+
+export default Card;
diff --git a/src/components/Carousel/Slide.tsx b/src/components/Carousel/Slide.tsx
new file mode 100644
index 0000000000..956701fe16
--- /dev/null
+++ b/src/components/Carousel/Slide.tsx
@@ -0,0 +1,67 @@
+import React, { useCallback } from "react";
+import { Linking, Image } from "react-native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import Touchable from "../Touchable";
+import { track } from "../../analytics";
+
+type SlideProps = {
+ url: string;
+ name: string;
+ title: string;
+ description: any;
+ image: any;
+ icon: any;
+ position: any;
+};
+
+const Slide = ({
+ url,
+ name,
+ title,
+ description,
+ image,
+ icon,
+ position,
+}: SlideProps) => {
+ const onClick = useCallback(() => {
+ track("Portfolio Recommended OpenUrl", {
+ url,
+ });
+ Linking.openURL(url);
+ }, [url]);
+ return (
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {image ? (
+
+ ) : icon ? (
+ {icon}
+ ) : null}
+
+
+
+ );
+};
+
+export default Slide;
diff --git a/src/components/Carousel/index.js b/src/components/Carousel/index.js
index 532ffd66b1..1c42ee6e9b 100644
--- a/src/components/Carousel/index.js
+++ b/src/components/Carousel/index.js
@@ -16,7 +16,7 @@ import Button from "../Button";
import IconClose from "../../icons/Close";
import Slide from "./Slide";
-const SLIDES = [
+export const SLIDES = [
{
url: urls.banners.ledgerAcademy,
name: "LedgerAcademy",
diff --git a/src/components/Carousel/index.tsx b/src/components/Carousel/index.tsx
new file mode 100644
index 0000000000..4902600a6a
--- /dev/null
+++ b/src/components/Carousel/index.tsx
@@ -0,0 +1,242 @@
+import React, { memo, useCallback, useMemo, useRef, useState } from "react";
+import { TouchableOpacity, ScrollView } from "react-native";
+import { useDispatch } from "react-redux";
+import map from "lodash/map";
+import { Trans } from "react-i18next";
+import { Box } from "@ledgerhq/native-ui";
+import { CloseMedium } from "@ledgerhq/native-ui/assets/icons";
+import styled from "styled-components/native";
+import Animated, { FadeOut, Layout } from "react-native-reanimated";
+import { urls } from "../../config/urls";
+import { setCarouselVisibility } from "../../actions/settings";
+import Slide from "./Slide";
+import Illustration from "../../images/illustration/Illustration";
+import AcademyLight from "../../images/illustration/Academy.light.png";
+import AcademyDark from "../../images/illustration/Academy.dark.png";
+import BuyCryptoLight from "../../images/illustration/BuyCrypto.light.png";
+import BuyCryptoDark from "../../images/illustration/BuyCrypto.dark.png";
+import SwapLight from "../../images/illustration/Swap.light.png";
+import SwapDark from "../../images/illustration/Swap.dark.png";
+import FamilyPackLight from "../../images/illustration/FamilyPack.light.png";
+import FamilyPackDark from "../../images/illustration/FamilyPack.dark.png";
+import { track } from "../../analytics";
+
+const DismissCarousel = styled(TouchableOpacity)`
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ width: 30px;
+ height: 30px;
+ align-items: center;
+ justify-content: center;
+`;
+
+export const SLIDES = [
+ {
+ url: urls.banners.ledgerAcademy,
+ name: "takeTour",
+ title: ,
+ description: ,
+ icon: (
+
+ ),
+ position: {
+ bottom: 70,
+ left: 15,
+ width: 146,
+ height: 93,
+ },
+ },
+ {
+ url: "ledgerlive://buy",
+ name: "buyCrypto",
+ title: ,
+ description: ,
+ icon: (
+
+ ),
+ position: {
+ bottom: 70,
+ left: 0,
+ width: 146,
+ height: 93,
+ },
+ },
+ {
+ url: "ledgerlive://swap",
+ name: "Swap",
+ title: ,
+ description: ,
+ icon: (
+
+ ),
+ position: {
+ bottom: 70,
+ left: 0,
+ width: 127,
+ height: 100,
+ },
+ },
+ {
+ url: urls.banners.familyPack,
+ name: "FamilyPack",
+ title: ,
+ description: ,
+ icon: (
+
+ ),
+ position: {
+ bottom: 70,
+ left: 0,
+ width: 180,
+ height: 80,
+ },
+ },
+];
+
+export const getDefaultSlides = () =>
+ map(SLIDES, slide => ({
+ id: slide.name,
+ Component: () => (
+
+ ),
+ }));
+
+const hitSlop = {
+ top: 16,
+ left: 16,
+ right: 16,
+ bottom: 16,
+};
+
+export const CAROUSEL_NONCE: number = 4;
+
+type CarouselCardProps = {
+ id: string;
+ children: React.ReactNode;
+ onHide: (cardId: string) => void;
+ index?: number;
+};
+
+const CarouselCard = ({ id, children, onHide, index }: CarouselCardProps) => (
+
+ {children}
+ onHide(id)}>
+
+
+
+);
+
+// TODO : make it generic in the ui
+const CarouselCardContainer = ({
+ id,
+ children,
+ onHide,
+ index,
+}: CarouselCardProps) => (
+
+ {children}
+
+);
+
+type Props = {
+ cardsVisibility: boolean[];
+};
+
+const Carousel = ({ cardsVisibility }: Props) => {
+ const dispatch = useDispatch();
+ const scrollViewRef = useRef(null);
+ const [currentPositionX, setCurrentPositionX] = useState(0);
+
+ const slides = useMemo(
+ () =>
+ getDefaultSlides().filter(slide => {
+ if (!cardsVisibility[slide.id]) {
+ return false;
+ }
+ if (slide.start && slide.start > new Date()) {
+ return false;
+ }
+ if (slide.end && slide.end < new Date()) {
+ return false;
+ }
+ return true;
+ }),
+ [cardsVisibility],
+ );
+
+ const onHide = useCallback(
+ cardId => {
+ const slide = SLIDES.find(slide => slide.name === cardId);
+ if (slide) {
+ track("Portfolio Recommended CloseUrl", {
+ url: slide.url,
+ });
+ }
+ dispatch(setCarouselVisibility({ ...cardsVisibility, [cardId]: false }));
+ },
+ [dispatch, cardsVisibility],
+ );
+
+ const onScrollEnd = useCallback(event => {
+ setCurrentPositionX(
+ event.nativeEvent.contentOffset.x +
+ event.nativeEvent.layoutMeasurement.width,
+ );
+ }, []);
+
+ const onScrollViewContentChange = useCallback(
+ contentWidth => {
+ // 264px = CarouselCard width + padding
+ if (currentPositionX > contentWidth) {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }
+ },
+ [currentPositionX],
+ );
+
+ if (!slides.length) {
+ // No slides or dismissed, no problem
+ return null;
+ }
+
+ return (
+
+ {slides.map(({ id, Component }, index) => (
+
+
+
+
+
+ ))}
+
+ );
+};
+
+export default memo(Carousel);
diff --git a/src/components/CheckBox.tsx b/src/components/CheckBox.tsx
new file mode 100644
index 0000000000..335bb9ce37
--- /dev/null
+++ b/src/components/CheckBox.tsx
@@ -0,0 +1,27 @@
+import React, { memo, useCallback } from "react";
+
+import { Checkbox as RNCheckbox } from "@ledgerhq/native-ui";
+
+type Props = {
+ isChecked: boolean;
+ onChange?: (value: boolean) => void;
+ disabled?: boolean;
+};
+
+function CheckBox({ isChecked, disabled, onChange, ...props }: Props) {
+ const onPress = useCallback(() => {
+ if (!onChange) return;
+ onChange(!isChecked);
+ }, [isChecked, onChange]);
+
+ return (
+
+ );
+}
+
+export default memo(CheckBox);
diff --git a/src/components/ChoiceButton.tsx b/src/components/ChoiceButton.tsx
new file mode 100644
index 0000000000..00e1f97fb4
--- /dev/null
+++ b/src/components/ChoiceButton.tsx
@@ -0,0 +1,43 @@
+import React, { ReactNode } from "react";
+import Button from "./wrappedUi/Button";
+
+type ChoiceButtonProps = {
+ disabled?: boolean;
+ onSelect: Function;
+ label: ReactNode;
+ description?: ReactNode;
+ Icon: any;
+ extra?: ReactNode;
+ event: string;
+ eventProperties: any;
+ navigationParams?: any[];
+ enableActions?: string;
+};
+
+const ChoiceButton = ({
+ event,
+ eventProperties,
+ disabled,
+ label,
+ Icon,
+ onSelect,
+ navigationParams,
+ enableActions,
+}: ChoiceButtonProps) => (
+
+);
+
+export default ChoiceButton;
diff --git a/src/components/ChoiceCard.tsx b/src/components/ChoiceCard.tsx
new file mode 100644
index 0000000000..da17ff3828
--- /dev/null
+++ b/src/components/ChoiceCard.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { TouchableOpacityProps } from "react-native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import Touchable from "./Touchable";
+
+const Card = ({
+ title,
+ titleProps,
+ subTitle,
+ subTitleProps,
+ labelBadge,
+ onPress,
+ Image,
+ disabled,
+ ...props
+}: {
+ title: string;
+ titleProps?: any;
+ subTitle?: string;
+ subTitleProps?: any;
+ labelBadge?: string;
+ Image: React.ReactNode;
+ onPress: TouchableOpacityProps["onPress"];
+ disabled?: boolean;
+}) => (
+
+
+
+ {labelBadge && (
+
+ {labelBadge}
+
+ )}
+
+ {title}
+
+ {subTitle && (
+
+ {subTitle}
+
+ )}
+
+
+ {Image}
+
+
+
+);
+
+export default Card;
diff --git a/src/components/CounterValue.js b/src/components/CounterValue.js
index c9930a1be5..5fe018c924 100644
--- a/src/components/CounterValue.js
+++ b/src/components/CounterValue.js
@@ -34,6 +34,7 @@ type Props = {
// wrapper component from outside
Wrapper?: React$ComponentType<*>,
subMagnitude?: number,
+ joinFragmentsSeparator?: string,
};
export const NoCountervaluePlaceholder = () => {
diff --git a/src/components/CurrencyIcon.tsx b/src/components/CurrencyIcon.tsx
new file mode 100644
index 0000000000..99e84c047d
--- /dev/null
+++ b/src/components/CurrencyIcon.tsx
@@ -0,0 +1,96 @@
+
+import React, { ComponentType, memo, useMemo } from "react";
+import { View } from "react-native";
+import {
+ getCryptoCurrencyIcon,
+ getTokenCurrencyIcon,
+} from "@ledgerhq/live-common/lib/reactNative";
+
+import {
+ CryptoCurrency,
+ Currency,
+ TokenCurrency,
+} from "@ledgerhq/live-common/lib/types";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import styled, { useTheme } from "styled-components/native";
+
+import { useCurrencyColor } from "../helpers/getCurrencyColor";
+
+const DefaultWrapper = styled(Flex)`
+ height: ${p => p.size}px;
+ width: ${p => p.size}px;
+ align-items: center;
+ justify-content: center;
+`;
+
+const CircleWrapper = styled(Flex)`
+ border-radius: 9999px;
+ border: 1px solid transparent;
+ background: ${p => p.color};
+ height: ${p => p.size}px;
+ width: ${p => p.size}px;
+ align-items: center;
+ justify-content: center;
+`;
+
+type IconProps = {
+ size: number;
+ color: string;
+};
+
+type Icon = ComponentType;
+
+const getIconComponent = (currency: CryptoCurrency | TokenCurrency): Icon => {
+ const icon =
+ currency.type === "TokenCurrency"
+ ? getTokenCurrencyIcon(currency)
+ : getCryptoCurrencyIcon(currency);
+
+ if (icon) {
+ return icon;
+ }
+
+ return ({ size, ...props }: IconProps) => (
+
+ {currency.ticker[0]}
+
+ );
+};
+
+type Props = {
+ currency: Currency;
+ size: number;
+ color?: string;
+ radius?: number;
+ bg?: string;
+ circle?: boolean;
+};
+
+const CurrencyIcon = ({ size, currency, circle, color, radius, bg }: Props) => {
+ const { colors } = useTheme();
+ const currencyColor = useCurrencyColor(currency, colors.background.main);
+
+ const overrideColor = color || currencyColor;
+
+ if (currency.type === "FiatCurrency") {
+ return null;
+ }
+
+ const IconComponent = getIconComponent(currency);
+
+ if (circle) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default memo(CurrencyIcon);
diff --git a/src/components/CurrencyRate.tsx b/src/components/CurrencyRate.tsx
new file mode 100644
index 0000000000..5ba08c58e1
--- /dev/null
+++ b/src/components/CurrencyRate.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import { BigNumber } from "bignumber.js";
+import {
+ CryptoCurrency,
+ TokenCurrency,
+} from "@ledgerhq/live-common/lib/types/currencies";
+import { Text } from "@ledgerhq/native-ui";
+import CounterValue from "./CounterValue";
+import CurrencyUnitValue from "./CurrencyUnitValue";
+
+type Props = {
+ currency: CryptoCurrency | TokenCurrency;
+};
+
+export default function CurrencyRate({ currency }: Props) {
+ const one = new BigNumber(10 ** currency.units[0].magnitude);
+
+ return (
+
+
+ {" = "}
+
+
+ );
+}
diff --git a/src/components/CurrencyUnitValue.tsx b/src/components/CurrencyUnitValue.tsx
new file mode 100644
index 0000000000..8ef0792b32
--- /dev/null
+++ b/src/components/CurrencyUnitValue.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import { formatCurrencyUnit } from "@ledgerhq/live-common/lib/currencies";
+import { Unit } from "@ledgerhq/live-common/lib/types";
+import { useSelector } from "react-redux";
+import { BigNumber } from "bignumber.js";
+
+import { discreetModeSelector, localeSelector } from "../reducers/settings";
+
+type Props = {
+ unit: Unit;
+ value: BigNumber | number;
+ showCode?: boolean;
+ alwaysShowSign?: boolean;
+ before?: string;
+ after?: string;
+ disableRounding?: boolean;
+ joinFragmentsSeparator?: string;
+};
+
+const CurrencyUnitValue = ({
+ unit,
+ value: valueProp,
+ showCode = true,
+ alwaysShowSign,
+ before = "",
+ after = "",
+ disableRounding = false,
+ joinFragmentsSeparator = "",
+}: Props): JSX.Element => {
+ const locale = useSelector(localeSelector);
+ const discreet = useSelector(discreetModeSelector);
+ const value =
+ valueProp instanceof BigNumber ? valueProp : new BigNumber(valueProp);
+
+ return (
+ <>
+ {before +
+ (value
+ ? formatCurrencyUnit(unit, value, {
+ showCode,
+ alwaysShowSign,
+ locale,
+ disableRounding,
+ discreet,
+ joinFragmentsSeparator,
+ })
+ : "") +
+ after}
+ >
+ );
+};
+
+export default CurrencyUnitValue;
diff --git a/src/components/CustomTabBar.tsx b/src/components/CustomTabBar.tsx
new file mode 100644
index 0000000000..9e89fdcc49
--- /dev/null
+++ b/src/components/CustomTabBar.tsx
@@ -0,0 +1,117 @@
+import React from "react";
+import { useTheme } from "styled-components/native";
+import { Flex } from "@ledgerhq/native-ui";
+import { TouchableOpacity } from "react-native";
+import Svg, { Path } from "react-native-svg";
+
+type SvgProps = {
+ color: string;
+};
+
+function TabBarShape({ color }: SvgProps) {
+ return (
+
+ );
+}
+
+export default function CustomTabBar({
+ state,
+ descriptors,
+ navigation,
+ colors,
+}: any) {
+ return (
+
+
+
+
+
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label =
+ options.tabBarLabel !== undefined
+ ? options.tabBarLabel
+ : options.title !== undefined
+ ? options.title
+ : route.name;
+ const Icon = options.tabBarIcon;
+
+ const isFocused = state.index === index;
+
+ const onPress = () => {
+ const event = navigation.emit({
+ type: "tabPress",
+ target: route.key,
+ canPreventDefault: true,
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ // The `merge: true` option makes sure that the params inside the tab screen are preserved
+ navigation.navigate({ name: route.name, merge: true });
+ }
+ };
+
+ const onLongPress = () => {
+ navigation.emit({
+ type: "tabLongPress",
+ target: route.key,
+ });
+ };
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/DelegationDrawer.js b/src/components/DelegationDrawer.js
index 78c0ae96e7..55125ad692 100644
--- a/src/components/DelegationDrawer.js
+++ b/src/components/DelegationDrawer.js
@@ -13,7 +13,6 @@ import type { AccountLike } from "@ledgerhq/live-common/lib/types";
// TODO move to component
import { useTheme } from "@react-navigation/native";
import DelegatingContainer from "../families/tezos/DelegatingContainer";
-import Close from "../icons/Close";
import { rgba } from "../colors";
import getWindowDimensions from "../logic/getWindowDimensions";
import BottomModal from "./BottomModal";
@@ -53,7 +52,6 @@ export default function DelegationDrawer({
undelegation,
icon,
}: Props) {
- const { colors } = useTheme();
const currency = getAccountCurrency(account);
const color = getCurrencyColor(currency);
const unit = getAccountUnit(account);
@@ -68,16 +66,6 @@ export default function DelegationDrawer({
onClose={onClose}
>
-
-
-
-
-
-
0
+ ? ["success.c100", ArrowUpMedium, "+"]
+ : ["error.c100", ArrowDownMedium, "-"]
+ : ["neutral.c100", () => null, ""];
+
+ return (
+
+ {percent && ArrowIcon ? : null}
+
+
+ {unit && absDelta !== 0 ? (
+
+ ) : percent ? (
+ `${absDelta.toFixed(0)}%`
+ ) : null}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ content: {
+ marginLeft: 5,
+ },
+});
+
+export default memo(Delta);
diff --git a/src/components/DeviceAction/getDeviceAnimation.js b/src/components/DeviceAction/getDeviceAnimation.js
index 5fbc58ac0c..ca066236eb 100644
--- a/src/components/DeviceAction/getDeviceAnimation.js
+++ b/src/components/DeviceAction/getDeviceAnimation.js
@@ -84,8 +84,8 @@ const animations = {
},
bluetooth: {
plugAndPinCode: {
- light: null,
- dark: null,
+ light: require("../../animations/nanoX/bluetooth/3EnterPinCode/light.json"),
+ dark: require("../../animations/nanoX/bluetooth/3EnterPinCode/dark.json"),
},
enterPinCode: {
light: require("../../animations/nanoX/bluetooth/3EnterPinCode/light.json"),
diff --git a/src/components/DeviceAction/rendering.tsx b/src/components/DeviceAction/rendering.tsx
new file mode 100644
index 0000000000..c8da364cd9
--- /dev/null
+++ b/src/components/DeviceAction/rendering.tsx
@@ -0,0 +1,549 @@
+import React, { useEffect } from "react";
+import { useDispatch } from "react-redux";
+import styled from "styled-components/native";
+import { WrongDeviceForAccount, UnexpectedBootloader } from "@ledgerhq/errors";
+import { TokenCurrency } from "@ledgerhq/live-common/lib/types";
+import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
+import { AppRequest } from "@ledgerhq/live-common/lib/hw/actions/app";
+import {
+ InfiniteLoader,
+ Text,
+ Flex,
+ Tag,
+ Icons,
+ Log,
+} from "@ledgerhq/native-ui";
+import { setModalLock } from "../../actions/appstate";
+import { urls } from "../../config/urls";
+import Alert from "../Alert";
+import { lighten } from "../../colors";
+import Button from "../Button";
+import { NavigatorName, ScreenName } from "../../const";
+import Animation from "../Animation";
+import getDeviceAnimation from "./getDeviceAnimation";
+import GenericErrorView from "../GenericErrorView";
+import Circle from "../Circle";
+import { MANAGER_TABS } from "../../screens/Manager/Manager";
+import ExternalLink from "../ExternalLink";
+import { track } from "../../analytics";
+
+const Wrapper = styled(Flex).attrs({
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ minHeight: "160px",
+})``;
+
+const AnimationContainer = styled(Flex).attrs(p => ({
+ alignSelf: "stretch",
+ alignItems: "center",
+ justifyContent: "center",
+ height: p.withConnectDeviceHeight
+ ? "100px"
+ : p.withVerifyAddressHeight
+ ? "72px"
+ : undefined,
+}))``;
+
+const ActionContainer = styled(Flex).attrs({
+ alignSelf: "stretch",
+})``;
+
+const SpinnerContainer = styled(Flex).attrs({
+ padding: 24,
+})``;
+
+const IconContainer = styled(Flex).attrs({
+ margin: 6,
+})``;
+
+const CenteredText = styled(Text).attrs({
+ fontWeight: "medium",
+ textAlign: "center",
+})``;
+
+const TitleContainer = styled(Flex).attrs({
+ py: 8,
+})``;
+
+const TitleText = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const DescriptionText = styled(CenteredText).attrs({
+ variant: "bodyLineHeight",
+ py: "8px",
+ fontWeight: "medium",
+ color: "neutral.c70",
+})``;
+
+const ConnectDeviceNameText = styled(Tag).attrs({
+ my: "8",
+})``;
+
+const StyledButton = styled(Button).attrs({
+ mt: 6,
+ alignSelf: "stretch",
+})``;
+
+const ConnectDeviceExtraContentWrapper = styled(Flex).attrs({
+ mb: 8,
+})``;
+
+type RawProps = {
+ t: (key: string, options?: { [key: string]: string | number }) => string;
+ colors?: any;
+ theme?: "light" | "dark";
+};
+
+export function renderRequestQuitApp({
+ t,
+ device,
+ theme,
+}: RawProps & {
+ device: Device;
+}) {
+ return (
+
+
+
+
+ {t("DeviceAction.quitApp")}
+
+ );
+}
+
+export function renderRequiresAppInstallation({
+ t,
+ navigation,
+ appNames,
+}: RawProps & {
+ navigation: any;
+ appNames: string[];
+}) {
+ const appNamesCSV = appNames.join(", ");
+
+ return (
+
+
+ {t("DeviceAction.appNotInstalled", {
+ appName: appNamesCSV,
+ count: appNames.length,
+ })}
+
+
+
+ navigation.navigate(NavigatorName.Manager, {
+ screen: ScreenName.Manager,
+ params: { searchQuery: appNamesCSV },
+ })
+ }
+ />
+
+
+ );
+}
+
+export function renderVerifyAddress({
+ t,
+ device,
+ currencyName,
+ onPress,
+ address,
+ theme,
+}: RawProps & {
+ device: Device;
+ currencyName: string;
+ onPress?: () => void;
+ address?: string;
+}) {
+ return (
+
+
+
+
+ {t("DeviceAction.verifyAddress.title")}
+
+ {t("DeviceAction.verifyAddress.description", { currencyName })}
+
+
+ {onPress && (
+
+ )}
+ {address && {address}}
+
+
+ );
+}
+
+export function renderConfirmSwap({
+ t,
+ device,
+ theme,
+}: RawProps & {
+ device: Device;
+}) {
+ return (
+
+
+ {t("DeviceAction.confirmSwap.alert")}
+
+
+
+
+ {t("DeviceAction.confirmSwap.title")}
+
+ );
+}
+
+export function renderConfirmSell({
+ t,
+ device,
+}: RawProps & {
+ device: Device;
+}) {
+ return (
+
+
+ {t("DeviceAction.confirmSell.alert")}
+
+
+
+
+ {t("DeviceAction.confirmSell.title")}
+
+ );
+}
+
+export function renderAllowManager({
+ t,
+ wording,
+ device,
+ theme,
+}: RawProps & {
+ wording: string;
+ device: Device;
+}) {
+ // TODO: disable gesture, modal close, hide header buttons
+ return (
+
+
+
+
+
+ {t("DeviceAction.allowManagerPermission", { wording })}
+
+
+ );
+}
+
+const AllowOpeningApp = ({
+ t,
+ navigation,
+ wording,
+ tokenContext,
+ isDeviceBlocker,
+ device,
+ theme,
+}: RawProps & {
+ navigation: any;
+ wording: string;
+ tokenContext?: TokenCurrency | null | undefined;
+ isDeviceBlocker?: boolean;
+ device: Device;
+}) => {
+ useEffect(() => {
+ if (isDeviceBlocker) {
+ // TODO: disable gesture, modal close, hide header buttons
+ navigation.setOptions({
+ gestureEnabled: false,
+ });
+ }
+ }, [isDeviceBlocker, navigation]);
+
+ return (
+
+
+
+
+ {t("DeviceAction.allowAppPermission", { wording })}
+ {tokenContext ? (
+
+ {t("DeviceAction.allowAppPermissionSubtitleToken", {
+ token: tokenContext.name,
+ })}
+
+ ) : null}
+
+ );
+};
+
+export function renderAllowOpeningApp({
+ t,
+ navigation,
+ wording,
+ tokenContext,
+ isDeviceBlocker,
+ device,
+ theme,
+}: RawProps & {
+ navigation: any;
+ wording: string;
+ tokenContext?: TokenCurrency | undefined | null;
+ isDeviceBlocker?: boolean;
+ device: Device;
+}) {
+ return (
+
+ );
+}
+
+export function renderInWrongAppForAccount({
+ t,
+ onRetry,
+ colors,
+ theme,
+}: RawProps & {
+ onRetry?: () => void;
+}) {
+ return renderError({
+ t,
+ error: new WrongDeviceForAccount(),
+ onRetry,
+ colors,
+ theme,
+ });
+}
+
+export function renderError({
+ t,
+ error,
+ onRetry,
+ managerAppName,
+ navigation,
+}: RawProps & {
+ navigation?: any;
+ error: Error;
+ onRetry?: () => void;
+ managerAppName?: string;
+}) {
+ const onPress = () => {
+ if (managerAppName && navigation) {
+ navigation.navigate(NavigatorName.Manager, {
+ screen: ScreenName.Manager,
+ params: {
+ tab: MANAGER_TABS.INSTALLED_APPS,
+ updateModalOpened: true,
+ },
+ });
+ } else if (onRetry) {
+ onRetry();
+ }
+ };
+ return (
+
+
+ {onRetry || managerAppName ? (
+
+
+
+ ) : null}
+
+ );
+}
+
+export function renderConnectYourDevice({
+ t,
+ unresponsive,
+ device,
+ theme,
+ onSelectDeviceLink,
+}: RawProps & {
+ unresponsive: boolean;
+ device: Device;
+ onSelectDeviceLink?: () => void;
+}) {
+ return (
+
+
+
+
+ {device.deviceName && (
+ {device.deviceName}
+ )}
+
+ {t(
+ unresponsive
+ ? "DeviceAction.unlockDevice"
+ : device.wired
+ ? "DeviceAction.connectAndUnlockDevice"
+ : "DeviceAction.turnOnAndUnlockDevice",
+ )}
+
+ {onSelectDeviceLink ? (
+
+
+
+ ) : null}
+
+ );
+}
+
+export function renderLoading({
+ t,
+ description,
+}: RawProps & {
+ description?: string;
+}) {
+ return (
+
+
+
+
+ {description ?? t("DeviceAction.loading")}
+
+ );
+}
+
+export function LoadingAppInstall({
+ analyticsPropertyFlow = "unknown",
+ request,
+ ...props
+}: RawProps & {
+ analyticsPropertyFlow: string;
+ description?: string;
+ request?: AppRequest;
+}) {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ // Nb Blocks closing the modal while the install is happening.
+ // releases the block on onmount.
+ dispatch(setModalLock(true));
+ return () => {
+ dispatch(setModalLock(false));
+ };
+ }, [dispatch]);
+
+ const currency = request?.currency || request?.account?.currency;
+ const appName = request?.appName || currency?.managerAppName;
+ useEffect(() => {
+ const trackingArgs = [
+ "In-line app install",
+ { appName, flow: analyticsPropertyFlow },
+ ];
+ track(...trackingArgs);
+ }, [appName, analyticsPropertyFlow]);
+ return renderLoading(props);
+}
+
+type WarningOutdatedProps = RawProps & {
+ colors: any;
+ navigation: any;
+ appName: string;
+ passWarning: () => void;
+};
+
+export function renderWarningOutdated({
+ t,
+ navigation,
+ appName,
+ passWarning,
+ colors,
+}: WarningOutdatedProps) {
+ function onOpenManager() {
+ navigation.navigate(NavigatorName.Manager);
+ }
+
+ return (
+
+
+
+
+
+
+ {t("DeviceAction.outdated")}
+
+ {t("DeviceAction.outdatedDesc", { appName })}
+
+
+
+
+
+
+ );
+}
+
+export function renderBootloaderStep({ t, colors, theme }: RawProps) {
+ return renderError({
+ t,
+ error: new UnexpectedBootloader(),
+ colors,
+ theme,
+ });
+}
diff --git a/src/components/DeviceActionModal.js b/src/components/DeviceActionModal.js
index e9522e404e..c0a4b68b8e 100644
--- a/src/components/DeviceActionModal.js
+++ b/src/components/DeviceActionModal.js
@@ -1,17 +1,14 @@
// @flow
import React from "react";
import { View, StyleSheet } from "react-native";
-import { useSelector } from "react-redux";
import type { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
import { SyncSkipUnderPriority } from "@ledgerhq/live-common/lib/bridge/react";
import { useTheme } from "@react-navigation/native";
import { useTranslation } from "react-i18next";
-import { isModalLockedSelector } from "../reducers/appstate";
import DeviceAction from "./DeviceAction";
import BottomModal from "./BottomModal";
import ModalBottomAction from "./ModalBottomAction";
-import Close from "../icons/Close";
-import Touchable from "./Touchable";
+
import InfoBox from "./InfoBox";
type Props = {
@@ -74,25 +71,10 @@ export default function DeviceActionModal({
/>
)}
{device && }
-
-
-
-
-
);
}
-const ModalLockAwareClose = ({ children }) => {
- const modalLock = useSelector(isModalLockedSelector);
- if (modalLock) return null;
- return children;
-};
-
const styles = StyleSheet.create({
footerContainer: {
flexDirection: "row",
diff --git a/src/components/DeviceActionModal.tsx b/src/components/DeviceActionModal.tsx
new file mode 100644
index 0000000000..89adc5cee5
--- /dev/null
+++ b/src/components/DeviceActionModal.tsx
@@ -0,0 +1,78 @@
+import React, { useState } from "react";
+import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
+import { SyncSkipUnderPriority } from "@ledgerhq/live-common/lib/bridge/react";
+import styled from "styled-components/native";
+import { Alert, Flex } from "@ledgerhq/native-ui";
+import { useTranslation } from "react-i18next";
+import DeviceAction from "./DeviceAction";
+import BottomModal from "./BottomModal";
+
+const DeviceActionContainer = styled(Flex).attrs({
+ flexDirection: "row",
+})``;
+
+type Props = {
+ // TODO: fix action type
+ action: any;
+ device: Device | null | undefined;
+ // TODO: fix request type
+ request?: any;
+ onClose?: () => void;
+ onModalHide?: () => void;
+ onResult?: (payload: any) => Promise | void;
+ renderOnResult?: (p: any) => React.ReactNode;
+ onSelectDeviceLink?: () => void;
+ analyticsPropertyFlow?: string;
+};
+
+export default function DeviceActionModal({
+ action,
+ device,
+ request,
+ onClose,
+ onResult,
+ renderOnResult,
+ onModalHide,
+ onSelectDeviceLink,
+ analyticsPropertyFlow,
+}: Props) {
+ const { t } = useTranslation();
+ const showAlert = !device?.wired;
+ const [result, setResult] = useState(null);
+ return (
+ {
+ if (onModalHide) onModalHide();
+ if (result) onResult(...result);
+ setResult(null);
+ }}
+ >
+ {result
+ ? null
+ : device && (
+
+
+ {
+ setResult([...props]);
+ }}
+ renderOnResult={renderOnResult}
+ onSelectDeviceLink={onSelectDeviceLink}
+ analyticsPropertyFlow={analyticsPropertyFlow}
+ />
+
+ {showAlert && (
+
+ )}
+
+ )}
+ {device && }
+
+ );
+}
diff --git a/src/components/DeviceJob/StepRunnerModal.js b/src/components/DeviceJob/StepRunnerModal.js
index 3869647a74..e05dfd73c5 100644
--- a/src/components/DeviceJob/StepRunnerModal.js
+++ b/src/components/DeviceJob/StepRunnerModal.js
@@ -1,12 +1,9 @@
// @flow
import React from "react";
-import { StyleSheet } from "react-native";
import type { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
import { useTheme } from "@react-navigation/native";
import BottomModal from "../BottomModal";
-import Close from "../../icons/Close";
-import Touchable from "../Touchable";
import type { Step } from "./types";
import type { DeviceNames } from "../../screens/Onboarding/types";
import { ErrorFooterGeneric, RenderError } from "./StepRenders";
@@ -52,17 +49,6 @@ export default function SelectDeviceConnectModal({
colors={colors}
/>
) : null}
-
-
-
);
}
-
-const styles = StyleSheet.create({
- close: {
- position: "absolute",
- right: 16,
- top: 16,
- },
-});
diff --git a/src/components/DiscreetModeButton.tsx b/src/components/DiscreetModeButton.tsx
new file mode 100644
index 0000000000..38f453cf91
--- /dev/null
+++ b/src/components/DiscreetModeButton.tsx
@@ -0,0 +1,31 @@
+import React, { useCallback } from "react";
+import { TouchableOpacity, StyleSheet } from "react-native";
+import { useDispatch, useSelector } from "react-redux";
+import { EyeMedium, EyeNoneMedium } from "@ledgerhq/native-ui/assets/icons";
+import { discreetModeSelector } from "../reducers/settings";
+import { setDiscreetMode } from "../actions/settings";
+
+export default function DiscreetModeButton({size = 24} : {size?: number}) {
+ const discreetMode = useSelector(discreetModeSelector);
+ const dispatch = useDispatch();
+ const onPress = useCallback(() => {
+ dispatch(setDiscreetMode(!discreetMode));
+ }, [discreetMode, dispatch]);
+
+ return (
+
+ {discreetMode ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ alignItems: "center",
+ justifyContent: "center",
+ },
+});
diff --git a/src/components/EditFeeUnit.js b/src/components/EditFeeUnit.js
index 1b8c162afd..634e043501 100644
--- a/src/components/EditFeeUnit.js
+++ b/src/components/EditFeeUnit.js
@@ -1,7 +1,7 @@
/* @flow */
import React, { useState } from "react";
import { FlatList, View, StyleSheet, Keyboard } from "react-native";
-import { useNavigation, useRoute, useTheme } from "@react-navigation/native";
+import { useNavigation, useRoute } from "@react-navigation/native";
import Icon from "react-native-vector-icons/dist/FontAwesome";
import type { Account } from "@ledgerhq/live-common/lib/types";
import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge";
@@ -15,7 +15,6 @@ import CurrencyInput from "./CurrencyInput";
import Touchable from "./Touchable";
import BottomModal from "./BottomModal";
import Button from "./Button";
-import CloseIcon from "../icons/Close";
type Props = {
account: Account,
@@ -23,7 +22,6 @@ type Props = {
};
export default function EditFreeUnit({ account, field }: Props) {
- const { colors } = useTheme();
const { navigate } = useNavigation();
const route = useRoute();
const { t } = useTranslation();
@@ -117,13 +115,6 @@ export default function EditFreeUnit({ account, field }: Props) {
{t("send.fees.edit.title")}
-
-
-
{
const { colors } = useTheme();
- const c = color || colors.live;
+ const c = color || colors.primary.c80;
return (
;
+ /** deprecated, use `iconPosition` instead */
+ iconFirst?: boolean;
+ iconPosition?: LinkProps["iconPosition"];
+ onPress?: LinkProps["onPress"];
+ text: LinkProps["children"];
+ type?: LinkProps["type"];
+};
+
+export default function ExternalLink({
+ disabled,
+ event,
+ eventProperties,
+ Icon,
+ iconFirst,
+ iconPosition,
+ onPress,
+ text,
+ type,
+}: Props) {
+ const handlePress = useCallback(
+ nativeEvent => {
+ if (event) {
+ track(event, ...(eventProperties ? [eventProperties] : []));
+ }
+ onPress && onPress(nativeEvent);
+ },
+ [event, eventProperties, onPress],
+ );
+
+ return (
+
+ {text}
+
+ );
+}
diff --git a/src/components/FabAccountButtonBar.tsx b/src/components/FabAccountButtonBar.tsx
new file mode 100644
index 0000000000..45e02823cf
--- /dev/null
+++ b/src/components/FabAccountButtonBar.tsx
@@ -0,0 +1,150 @@
+import React, {
+ useCallback,
+ memo,
+ useState,
+ ComponentType,
+ ReactElement,
+ ReactNode,
+} from "react";
+import { useNavigation } from "@react-navigation/native";
+
+import { AccountLike, Account } from "@ledgerhq/live-common/lib/types";
+
+import { Flex } from "@ledgerhq/native-ui";
+import ChoiceButton from "./ChoiceButton";
+import InfoModal from "./InfoModal";
+import Button from "./wrappedUi/Button";
+
+type ActionButtonEventProps = {
+ navigationParams?: any[];
+ confirmModalProps?: {
+ withCancel?: boolean;
+ id?: string;
+ title?: string | ReactElement;
+ desc?: string | ReactElement;
+ Icon?: ComponentType;
+ children?: ReactNode;
+ confirmLabel?: string | ReactElement;
+ confirmProps?: any;
+ };
+ Component?: ComponentType;
+ enableActions?: string;
+};
+
+type ActionButton = ActionButtonEventProps & {
+ label: ReactNode;
+ Icon?: ComponentType<{ size: number; color: string }>;
+ event: string;
+ eventProperties?: { [key: string]: any };
+ Component?: ComponentType;
+};
+
+type Props = {
+ buttons: ActionButton[];
+ actions?: { default: ActionButton[]; lending?: ActionButton[] };
+ account?: AccountLike;
+ parentAccount?: Account;
+};
+
+function FabAccountButtonBar({
+ buttons,
+ actions,
+ account,
+ parentAccount,
+}: Props) {
+ const navigation = useNavigation();
+
+ const [infoModalProps, setInfoModalProps] = useState<
+ ActionButtonEventProps | undefined
+ >();
+ const [isModalInfoOpened, setIsModalInfoOpened] = useState();
+
+ const onNavigate = useCallback(
+ (name: string, options?: any) => {
+ const accountId = account ? account.id : undefined;
+ const parentId = parentAccount ? parentAccount.id : undefined;
+ navigation.navigate(name, {
+ ...options,
+ params: {
+ ...(options ? options.params : {}),
+ accountId,
+ parentId,
+ },
+ });
+ },
+ [account, parentAccount, navigation],
+ );
+
+ const onPress = useCallback(
+ (data: ActionButtonEventProps) => {
+ const { navigationParams, confirmModalProps } = data;
+ if (!confirmModalProps) {
+ setInfoModalProps();
+ if (navigationParams) onNavigate(...navigationParams);
+ } else {
+ setInfoModalProps(data);
+ setIsModalInfoOpened(true);
+ }
+ },
+ [onNavigate, setIsModalInfoOpened],
+ );
+
+ const onContinue = useCallback(() => {
+ setIsModalInfoOpened(false);
+ onPress({ ...infoModalProps, confirmModalProps: undefined });
+ }, [infoModalProps, onPress]);
+
+ const onClose = useCallback(() => {
+ setIsModalInfoOpened();
+ }, []);
+
+ const onChoiceSelect = useCallback(({ navigationParams }) => {
+ if (navigationParams) {
+ onNavigate(...navigationParams);
+ }
+ }, []);
+
+ return (
+
+ {buttons.map(
+ (
+ { label, Icon, event, eventProperties, Component, ...rest },
+ index,
+ ) => (
+
+ ),
+ )}
+ {actions?.default?.map((a, i) =>
+ a.Component ? (
+
+ ) : (
+
+ ),
+ )}
+ {isModalInfoOpened && infoModalProps && (
+
+ )}
+
+ );
+}
+
+export default memo(FabAccountButtonBar);
diff --git a/src/components/FabActions.tsx b/src/components/FabActions.tsx
new file mode 100644
index 0000000000..c7d50da423
--- /dev/null
+++ b/src/components/FabActions.tsx
@@ -0,0 +1,173 @@
+import React from "react";
+
+import { useTheme } from "@react-navigation/native";
+import { Trans } from "react-i18next";
+import { useSelector } from "react-redux";
+import { PlusMedium } from "@ledgerhq/native-ui/assets/icons";
+
+import { getAccountCurrency } from "@ledgerhq/live-common/lib/account";
+
+import { AccountLike, Account } from "@ledgerhq/live-common/lib/types";
+
+import { isCurrencySupported } from "../screens/Exchange/coinifyConfig";
+
+import {
+ readOnlyModeEnabledSelector,
+ swapSelectableCurrenciesSelector,
+} from "../reducers/settings";
+import { accountsCountSelector } from "../reducers/accounts";
+import { NavigatorName, ScreenName } from "../const";
+import FabAccountButtonBar from "./FabAccountButtonBar";
+import Exchange from "../icons/Exchange";
+import Swap from "../icons/Swap";
+import useActions from "../screens/Account/hooks/useActions";
+import useLendingActions from "../screens/Account/hooks/useLendingActions";
+
+type Props = {
+ account?: AccountLike;
+ parentAccount?: Account;
+};
+
+type FabAccountActionsProps = {
+ account: AccountLike;
+ parentAccount?: Account;
+};
+
+function FabAccountActions({ account, parentAccount }: FabAccountActionsProps) {
+ const { colors } = useTheme();
+
+ const currency = getAccountCurrency(account);
+ const swapSelectableCurrencies = useSelector(
+ swapSelectableCurrenciesSelector,
+ );
+ const availableOnSwap =
+ swapSelectableCurrencies.includes(currency.id) && account.balance.gt(0);
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+
+ const canBeBought = isCurrencySupported(currency, "buy");
+
+ const allActions = [
+ ...(!readOnlyModeEnabled && canBeBought
+ ? [
+ {
+ navigationParams: [
+ NavigatorName.ExchangeBuyFlow,
+ {
+ screen: ScreenName.ExchangeConnectDevice,
+ params: {
+ account,
+ mode: "buy",
+ parentId:
+ account.type !== "Account" ? account.parentId : undefined,
+ },
+ },
+ ],
+ label: ,
+ Icon: PlusMedium,
+ event: "Buy Crypto Account Button",
+ eventProperties: {
+ currencyName: currency.name,
+ },
+ },
+ ]
+ : []),
+ ...(availableOnSwap
+ ? [
+ {
+ navigationParams: [
+ NavigatorName.Swap,
+ {
+ screen: ScreenName.Swap,
+ params: {
+ defaultAccount: account,
+ defaultParentAccount: parentAccount,
+ },
+ },
+ ],
+ label: (
+
+ ),
+ Icon: Swap,
+ event: "Swap Crypto Account Button",
+ eventProperties: { currencyName: currency.name },
+ },
+ ]
+ : []),
+ ...useActions({ account, parentAccount, colors }),
+ ];
+
+ // Do not display separators as buttons. (they do not have a label)
+ //
+ // First, count the index at which there are 2 valid buttons.
+ let counter = 0;
+ const sliceIndex =
+ allActions.findIndex(action => {
+ if (action.label) counter++;
+ return counter === 2;
+ }) + 1;
+
+ // Then slice from 0 to the index and ignore invalid button elements.
+ // Chaining methods should not be a big deal given the size of the actions array.
+ const buttons = allActions
+ .slice(0, sliceIndex || undefined)
+ .filter(action => !!action.label)
+ .slice(0, 2);
+
+ const actions = {
+ default: sliceIndex ? allActions.slice(sliceIndex) : [],
+ lending: useLendingActions({ account }),
+ };
+
+ return (
+
+ );
+}
+
+function FabActions({ account, parentAccount }: Props) {
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+ const accountsCount = useSelector(accountsCountSelector);
+
+ if (account)
+ return (
+
+ );
+
+ const actions = [
+ {
+ event: "TransferExchange",
+ label: ,
+ Icon: Exchange,
+ navigationParams: [
+ NavigatorName.Exchange,
+ { screen: ScreenName.ExchangeBuy },
+ ],
+ },
+ ...(accountsCount > 0 && !readOnlyModeEnabled
+ ? [
+ {
+ event: "TransferSwap",
+ label: ,
+ Icon: Swap,
+ navigationParams: [
+ NavigatorName.Swap,
+ {
+ screen: ScreenName.Swap,
+ },
+ ],
+ },
+ ]
+ : []),
+ ];
+
+ return ;
+}
+
+export default FabActions;
diff --git a/src/components/FilteredSearchBar.tsx b/src/components/FilteredSearchBar.tsx
new file mode 100644
index 0000000000..0d2ab43bee
--- /dev/null
+++ b/src/components/FilteredSearchBar.tsx
@@ -0,0 +1,52 @@
+import React, { ReactNode, useState } from "react";
+import { SearchInput } from "@ledgerhq/native-ui";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "styled-components/native";
+
+import Search from "./Search";
+
+type Props = {
+ initialQuery?: string;
+ renderList: (list: any[]) => ReactNode;
+ renderEmptySearch: () => ReactNode;
+ keys?: string[];
+ list: any[];
+ inputWrapperStyle?: any;
+};
+
+const FilteredSearchBar = ({
+ keys = ["name"],
+ initialQuery,
+ renderList,
+ list,
+ renderEmptySearch,
+}: Props) => {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const [query, setQuery] = useState(initialQuery || "");
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default FilteredSearchBar;
diff --git a/src/components/FirmwareUpdateBanner.js b/src/components/FirmwareUpdateBanner.js
index 5786a1821f..60b3f56c9e 100644
--- a/src/components/FirmwareUpdateBanner.js
+++ b/src/components/FirmwareUpdateBanner.js
@@ -4,10 +4,10 @@ import React, { useState, useEffect, useContext } from "react";
import {
View,
- TouchableOpacity,
TouchableHighlight,
StyleSheet,
Platform,
+ TouchableOpacity,
} from "react-native";
import manager from "@ledgerhq/live-common/lib/manager";
import * as Animatable from "react-native-animatable";
@@ -27,8 +27,8 @@ import { hasConnectedDeviceSelector } from "../reducers/appstate";
import IconExclamation from "../icons/ExclamationCircleFull";
import { BaseButton as Button } from "./Button";
import IconDownload from "../icons/Download";
-import BottomModal from "./BottomModal";
import IconClose from "../icons/Close";
+import BottomModal from "./BottomModal";
import IconNano from "../icons/NanoS";
import { rgba } from "../colors";
import LText from "./LText";
@@ -120,13 +120,6 @@ const FirmwareUpdateBanner = () => {
isOpened={showDrawer}
onClose={onCloseDrawer}
>
-
-
-
-
{
+ const lastSeenDevice: DeviceModelInfo | null = useSelector(
+ lastSeenDeviceSelector,
+ );
+ const hasConnectedDevice = useSelector(hasConnectedDeviceSelector);
+ const hasCompletedOnboarding: boolean = useSelector(
+ hasCompletedOnboardingSelector,
+ );
+ const [showDrawer, setShowDrawer] = useState(false);
+ const [showBanner, setShowBanner] = useState(false);
+ const [version, setVersion] = useState("");
+
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+ const useTouchable = useContext(ButtonUseTouchable);
+
+ useEffect(() => {
+ async function getLatestFirmwareForDevice() {
+ const fw:
+ | FirmwareUpdateContext
+ | null
+ | undefined = await manager.getLatestFirmwareForDevice(
+ lastSeenDevice?.deviceInfo,
+ );
+
+ setShowBanner(Boolean(fw));
+ setVersion(fw?.final?.name ?? "");
+ }
+
+ getLatestFirmwareForDevice();
+ }, [lastSeenDevice, setShowBanner, setVersion]);
+
+ const onPress = () => {
+ setShowDrawer(true);
+ };
+ const onCloseDrawer = () => {
+ setShowDrawer(false);
+ };
+ const onDismissBanner = () => {
+ setShowBanner(false);
+ };
+
+ return showBanner && hasConnectedDevice && hasCompletedOnboarding ? (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ ) : null;
+};
+
+const styles = {
+ banner: StyleSheet.create({
+ root: {
+ position: "absolute",
+ width: "100%",
+ top: 30,
+ left: 0,
+ zIndex: 100,
+ padding: 16,
+ },
+ }),
+};
+
+export default FirmwareUpdateBanner;
diff --git a/src/components/GenericErrorBottomModal.js b/src/components/GenericErrorBottomModal.js
index f1d88c3f64..e34e290b68 100644
--- a/src/components/GenericErrorBottomModal.js
+++ b/src/components/GenericErrorBottomModal.js
@@ -2,9 +2,6 @@
import React, { memo } from "react";
import { View, StyleSheet } from "react-native";
-import { useTheme } from "@react-navigation/native";
-import Touchable from "./Touchable";
-import Close from "../icons/Close";
import BottomModal from "./BottomModal";
import type { Props as BottomModalProps } from "./BottomModal";
import GenericErrorView from "./GenericErrorView";
@@ -23,7 +20,6 @@ function GenericErrorBottomModal({
hasExportLogButton,
...otherProps
}: Props) {
- const { colors } = useTheme();
return (
{footerButtons}
) : null}
- {onClose ? (
-
-
-
- ) : null}
) : null}
diff --git a/src/components/GenericErrorView.tsx b/src/components/GenericErrorView.tsx
new file mode 100644
index 0000000000..0cebbfebc5
--- /dev/null
+++ b/src/components/GenericErrorView.tsx
@@ -0,0 +1,81 @@
+import React from "react";
+import { Trans } from "react-i18next";
+import { Box, Button, Flex, IconBox, Text } from "@ledgerhq/native-ui";
+import { CloseMedium } from "@ledgerhq/native-ui/assets/icons";
+import useExportLogs from "./useExportLogs";
+import TranslatedError from "./TranslatedError";
+import SupportLinkError from "./SupportLinkError";
+import DownloadFileIcon from "../icons/DownloadFile";
+
+const GenericErrorView = ({
+ error,
+ outerError,
+ withDescription = true,
+ withIcon = true,
+ hasExportLogButton = true,
+}: {
+ error: Error;
+ // sometimes we want to "hide" the technical error into a category
+ // for instance, for Genuine check we want to express "Genuine check failed" because ""
+ // in such case, the outerError is GenuineCheckFailed and the actual error is still error
+ outerError?: Error;
+ withDescription?: boolean;
+ withIcon?: boolean;
+ hasExportLogButton?: boolean;
+}) => {
+ const onExport = useExportLogs();
+
+ const titleError = outerError || error;
+ const subtitleError = outerError ? error : null;
+
+ return (
+
+ {withIcon ? (
+
+
+
+ ) : null}
+
+
+
+ {subtitleError ? (
+
+
+
+ ) : null}
+ {withDescription ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+ {hasExportLogButton ? (
+ <>
+
+ >
+ ) : null}
+
+ );
+};
+
+export default GenericErrorView;
diff --git a/src/components/GraphCard.tsx b/src/components/GraphCard.tsx
new file mode 100644
index 0000000000..9e6a182b1f
--- /dev/null
+++ b/src/components/GraphCard.tsx
@@ -0,0 +1,134 @@
+import React, { ReactNode, useCallback } from "react";
+import { TouchableOpacity, View } from "react-native";
+import { Currency, Unit } from "@ledgerhq/live-common/lib/types";
+import {
+ Portfolio,
+ ValueChange,
+} from "@ledgerhq/live-common/lib/portfolio/v2/types";
+import { BoxedIcon, Flex, Text } from "@ledgerhq/native-ui";
+import { Trans } from "react-i18next";
+import { PieChartMedium } from "@ledgerhq/native-ui/assets/icons";
+import Delta from "./Delta";
+import { Item } from "./Graph/types";
+import TransactionsPendingConfirmationWarning from "./TransactionsPendingConfirmationWarning";
+import CurrencyUnitValue from "./CurrencyUnitValue";
+import Placeholder from "./Placeholder";
+import DiscreetModeButton from "./DiscreetModeButton";
+import { useNavigation } from "@react-navigation/native";
+import { NavigatorName } from "../const";
+
+type Props = {
+ portfolio: Portfolio;
+ counterValueCurrency: Currency;
+ useCounterValue?: boolean;
+ renderTitle?: ({ counterValueUnit: Unit, item: Item }) => ReactNode;
+};
+
+export default function GraphCard({
+ portfolio,
+ renderTitle,
+ counterValueCurrency,
+}: Props) {
+ const { countervalueChange } = portfolio;
+
+ const isAvailable = portfolio.balanceAvailable;
+ const balanceHistory = portfolio.balanceHistory;
+
+ return (
+
+
+
+ );
+}
+
+function GraphCardHeader({
+ unit,
+ valueChange,
+ renderTitle,
+ isLoading,
+ to,
+}: {
+ isLoading: boolean;
+ valueChange: ValueChange;
+ unit: Unit;
+ to: Item;
+ renderTitle?: ({ counterValueUnit: Unit, item: Item }) => ReactNode;
+}) {
+ const item = to;
+ const navigation = useNavigation();
+
+ const onPieChartButtonpress = useCallback(() => {
+ navigation.navigate(NavigatorName.Analytics);
+ }, [navigation]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : renderTitle ? (
+ renderTitle({ counterValueUnit: unit, item })
+ ) : (
+
+
+
+ )}
+
+
+
+ {isLoading ? (
+ <>
+
+
+ >
+ ) : (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/HeaderBackImage.tsx b/src/components/HeaderBackImage.tsx
new file mode 100644
index 0000000000..1d024f1925
--- /dev/null
+++ b/src/components/HeaderBackImage.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { View, Platform, StyleSheet } from "react-native";
+import { ArrowLeftMedium } from "@ledgerhq/native-ui/assets/icons";
+
+export default function HeaderBackImage() {
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ marginLeft: Platform.OS === "ios" ? 0 : -13,
+ padding: 16,
+ },
+});
diff --git a/src/components/HeaderTitle.tsx b/src/components/HeaderTitle.tsx
new file mode 100644
index 0000000000..669d7e0a3b
--- /dev/null
+++ b/src/components/HeaderTitle.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { TouchableWithoutFeedback } from "react-native";
+import { Text } from "@ledgerhq/native-ui";
+import { scrollToTop } from "../navigation/utils";
+
+export default function HeaderTitle(props: any) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/InfoModal.tsx b/src/components/InfoModal.tsx
new file mode 100644
index 0000000000..842962a345
--- /dev/null
+++ b/src/components/InfoModal.tsx
@@ -0,0 +1,156 @@
+import React, { memo } from "react";
+import { StyleSheet, View } from "react-native";
+import { Trans } from "react-i18next";
+
+import { useTheme } from "styled-components/native";
+import { Icons, IconBox, Flex, Button } from "@ledgerhq/native-ui";
+import BottomModal from "./BottomModal";
+import LText from "./LText";
+import IconArrowRight from "../icons/ArrowRight";
+import type { Props as ModalProps } from "./BottomModal";
+
+type BulletItem = {
+ key: string;
+ val: React.ReactNode;
+};
+
+type InfoModalProps = ModalProps & {
+ id?: string;
+ title?: React.ReactNode;
+ desc?: React.ReactNode;
+ bullets?: BulletItem[];
+ Icon?: React.ReactNode;
+ withCancel?: boolean;
+ onContinue?: () => void;
+ children?: React.ReactNode;
+ confirmLabel?: React.ReactNode;
+ confirmProps?: any;
+};
+
+const InfoModal = ({
+ isOpened,
+ onClose,
+ id,
+ title,
+ desc,
+ bullets,
+ Icon = Icons.InfoMedium,
+ withCancel,
+ onContinue,
+ children,
+ confirmLabel,
+ confirmProps,
+ style,
+ containerStyle,
+}: InfoModalProps) => (
+
+
+
+ {title ? (
+
+ {title}
+
+ ) : null}
+
+ {desc ? (
+
+ {desc}
+
+ ) : null}
+ {bullets ? (
+
+ {bullets.map(b => (
+ {b.val}
+ ))}
+
+ ) : null}
+
+ {children}
+
+
+
+
+ {withCancel ? (
+
+ ) : null}
+
+
+
+);
+
+function BulletLine({ children }: { children: any }) {
+ const { colors } = useTheme();
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ modal: {
+ paddingHorizontal: 16,
+ paddingTop: 24,
+ alignItems: "center",
+ },
+ modalTitle: {
+ marginVertical: 16,
+ fontSize: 14,
+ lineHeight: 21,
+ },
+ modalDesc: {
+ textAlign: "center",
+
+ marginBottom: 24,
+ },
+ bulletsContainer: {
+ alignSelf: "flex-start",
+ },
+ bulletLine: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: 8,
+ },
+ bulletLineText: {
+ marginLeft: 4,
+ textAlign: "left",
+ },
+ childrenContainer: {
+ paddingTop: 24,
+ },
+ footer: {
+ alignSelf: "stretch",
+ paddingTop: 24,
+ flexDirection: "row",
+ },
+});
+
+export default memo(InfoModal);
diff --git a/src/components/InvertTheme.tsx b/src/components/InvertTheme.tsx
new file mode 100644
index 0000000000..ded040caf4
--- /dev/null
+++ b/src/components/InvertTheme.tsx
@@ -0,0 +1,22 @@
+import React, { useMemo } from "react";
+import { ThemeProvider, useTheme } from "styled-components/native";
+import { defaultTheme, palettes } from "@ledgerhq/native-ui/styles";
+
+export default function InvertTheme({
+ children,
+}: {
+ children?: React.ReactNode;
+}): React.ReactElement {
+ const { theme } = useTheme();
+ const revertTheme = theme === "light" ? "dark" : "light";
+ const newTheme = useMemo(
+ () => ({
+ ...defaultTheme,
+ colors: { ...defaultTheme.colors, palette: palettes[revertTheme] },
+ theme: revertTheme,
+ }),
+ [revertTheme],
+ );
+
+ return {children};
+}
diff --git a/src/components/LText/index.tsx b/src/components/LText/index.tsx
new file mode 100644
index 0000000000..7f6248bb3a
--- /dev/null
+++ b/src/components/LText/index.tsx
@@ -0,0 +1,56 @@
+/* @flow */
+import React from "react";
+import { Text } from "@ledgerhq/native-ui";
+import getFontStyle from "./getFontStyle";
+import { FontWeightTypes } from "@ledgerhq/native-ui/components/Text/getTextStyle";
+
+export { getFontStyle };
+
+export type Opts = {
+ bold?: boolean;
+ semiBold?: boolean;
+ secondary?: boolean;
+ monospace?: boolean;
+ color?: string;
+ bg?: string;
+ children?: React.ReactNode;
+};
+
+export type Res = {
+ fontFamily: string;
+ fontWeight:
+ | "normal"
+ | "bold"
+ | "100"
+ | "200"
+ | "300"
+ | "400"
+ | "500"
+ | "600"
+ | "700"
+ | "800"
+ | "900";
+};
+
+const inferFontWeight = ({ semiBold, bold }: Opts): FontWeightTypes => {
+ if (bold) {
+ return 'bold'
+ } else if (semiBold) {
+ return 'semibold'
+ }
+ return 'medium'
+};
+
+/**
+ * This component is just a proxy to the Text component defined in @ledgerhq/react-ui.
+ * It should only be used to map legacy props/logic from LLM to the new text component.
+ *
+ * @deprecated Please, prefer using the Text component from our design-system if possible.
+ */
+export default function LText({ color, children, semiBold, bold, ...props }: Opts) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/ModalBottomAction.tsx b/src/components/ModalBottomAction.tsx
new file mode 100644
index 0000000000..bf437419bc
--- /dev/null
+++ b/src/components/ModalBottomAction.tsx
@@ -0,0 +1,48 @@
+/* @flow */
+import React, { Component } from "react";
+import { Flex, Text } from "@ledgerhq/native-ui";
+
+export default class ModalBottomAction extends Component<{
+ icon?: any;
+ title?: any;
+ uppercase?: boolean;
+ description?: any;
+ footer: any;
+ shouldWrapDesc?: boolean;
+}> {
+ render() {
+ const {
+ icon,
+ title,
+ uppercase,
+ description,
+ footer,
+ shouldWrapDesc = true,
+ } = this.props;
+ return (
+
+ {icon && {icon}}
+ {title ? (
+
+ {title}
+
+ ) : null}
+
+ {description && shouldWrapDesc ? (
+
+ {description}
+
+ ) : (
+ description
+ )}
+ {footer}
+
+
+ );
+ }
+}
diff --git a/src/components/NavigationHeader.tsx b/src/components/NavigationHeader.tsx
new file mode 100644
index 0000000000..3666e27f10
--- /dev/null
+++ b/src/components/NavigationHeader.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { View } from "react-native";
+import { ArrowLeftMedium, CloseMedium } from "@ledgerhq/native-ui/assets/icons";
+import { Flex, Text, Link } from "@ledgerhq/native-ui";
+import { StackHeaderProps } from "@react-navigation/stack";
+import { getHeaderTitle } from "@react-navigation/elements";
+import { FlexBoxProps } from "@ledgerhq/native-ui/components/layout/Flex";
+
+type NavigationHeaderProps = StackHeaderProps & {
+ containerProps?: FlexBoxProps;
+ hideBack?: boolean;
+};
+
+function NavigationHeader({
+ navigation,
+ route,
+ options,
+ back,
+ hideBack,
+ containerProps,
+}: NavigationHeaderProps) {
+ const { t } = useTranslation();
+ const title = t(getHeaderTitle(options, route.name));
+ return (
+
+ {back && !hideBack ? (
+
+ ) : (
+
+ )}
+ {title.length ? (
+
+ {title}
+
+ ) : null}
+ {
+ navigation.getParent()?.goBack();
+ }}
+ />
+
+ );
+}
+
+export default (props: NavigationHeaderProps) => (
+
+);
diff --git a/src/components/NavigationModalContainer.tsx b/src/components/NavigationModalContainer.tsx
new file mode 100644
index 0000000000..3aa2dbe2dd
--- /dev/null
+++ b/src/components/NavigationModalContainer.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+import { Pressable } from "react-native";
+import { Flex, ScrollContainer } from "@ledgerhq/native-ui";
+import { StackScreenProps } from "@react-navigation/stack";
+import styled from "styled-components/native";
+import type { FlexBoxProps } from "@ledgerhq/native-ui/components/layout/Flex";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+export const MIN_MODAL_HEIGHT = 30;
+
+const ScreenContainer = styled(Flex).attrs(p => ({
+ edges: ["bottom"],
+ flex: 1,
+ p: p.p ?? 6,
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+}))``;
+type Props = StackScreenProps<{}> & {
+ children: React.ReactNode,
+ contentContainerProps?: FlexBoxProps,
+ deadZoneProps?: FlexBoxProps,
+ backgroundColor?: string,
+ };
+
+export default function NavigationModalContainer({
+ navigation,
+ children,
+ contentContainerProps,
+ deadZoneProps,
+ backgroundColor = "palette.neutral.c00",
+}: Props) {
+ return (
+
+
+ {
+ navigation.canGoBack() && navigation.goBack();
+ }}
+ />
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/NavigationOverlay.tsx b/src/components/NavigationOverlay.tsx
new file mode 100644
index 0000000000..2e45148461
--- /dev/null
+++ b/src/components/NavigationOverlay.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { Pressable, StyleSheet } from "react-native";
+import { useNavigation } from "@react-navigation/native";
+import styled from "styled-components/native";
+
+const Container = styled(Pressable)`
+ background-color: ${p => p.theme.colors.constant.overlay};
+`;
+
+export default function NavigationOverlay() {
+ const navigation = useNavigation();
+
+ return (
+ {
+ navigation.canGoBack() && navigation.goBack();
+ }}
+ />
+ );
+}
diff --git a/src/components/NavigationScrollView.tsx b/src/components/NavigationScrollView.tsx
new file mode 100644
index 0000000000..ba1b5354f2
--- /dev/null
+++ b/src/components/NavigationScrollView.tsx
@@ -0,0 +1,18 @@
+import { ScrollListContainer } from "@ledgerhq/native-ui";
+import React, { useRef } from "react";
+import { ScrollViewProps } from "react-native";
+import { useScrollToTop } from "../navigation/utils";
+
+export default function NavigationScrollView({
+ children,
+ ...scrollViewProps
+}: ScrollViewProps) {
+ const ref = useRef();
+ useScrollToTop(ref);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/Nft/NftCollectionRow.tsx b/src/components/Nft/NftCollectionRow.tsx
new file mode 100644
index 0000000000..7c142f0319
--- /dev/null
+++ b/src/components/Nft/NftCollectionRow.tsx
@@ -0,0 +1,78 @@
+import React from "react";
+import { StyleSheet } from "react-native";
+import { RectButton } from "react-native-gesture-handler";
+import {
+ useNftMetadata,
+ CollectionWithNFT,
+} from "@ledgerhq/live-common/lib/nft";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import Skeleton from "../Skeleton";
+import NftImage from "./NftImage";
+
+type Props = {
+ collection: CollectionWithNFT;
+ onCollectionPress: () => void;
+};
+
+function NftCollectionRow({ collection, onCollectionPress }: Props) {
+ const { colors } = useTheme();
+ const { contract, nfts } = collection;
+ const { status, metadata } = useNftMetadata(contract, nfts[0].tokenId);
+ const loading = status === "loading";
+
+ return (
+
+
+
+
+
+
+ {metadata?.tokenName || collection.contract}
+
+
+
+
+ {collection.nfts.length}
+
+
+
+ );
+}
+
+export default NftCollectionRow;
+
+const styles = StyleSheet.create({
+ container: {
+ borderRadius: 4,
+ },
+ collectionNameSkeleton: {
+ height: 8,
+ width: 113,
+ borderRadius: 4,
+ },
+ collectionImage: {
+ borderRadius: 4,
+ width: 36,
+ aspectRatio: 1,
+ overflow: "hidden",
+ },
+});
diff --git a/src/components/Nft/NftLinksPanel.js b/src/components/Nft/NftLinksPanel.js
index 6934c9fb97..384a178922 100644
--- a/src/components/Nft/NftLinksPanel.js
+++ b/src/components/Nft/NftLinksPanel.js
@@ -10,7 +10,6 @@ import ExternalLinkIcon from "../../icons/ExternalLink";
import OpenSeaIcon from "../../icons/OpenSea";
import RaribleIcon from "../../icons/Rarible";
import GlobeIcon from "../../icons/Globe";
-import CloseIcon from "../../icons/Close";
import BottomModal from "../BottomModal";
import { rgba } from "../../colors";
import LText from "../LText";
@@ -66,10 +65,6 @@ const NftLinksPanel = ({ links, isOpen, onClose }: Props) => {
isOpened={isOpen}
onClose={onClose}
>
-
-
-
-
{!links.opensea ? null : (
({
+ height: "64px",
+ flexDirection: "row",
+ alignItems: "center",
+ px: 0,
+ py: 6,
+}))<{ isLast?: boolean }>``;
+
+const Wrapper = styled(Flex).attrs(p => ({
+ flex: 1,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginLeft: 4,
+ marginRight: 0,
+ opacity: p.isOptimistic ? 0.5 : 1,
+}))<{ isOptimistic?: boolean }>``;
+
+const SpinnerContainer = styled(Box).attrs({
+ height: 14,
+ mr: 2,
+ justifyContent: "center",
+})``;
+
+const BodyLeftContainer = styled(Flex).attrs({
+ flexDirection: "column",
+ justifyContent: "flex-start",
+ alignItems: "flex-start",
+ flex: 1,
+})``;
+
+const BodyRightContainer = styled(Flex).attrs({
+ flexDirection: "column",
+ justifyContent: "flex-start",
+ alignItems: "flex-end",
+ flexShrink: 0,
+ pl: 3,
+})``;
+
+type Props = {
+ operation: Operation;
+ parentAccount: Account | undefined | null;
+ account: AccountLike;
+ multipleAccounts?: boolean;
+ isLast: boolean;
+ isSubOperation?: boolean;
+};
+
+const placeholderProps = {
+ width: 40,
+ containerHeight: 20,
+};
+
+export default function OperationRow({
+ account,
+ parentAccount,
+ operation,
+ isSubOperation,
+ multipleAccounts,
+ isLast,
+}: Props) {
+ const navigation = useNavigation();
+
+ const goToOperationDetails = debounce(() => {
+ const params = [
+ ScreenName.OperationDetails,
+ {
+ accountId: account.id,
+ parentId: parentAccount && parentAccount.id,
+ operation, // FIXME we should pass a operationId instead because data can changes over time.
+ isSubOperation,
+ key: operation.id,
+ },
+ ];
+
+ /** if suboperation push to stack navigation else we simply navigate */
+ if (isSubOperation) navigation.push(...params);
+ else navigation.navigate(...params);
+ }, 300);
+
+ const renderAmountCellExtra = useCallback(() => {
+ const mainAccount = getMainAccount(account, parentAccount);
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+ const specific = mainAccount.currency.family
+ ? perFamilyOperationDetails[mainAccount.currency.family]
+ : null;
+
+ const SpecificAmountCell =
+ specific && specific.amountCell
+ ? specific.amountCell[operation.type]
+ : null;
+
+ return SpecificAmountCell ? (
+
+ ) : null;
+ }, [account, parentAccount, operation]);
+
+ const amount = getOperationAmountNumber(operation);
+ const valueColor = amount.isNegative() ? "neutral.c100" : "success.c100";
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+
+ const text = ;
+ const isOptimistic = operation.blockHeight === null;
+ const spinner = (
+
+
+
+ );
+
+ return (
+
+
+
+
+
+
+
+ {multipleAccounts ? getAccountName(account) : text}
+
+
+ {isOptimistic ? (
+
+ {spinner}
+
+
+
+
+ ) : (
+
+ {text}
+
+ )}
+
+
+ {renderAmountCellExtra()}
+
+ {amount.isZero() ? null : (
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/PasswordInput.tsx b/src/components/PasswordInput.tsx
new file mode 100644
index 0000000000..0b882fcd1a
--- /dev/null
+++ b/src/components/PasswordInput.tsx
@@ -0,0 +1,152 @@
+import React, { useEffect, useState, useCallback, useRef } from "react";
+import { View, StyleSheet, TextInput } from "react-native";
+import Icon from "react-native-vector-icons/dist/Feather";
+import Touchable from "./Touchable";
+import { getFontStyle } from "./LText";
+import { withTheme } from "../colors";
+
+type Props = {
+ secureTextEntry: boolean;
+ onChange: (value: string) => void;
+ onSubmit: () => void;
+ toggleSecureTextEntry: () => void;
+ placeholder: string;
+ autoFocus?: boolean;
+ inline?: boolean;
+ onFocus?: any;
+ onBlur?: any;
+ error?: Error;
+ password?: string;
+ colors: any;
+};
+
+const PasswordInput = ({
+ autoFocus,
+ error,
+ secureTextEntry,
+ onChange,
+ onSubmit,
+ onFocus,
+ onBlur,
+ toggleSecureTextEntry,
+ placeholder,
+ inline,
+ password,
+ colors,
+}: Props) => {
+ const [isFocused, setIsFocused] = useState(false);
+ const ref = useRef();
+
+ useEffect(() => {
+ if (autoFocus) {
+ ref.current?.focus();
+ }
+ }, [autoFocus]);
+
+ const wrappedOnFocus = useCallback(() => {
+ setIsFocused(true);
+ onFocus && onFocus();
+ }, [onFocus]);
+
+ const wrappedOnBlur = useCallback(() => {
+ setIsFocused(false);
+ onBlur && onBlur();
+ }, [onBlur]);
+
+ let borderColorOverride = {};
+ if (!inline && isFocused) {
+ if (error) {
+ borderColorOverride = { borderColor: colors.alert };
+ } else {
+ borderColorOverride = { borderColor: colors.live };
+ }
+ }
+
+ return (
+
+
+ {secureTextEntry ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export default withTheme(PasswordInput);
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: "row",
+ borderRadius: 4,
+ marginBottom: 16,
+ },
+ nonInlineContainer: {
+ borderWidth: 1,
+ },
+ inlineTextInput: {
+ fontSize: 20,
+ },
+ input: {
+ fontSize: 16,
+ paddingHorizontal: 16,
+ height: 48,
+ flex: 1,
+ },
+ iconInput: {
+ justifyContent: "center",
+ marginRight: 16,
+ },
+});
diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx
new file mode 100644
index 0000000000..a2559a1db6
--- /dev/null
+++ b/src/components/Pills.tsx
@@ -0,0 +1,31 @@
+import React, { memo } from "react";
+import { GraphTabs } from "@ledgerhq/native-ui";
+
+type Item = {
+ key: string;
+ label: string;
+ value?: any;
+};
+
+type Props = {
+ value: string;
+ items: Item[];
+ onChange: (value: Item) => void;
+ isDisabled?: boolean;
+};
+
+function Pills({ items, value, onChange, isDisabled }: Props) {
+ const activeIndex = items.findIndex(item => item.key === value);
+ return (
+ item.label)}
+ onChange={activeIndex => onChange(items[activeIndex])}
+ disabled={isDisabled}
+ size={"small"}
+ activeBg={"neutral.c40"}
+ />
+ );
+}
+
+export default memo(Pills);
diff --git a/src/components/RecipientInput.tsx b/src/components/RecipientInput.tsx
new file mode 100644
index 0000000000..be38e61636
--- /dev/null
+++ b/src/components/RecipientInput.tsx
@@ -0,0 +1,53 @@
+import { Flex } from "@ledgerhq/native-ui";
+import { PasteMedium } from "@ledgerhq/native-ui/assets/icons";
+import React, { ForwardedRef } from "react";
+import { useTranslation } from "react-i18next";
+import { TextInput as BaseTextInput } from "react-native";
+import { TouchableOpacity } from "react-native-gesture-handler";
+import styled from "styled-components/native";
+
+import TextInput, { Props as TextInputProps } from "./TextInput";
+
+const PasteButton = styled(TouchableOpacity).attrs(() => ({
+ activeOpacity: 0.6,
+}))`
+ background-color: ${p => p.theme.colors.neutral.c100};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 38px;
+ height: 38px;
+ border-radius: 38px;
+ border-width: 0;
+`;
+
+const PasteIcon = styled(PasteMedium).attrs(p => ({
+ color: p.theme.colors.neutral.c00,
+ size: 20,
+}))``;
+
+type Props = TextInputProps & {
+ ref?: ForwardedRef;
+ onPaste?: () => void;
+};
+
+const RecipientInput = ({ ref, onPaste, ...props }: Props) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ }
+ {...props}
+ />
+ );
+};
+
+export default RecipientInput;
diff --git a/src/components/RequireTerms.js b/src/components/RequireTerms.js
index 672cea34e2..66ea74bdc2 100644
--- a/src/components/RequireTerms.js
+++ b/src/components/RequireTerms.js
@@ -12,7 +12,7 @@ import {
import { useTheme } from "@react-navigation/native";
import { useTerms, useTermsAccept, url } from "../logic/terms";
import getWindowDimensions from "../logic/getWindowDimensions";
-import { useLocale } from "../context/Locale";
+import { useTranslationLocale } from "../context/Locale";
import LText from "./LText";
import SafeMarkdown from "./SafeMarkdown";
import Button from "./Button";
@@ -64,7 +64,7 @@ const styles = StyleSheet.create({
const RequireTermsModal = () => {
const { colors } = useTheme();
- const { locale } = useLocale();
+ const { locale } = useTranslationLocale();
const [markdown, error, retry] = useTerms(locale);
const [accepted, accept] = useTermsAccept();
const [toggle, setToggle] = useState(false);
@@ -147,7 +147,7 @@ export const TermModals = ({
close: () => void,
}) => {
const { colors } = useTheme();
- const { locale } = useLocale();
+ const { locale } = useTranslationLocale();
const [markdown, error, retry] = useTerms(locale);
const height = getWindowDimensions().height - 320;
diff --git a/src/components/RequireTerms.tsx b/src/components/RequireTerms.tsx
new file mode 100644
index 0000000000..07abea3d65
--- /dev/null
+++ b/src/components/RequireTerms.tsx
@@ -0,0 +1,196 @@
+import React, { useCallback, useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import {
+ StyleSheet,
+ View,
+ ScrollView,
+ Linking,
+ ActivityIndicator,
+} from "react-native";
+import { useTheme } from "@react-navigation/native";
+import { GraphGrowAltMedium } from "@ledgerhq/native-ui/assets/icons";
+import { BottomDrawer } from "@ledgerhq/native-ui";
+import { useTerms, useTermsAccept, url } from "../logic/terms";
+import getWindowDimensions from "../logic/getWindowDimensions";
+import { useTranslationLocale } from "../context/Locale";
+import LText from "./LText";
+import SafeMarkdown from "./SafeMarkdown";
+import Button from "./Button";
+import BottomModal from "./BottomModal";
+import ExternalLink from "./ExternalLink";
+import CheckBox from "./CheckBox";
+import Touchable from "./Touchable";
+import GenericErrorView from "./GenericErrorView";
+import RetryButton from "./RetryButton";
+
+const styles = StyleSheet.create({
+ modal: {},
+ root: {
+ paddingTop: 0,
+ padding: 16,
+ },
+ header: {
+ paddingVertical: 16,
+ },
+ title: {
+ textAlign: "center",
+
+ fontSize: 16,
+ },
+ body: {},
+ switchRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginVertical: 20,
+ },
+ switchLabel: {
+ marginLeft: 8,
+
+ fontSize: 13,
+ paddingRight: 16,
+ },
+ footer: {
+ flexDirection: "column",
+ justifyContent: "space-between",
+ borderTopWidth: 1,
+ },
+ footerClose: {
+ marginTop: 16,
+ },
+ retryButton: {
+ marginTop: 16,
+ },
+});
+
+const RequireTermsModal = () => {
+ const { colors } = useTheme();
+ const { locale } = useTranslationLocale();
+ const [markdown, error, retry] = useTerms(locale);
+ const [accepted, accept] = useTermsAccept();
+ const [toggle, setToggle] = useState(false);
+ const onSwitch = useCallback(() => {
+ setToggle(!toggle);
+ }, [toggle]);
+
+ const height = getWindowDimensions().height - 320;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {markdown ? (
+
+ ) : error ? (
+
+
+ }
+ onPress={() => Linking.openURL(url)}
+ event="OpenTerms"
+ />
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+ );
+};
+
+export default RequireTermsModal;
+
+export const TermModals = ({
+ isOpened,
+ close,
+}: {
+ isOpened: boolean;
+ close: () => void;
+}) => {
+ const { t } = useTranslation();
+
+ const { colors } = useTheme();
+ const { locale } = useTranslationLocale();
+ const [markdown, error, retry] = useTerms(locale);
+ const height = getWindowDimensions().height - 320;
+
+ const onClose = useCallback(() => {
+ close();
+ }, [close]);
+
+ return (
+
+
+ {markdown ? (
+
+ ) : error ? (
+
+
+ }
+ onPress={() => Linking.openURL(url)}
+ event="OpenTerms"
+ />
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/RequiresBLE/BluetoothDisabled.tsx b/src/components/RequiresBLE/BluetoothDisabled.tsx
new file mode 100644
index 0000000000..78694fa357
--- /dev/null
+++ b/src/components/RequiresBLE/BluetoothDisabled.tsx
@@ -0,0 +1,34 @@
+import React, { memo } from "react";
+import { Trans } from "react-i18next";
+import { IconBox, Text } from "@ledgerhq/native-ui";
+import { BluetoothMedium } from "@ledgerhq/native-ui/assets/icons";
+import styled from "styled-components/native";
+import { deviceNames } from "../../wording";
+
+const SafeAreaContainer = styled.SafeAreaView`
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ background-color: ${p => p.theme.colors.background.main};
+`;
+
+function BluetoothDisabled() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default memo<{}>(BluetoothDisabled);
diff --git a/src/components/RootNavigator/AccountSettingsNavigator.js b/src/components/RootNavigator/AccountSettingsNavigator.js
index 6a49fc95b8..90e545bc60 100644
--- a/src/components/RootNavigator/AccountSettingsNavigator.js
+++ b/src/components/RootNavigator/AccountSettingsNavigator.js
@@ -31,7 +31,7 @@ export default function AccountSettingsNavigator() {
component={AccountSettingsMain}
options={{
title: t("account.settings.header"),
- headerRight: closableNavconfig.headerRight,
+ headerRight: null,
}}
/>
getStackNavigatorConfig(colors), [
+ colors,
+ ]);
+ return (
+
+
+ ,
+ headerRight: () => ,
+ }}
+ />
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/AddAccountsNavigator.js b/src/components/RootNavigator/AddAccountsNavigator.js
index 2d6408e4d5..746534cdec 100644
--- a/src/components/RootNavigator/AddAccountsNavigator.js
+++ b/src/components/RootNavigator/AddAccountsNavigator.js
@@ -47,7 +47,6 @@ export default function AddAccountsNavigator({ route }: { route: Route }) {
screenOptions={{
...stackNavConfig,
headerRight: () => ,
- headerMode: "float",
}}
>
getLineTabNavigatorConfig(colors), [
+ colors,
+ ]);
+
+ // Fixme Typescript: Update react-native-tab-view to 3.1.1 to remove Tab.navigator ts error
+ return (
+
+ (
+
+ {t("analytics.allocation.title")}
+
+ ),
+ }}
+ />
+ (
+
+ {t("analytics.operations.title")}
+
+ ),
+ }}
+ />
+
+ );
+}
diff --git a/src/components/RootNavigator/BaseNavigator.js b/src/components/RootNavigator/BaseNavigator.js
index af12d0334e..242956ee96 100644
--- a/src/components/RootNavigator/BaseNavigator.js
+++ b/src/components/RootNavigator/BaseNavigator.js
@@ -7,6 +7,7 @@ import {
} from "@react-navigation/stack";
import { useTranslation } from "react-i18next";
import { useTheme } from "@react-navigation/native";
+import { Flex, Icons } from "@ledgerhq/native-ui";
import { ScreenName, NavigatorName } from "../../const";
import * as families from "../../families";
import OperationDetails, {
@@ -49,6 +50,8 @@ import LendingEnableFlowNavigator from "./LendingEnableFlowNavigator";
import LendingSupplyFlowNavigator from "./LendingSupplyFlowNavigator";
import LendingWithdrawFlowNavigator from "./LendingWithdrawFlowNavigator";
import NotificationCenterNavigator from "./NotificationCenterNavigator";
+// eslint-disable-next-line import/no-unresolved
+import AnalyticsNavigator from "./AnalyticsNavigator";
import NftNavigator from "./NftNavigator";
import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
import Account from "../../screens/Account";
@@ -69,6 +72,10 @@ import SwapFormSelectCurrency from "../../screens/Swap/FormSelection/SelectCurre
import SwapFormSelectFees from "../../screens/Swap/FormSelection/SelectFeesScreen";
import SwapFormSelectProviderRate from "../../screens/Swap/FormSelection/SelectProviderRateScreen";
+import BuyDeviceScreen from "../../screens/BuyDeviceScreen";
+import { readOnlyModeEnabledSelector } from "../../reducers/settings";
+import { useSelector } from "react-redux";
+
export default function BaseNavigator() {
const { t } = useTranslation();
const { colors } = useTheme();
@@ -76,11 +83,13 @@ export default function BaseNavigator() {
() => getStackNavigatorConfig(colors, true),
[colors],
);
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+
return (
+
({
+ headerBackImage: () => (
+
+
+
+ ),
headerStyle: styles.headerNoShadow,
title: route.params.name,
})}
@@ -273,18 +292,32 @@ export default function BaseNavigator() {
/>
+
,
headerRight: null,
@@ -400,7 +444,7 @@ export default function BaseNavigator() {
/>
({
headerLeft: () => (
@@ -481,28 +525,20 @@ export default function BaseNavigator() {
/>
,
- tabBarTestID: "TabBarManager",
- headerShown: false,
- }}
- listeners={({ navigation }) => ({
- tabPress: e => {
- e.preventDefault();
- // NB The default behaviour is not reset route params, leading to always having the same
- // search query or preselected tab after the first time (ie from Swap/Sell)
- // https://github.com/react-navigation/react-navigation/issues/6674#issuecomment-562813152
- navigation.navigate(NavigatorName.Manager, {
- screen: ScreenName.Manager,
- params: {
- tab: undefined,
- searchQuery: undefined,
- updateModalOpened: undefined,
+ {...(readOnlyModeEnabled
+ ? {
+ component: BuyDeviceScreen,
+ options: {
+ ...TransitionPresets.ModalTransition,
+ headerShown: false,
},
- });
- },
- })}
+ }
+ : {
+ component: ManagerNavigator,
+ options: {
+ headerShown: false,
+ },
+ })}
/>
{Object.keys(families).map(name => {
const { component, options } = families[name];
diff --git a/src/components/RootNavigator/BaseNavigator.tsx b/src/components/RootNavigator/BaseNavigator.tsx
new file mode 100644
index 0000000000..c97b664133
--- /dev/null
+++ b/src/components/RootNavigator/BaseNavigator.tsx
@@ -0,0 +1,559 @@
+import React, { useMemo } from "react";
+import {
+ createStackNavigator,
+ CardStyleInterpolators,
+ TransitionPresets,
+} from "@react-navigation/stack";
+import { useTranslation } from "react-i18next";
+import { Flex, Icons } from "@ledgerhq/native-ui";
+import { useSelector } from "react-redux";
+import { useTheme } from "styled-components/native";
+import { ScreenName, NavigatorName } from "../../const";
+import * as families from "../../families";
+import OperationDetails, {
+ BackButton,
+ CloseButton,
+} from "../../screens/OperationDetails";
+import PairDevices from "../../screens/PairDevices";
+import EditDeviceName from "../../screens/EditDeviceName";
+import Distribution from "../../screens/Distribution";
+import Asset, { HeaderTitle } from "../../screens/Asset";
+import ScanRecipient from "../../screens/SendFunds/ScanRecipient";
+import WalletConnectScan from "../../screens/WalletConnect/Scan";
+import WalletConnectConnect from "../../screens/WalletConnect/Connect";
+import WalletConnectDeeplinkingSelectAccount from "../../screens/WalletConnect/DeeplinkingSelectAccount";
+import FallbackCameraSend from "../FallbackCamera/FallbackCameraSend";
+import Main from "./MainNavigator";
+import { ErrorHeaderInfo } from "./BaseOnboardingNavigator";
+import SettingsNavigator from "./SettingsNavigator";
+import ReceiveFundsNavigator from "./ReceiveFundsNavigator";
+import SendFundsNavigator from "./SendFundsNavigator";
+import SignMessageNavigator from "./SignMessageNavigator";
+import SignTransactionNavigator from "./SignTransactionNavigator";
+import FreezeNavigator from "./FreezeNavigator";
+import UnfreezeNavigator from "./UnfreezeNavigator";
+import ClaimRewardsNavigator from "./ClaimRewardsNavigator";
+import AddAccountsNavigator from "./AddAccountsNavigator";
+import ExchangeBuyFlowNavigator from "./ExchangeBuyFlowNavigator";
+import ExchangeSellFlowNavigator from "./ExchangeSellFlowNavigator";
+import ExchangeNavigator from "./ExchangeNavigator";
+import FirmwareUpdateNavigator from "./FirmwareUpdateNavigator";
+import AccountSettingsNavigator from "./AccountSettingsNavigator";
+import ImportAccountsNavigator from "./ImportAccountsNavigator";
+import PasswordAddFlowNavigator from "./PasswordAddFlowNavigator";
+import PasswordModifyFlowNavigator from "./PasswordModifyFlowNavigator";
+import MigrateAccountsFlowNavigator from "./MigrateAccountsFlowNavigator";
+import SwapNavigator from "./SwapNavigator";
+import LendingNavigator from "./LendingNavigator";
+import LendingInfoNavigator from "./LendingInfoNavigator";
+import LendingEnableFlowNavigator from "./LendingEnableFlowNavigator";
+import LendingSupplyFlowNavigator from "./LendingSupplyFlowNavigator";
+import LendingWithdrawFlowNavigator from "./LendingWithdrawFlowNavigator";
+import NotificationCenterNavigator from "./NotificationCenterNavigator";
+import AnalyticsNavigator from "./AnalyticsNavigator";
+import NftNavigator from "./NftNavigator";
+import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
+import Account from "../../screens/Account";
+import TransparentHeaderNavigationOptions from "../../navigation/TransparentHeaderNavigationOptions";
+import styles from "../../navigation/styles";
+import HeaderRightClose from "../HeaderRightClose";
+import StepHeader from "../StepHeader";
+import AccountHeaderTitle from "../../screens/Account/AccountHeaderTitle";
+import AccountHeaderRight from "../../screens/Account/AccountHeaderRight";
+import PortfolioHistory from "../../screens/Portfolio/PortfolioHistory";
+import RequestAccountNavigator from "./RequestAccountNavigator";
+import VerifyAccount from "../../screens/VerifyAccount";
+import PlatformApp from "../../screens/Platform/App";
+import AccountsNavigator from "./AccountsNavigator";
+
+import SwapFormSelectAccount from "../../screens/Swap/FormSelection/SelectAccountScreen";
+import SwapFormSelectCurrency from "../../screens/Swap/FormSelection/SelectCurrencyScreen";
+import SwapFormSelectFees from "../../screens/Swap/FormSelection/SelectFeesScreen";
+import SwapFormSelectProviderRate from "../../screens/Swap/FormSelection/SelectProviderRateScreen";
+
+import BuyDeviceScreen from "../../screens/BuyDeviceScreen";
+import { readOnlyModeEnabledSelector } from "../../reducers/settings";
+import useFeature from "@ledgerhq/live-common/lib/featureFlags/useFeature";
+import Learn from "../../screens/Learn";
+import ManagerMain from "../../screens/Manager/Manager";
+
+export default function BaseNavigator() {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const stackNavigationConfig = useMemo(
+ () => getStackNavigatorConfig(colors, true),
+ [colors],
+ );
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+ const learn = useFeature("learn");
+
+ return (
+
+
+
+
+
+
+ ({
+ headerBackImage: () => (
+
+
+
+ ),
+ headerStyle: styles.headerNoShadow,
+ title: route.params.name,
+ })}
+ />
+ {learn?.enabled ? (
+
+ ) : null}
+
+
+
+ ({
+ headerTitle: () => (
+
+ ),
+ headerRight: null,
+ })}
+ />
+ ,
+ headerRight: null,
+ }}
+ />
+ (
+
+ ),
+ headerRight: null,
+ }}
+ />
+ (
+
+ ),
+ headerRight: null,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ ({
+ beforeRemove: () => {
+ /**
+ react-navigation workaround try to fetch params from current route params
+ or fallback to child navigator route params
+ since this listener is on top of another navigator
+ */
+ const onError =
+ route.params?.onError || route.params?.params?.onError;
+ // @TODO replace with correct error
+ if (onError && typeof onError === "function")
+ onError(
+ route.params.error ||
+ new Error("Request account interrupted by user"),
+ );
+ },
+ })}
+ />
+ ({
+ beforeRemove: () => {
+ const onClose =
+ route.params?.onClose || route.params?.params?.onClose;
+ if (onClose && typeof onClose === "function") {
+ onClose();
+ }
+ },
+ })}
+ />
+
+
+
+
+ {
+ if (route.params?.isSubOperation) {
+ return {
+ headerTitle: () => (
+
+ ),
+ headerLeft: () => ,
+ headerRight: () => ,
+ };
+ }
+
+ return {
+ headerTitle: () => (
+
+ ),
+ headerLeft: () => ,
+ headerRight: null,
+ };
+ }}
+ />
+
+
+ ({
+ title: null,
+ headerRight: () => (
+
+ ),
+ headerShown: true,
+ headerStyle: styles.headerNoShadow,
+ })}
+ />
+
+
+
+
+
+
+ ,
+ headerRight: null,
+ }}
+ />
+
+ ({
+ headerLeft: () => (
+
+ ),
+ headerTitle: () => ,
+ headerRight: () => ,
+ })}
+ />
+ (
+
+ ),
+ headerLeft: null,
+ }}
+ />
+ (
+
+ ),
+ headerLeft: null,
+ }}
+ />
+ ,
+ headerLeft: null,
+ }}
+ />
+
+
+ ({
+ title: t("notificationCenter.title"),
+ headerLeft: null,
+ headerRight: () => ,
+ cardStyleInterpolator: CardStyleInterpolators.forVerticalIOS,
+ })}
+ />
+ ({
+ title: null,
+ headerRight: null,
+ headerLeft: () => ,
+ })}
+ />
+
+
+ {Object.keys(families).map(name => {
+ const { component, options } = families[name];
+ return (
+
+ );
+ })}
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/CustomBlockRouterNavigator.tsx b/src/components/RootNavigator/CustomBlockRouterNavigator.tsx
new file mode 100644
index 0000000000..4d3d139fa2
--- /dev/null
+++ b/src/components/RootNavigator/CustomBlockRouterNavigator.tsx
@@ -0,0 +1,47 @@
+// @flow
+import { useEffect, useState } from "react";
+import { BehaviorSubject } from "rxjs";
+
+export const lockSubject = new BehaviorSubject(false);
+
+export function useIsNavLocked(): boolean {
+ const [isLocked, setIsLocked] = useState(false);
+
+ useEffect(() => {
+ const subscription = lockSubject.subscribe(val => {
+ setIsLocked(val);
+ });
+
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, []);
+
+ return isLocked;
+}
+
+/** use Effect to trigger lock navigation updates and callback to retrieve catched navigation actions */
+export const useLockNavigation = (
+ when: boolean,
+ callback: (...args: any[]) => void = () => {},
+ navigation: any,
+) => {
+ useEffect(() => {
+ lockSubject.next(when);
+ navigation.addListener("beforeRemove", (e: any) => {
+ if (!when) {
+ // If we don't have unsaved changes, then we don't need to do anything
+ return;
+ }
+
+ // Prevent default behavior of leaving the screen
+ e.preventDefault();
+
+ callback(e.data.action);
+ });
+
+ return () => {
+ navigation.removeListener("beforeRemove");
+ };
+ }, [callback, navigation, when]);
+};
diff --git a/src/components/RootNavigator/PlatformNavigator.js b/src/components/RootNavigator/DiscoverNavigator.ios.tsx
similarity index 78%
rename from src/components/RootNavigator/PlatformNavigator.js
rename to src/components/RootNavigator/DiscoverNavigator.ios.tsx
index a1515bd4e3..2fc4ed69ff 100644
--- a/src/components/RootNavigator/PlatformNavigator.js
+++ b/src/components/RootNavigator/DiscoverNavigator.ios.tsx
@@ -4,20 +4,21 @@ import React, { useMemo } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useTheme } from "@react-navigation/native";
import { ScreenName } from "../../const";
-import PlatformCatalog from "../../screens/Platform";
import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
+import Discover from "../../screens/Discover";
-export default function PlatformNavigator() {
+export default function DiscoverNavigator() {
const { colors } = useTheme();
const stackNavigationConfig = useMemo(
() => getStackNavigatorConfig(colors, true),
[colors],
);
+
return (
getStackNavigatorConfig(colors, true),
+ [colors],
+ );
+ const learn = useFeature("learn");
+
+ return (
+
+
+
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/ExchangeBuyFlowNavigator.js b/src/components/RootNavigator/ExchangeBuyFlowNavigator.js
index 9a09c96a63..c589f700af 100644
--- a/src/components/RootNavigator/ExchangeBuyFlowNavigator.js
+++ b/src/components/RootNavigator/ExchangeBuyFlowNavigator.js
@@ -23,7 +23,6 @@ export default function ExchangeNavigator() {
screenOptions={{
...stackNavigationConfig,
headerRight: () => ,
- headerMode: "float",
}}
>
getLineTabNavigatorConfig(colors), [
+ colors,
+ ]);
+ return (
+
+ (
+
+ {t("exchange.buy.tabTitle")}
+
+ ),
+ }}
+ />
+ (
+
+ {t("exchange.sell.tabTitle")}
+
+ ),
+ }}
+ />
+
+ );
+}
+
+const Tab = createMaterialTopTabNavigator();
diff --git a/src/components/RootNavigator/ExchangeSellFlowNavigator.js b/src/components/RootNavigator/ExchangeSellFlowNavigator.js
index 43c2a92e38..44c3fd3a31 100644
--- a/src/components/RootNavigator/ExchangeSellFlowNavigator.js
+++ b/src/components/RootNavigator/ExchangeSellFlowNavigator.js
@@ -23,7 +23,6 @@ export default function ExchangeNavigator() {
screenOptions={{
...stackNavigationConfig,
headerRight: () => ,
- headerMode: "float",
}}
>
,
headerLeft: null,
+ headerTitleStyle: { color: "#fff" },
}}
/>
,
}}
/>
@@ -47,7 +48,7 @@ export default function ImportAccountsNavigator() {
name={ScreenName.FallBackCameraScreen}
component={FallBackCameraScreen}
options={{
- title: t("account.import.fallback.header"),
+ headerTitle: t("account.import.fallback.header"),
}}
/>
diff --git a/src/components/RootNavigator/ImportAccountsNavigator.tsx b/src/components/RootNavigator/ImportAccountsNavigator.tsx
new file mode 100644
index 0000000000..e895a3f41a
--- /dev/null
+++ b/src/components/RootNavigator/ImportAccountsNavigator.tsx
@@ -0,0 +1,68 @@
+// @flow
+import React, { useMemo } from "react";
+import { createStackNavigator } from "@react-navigation/stack";
+import { useTranslation } from "react-i18next";
+import { Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import { ScreenName } from "../../const";
+import ScanAccounts from "../../screens/ImportAccounts/Scan";
+import DisplayResult, {
+ BackButton,
+} from "../../screens/ImportAccounts/DisplayResult";
+import FallBackCameraScreen from "../../screens/ImportAccounts/FallBackCameraScreen";
+import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
+import TransparentHeaderNavigationOptions from "../../navigation/TransparentHeaderNavigationOptions";
+import HeaderRightClose from "../HeaderRightClose";
+
+export default function ImportAccountsNavigator() {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const stackNavigationConfig = useMemo(
+ () => getStackNavigatorConfig(colors, true),
+ [colors],
+ );
+ return (
+
+ (
+
+ {t("account.import.scan.title")}
+
+ ),
+ headerRight: props => ,
+ headerLeft: null,
+ }}
+ />
+
+ {t("account.import.result.title")}
+
+ ),
+ headerLeft: () => ,
+ }}
+ />
+
+ {t("account.import.fallback.header")}
+
+ ),
+ }}
+ />
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/LendingInfoNavigator.js b/src/components/RootNavigator/LendingInfoNavigator.js
index 48dcc7d71c..713c871616 100644
--- a/src/components/RootNavigator/LendingInfoNavigator.js
+++ b/src/components/RootNavigator/LendingInfoNavigator.js
@@ -44,7 +44,6 @@ export default function LendingInfoNavigator() {
headerLeft: null,
headerRight: () => ,
gestureEnabled: false,
- headerMode: "float",
})}
>
+ ,
+ }}
+ />
+ (
+
+ ),
+ }}
+ listeners={({ navigation }) => ({
+ tabPress: (e: any) => {
+ e.preventDefault();
+ // NB The default behaviour is not reset route params, leading to always having the same
+ // search query or preselected tab after the first time (ie from Swap/Sell)
+ // https://github.com/react-navigation/react-navigation/issues/6674#issuecomment-562813152
+ navigation.navigate(NavigatorName.Market, {
+ screen: ScreenName.MarketList,
+ });
+ },
+ })}
+ />
+
+ ,
+ }}
+ />
+ (
+
+ ),
+ }}
+ listeners={({ navigation }) => ({
+ tabPress: (e: any) => {
+ e.preventDefault();
+ // NB The default behaviour is not reset route params, leading to always having the same
+ // search query or preselected tab after the first time (ie from Swap/Sell)
+ // https://github.com/react-navigation/react-navigation/issues/6674#issuecomment-562813152
+ navigation.navigate(NavigatorName.Discover, {
+ screen: ScreenName.DiscoverScreen,
+ });
+ },
+ })}
+ />
+ ,
+ tabBarTestID: "TabBarManager",
+ }}
+ listeners={({ navigation }) => ({
+ tabPress: (e: any) => {
+ e.preventDefault();
+ if (readOnlyModeEnabled) {
+ // NB The default behaviour is not reset route params, leading to always having the same
+ // search query or preselected tab after the first time (ie from Swap/Sell)
+ // https://github.com/react-navigation/react-navigation/issues/6674#issuecomment-562813152
+ navigation.navigate(ScreenName.BuyDeviceScreen, {
+ from: NavigatorName.Manager,
+ });
+ } else {
+ // NB The default behaviour is not reset route params, leading to always having the same
+ // search query or preselected tab after the first time (ie from Swap/Sell)
+ // https://github.com/react-navigation/react-navigation/issues/6674#issuecomment-562813152
+ navigation.navigate(NavigatorName.Manager, {
+ screen: ScreenName.Manager,
+ params: {
+ tab: undefined,
+ searchQuery: undefined,
+ updateModalOpened: undefined,
+ },
+ });
+ }
+ },
+ })}
+ />
+
+ );
+}
diff --git a/src/components/RootNavigator/ManagerNavigator.tsx b/src/components/RootNavigator/ManagerNavigator.tsx
new file mode 100644
index 0000000000..f1e277b09c
--- /dev/null
+++ b/src/components/RootNavigator/ManagerNavigator.tsx
@@ -0,0 +1,106 @@
+// @flow
+import React, { useMemo } from "react";
+import { TouchableOpacity } from "react-native";
+import styled, { useTheme } from "styled-components/native";
+import { createStackNavigator } from "@react-navigation/stack";
+import { useSelector } from "react-redux";
+import { useTranslation } from "react-i18next";
+import { NanoFoldedMedium } from "@ledgerhq/native-ui/assets/icons";
+import { ScreenName } from "../../const";
+import { hasAvailableUpdateSelector } from "../../reducers/settings";
+import Manager from "../../screens/Manager";
+import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
+import styles from "../../navigation/styles";
+import ReadOnlyTab from "../ReadOnlyTab";
+import NanoXIcon from "../../icons/TabNanoX";
+import { useIsNavLocked } from "./CustomBlockRouterNavigator";
+
+import { Box, Icons, Flex } from "@ledgerhq/native-ui";
+
+const BadgeContainer = styled(Flex).attrs({
+ position: "absolute",
+ top: -1,
+ right: -1,
+ width: 14,
+ height: 14,
+ borderRadius: 7,
+ borderWidth: 3,
+})``;
+
+const Badge = () => {
+ const { colors } = useTheme();
+ return (
+
+ );
+};
+
+const ManagerIconWithUpate = ({
+ color,
+ size,
+}: {
+ color: string;
+ size: number;
+}) => (
+
+
+
+
+);
+
+export default function ManagerNavigator() {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const stackNavConfig = useMemo(() => getStackNavigatorConfig(colors), [
+ colors,
+ ]);
+ return (
+
+
+
+ );
+}
+
+const Stack = createStackNavigator();
+
+export function ManagerTabIcon(props: any) {
+ const isNavLocked = useIsNavLocked();
+ const hasAvailableUpdate = useSelector(hasAvailableUpdateSelector);
+
+ const content = (
+
+ );
+
+ if (isNavLocked) {
+ return {}}>{content};
+ }
+
+ return content;
+}
diff --git a/src/components/RootNavigator/MarketNavigator.js b/src/components/RootNavigator/MarketNavigator.js
index 1c85ede098..48254eefbd 100644
--- a/src/components/RootNavigator/MarketNavigator.js
+++ b/src/components/RootNavigator/MarketNavigator.js
@@ -22,7 +22,11 @@ export default function MarketNavigator() {
[colors],
);
return (
-
+
diff --git a/src/components/RootNavigator/NotificationCenterNavigator.tsx b/src/components/RootNavigator/NotificationCenterNavigator.tsx
new file mode 100644
index 0000000000..781d5a9a79
--- /dev/null
+++ b/src/components/RootNavigator/NotificationCenterNavigator.tsx
@@ -0,0 +1,101 @@
+import React, { useState } from "react";
+import {
+ createMaterialTopTabNavigator,
+ MaterialTopTabBarProps,
+} from "@react-navigation/material-top-tabs";
+import { useTranslation } from "react-i18next";
+import { useAnnouncements } from "@ledgerhq/live-common/lib/notifications/AnnouncementProvider";
+
+import { Flex } from "@ledgerhq/native-ui";
+import styled from "styled-components/native";
+import { TabsContainer } from "@ledgerhq/native-ui/components/Tabs/TemplateTabs";
+import { ChipTab } from "@ledgerhq/native-ui/components/Tabs/Chip";
+import NotificationCenterStatus from "../../screens/NotificationCenter/Status";
+import NotificationCenterNews from "../../screens/NotificationCenter/News";
+import { ScreenName } from "../../const";
+
+const Tab = createMaterialTopTabNavigator();
+
+const TabBarContainer = styled(Flex)`
+ border-bottom-width: 1px;
+ border-bottom-color: ${p => p.theme.colors.palette.neutral.c40};
+ background-color: ${p => p.theme.colors.palette.background.main};
+`;
+
+function TabBar({ state, descriptors, navigation }: MaterialTopTabBarProps) {
+ return (
+
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label = options.title;
+
+ const isActive = state.index === index;
+
+ const onPress = () => {
+ const event = navigation.emit({
+ type: "tabPress",
+ target: route.key,
+ canPreventDefault: true,
+ });
+
+ if (!isActive && !event.defaultPrevented) {
+ navigation.navigate(route.name);
+ }
+ };
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
+
+export default function NotificationCenterNavigator() {
+ const { t } = useTranslation();
+ const { allIds, seenIds } = useAnnouncements();
+ const [notificationsCount] = useState(allIds.length - seenIds.length);
+
+ // Fixme Typescript: Update react-native-tab-view to 3.1.1 to remove Tab.navigator ts error
+ return (
+ <>
+ {/* @ts-ignore */}
+ }>
+ 0
+ ? "notificationCenter.news.titleCount"
+ : "notificationCenter.news.title",
+ {
+ count: notificationsCount,
+ },
+ ),
+ }}
+ />
+
+
+ >
+ );
+}
diff --git a/src/components/RootNavigator/OnboardingNavigator.tsx b/src/components/RootNavigator/OnboardingNavigator.tsx
new file mode 100644
index 0000000000..edfd957340
--- /dev/null
+++ b/src/components/RootNavigator/OnboardingNavigator.tsx
@@ -0,0 +1,268 @@
+import React from "react";
+import {
+ createStackNavigator,
+ CardStyleInterpolators,
+ TransitionPresets,
+ StackNavigationOptions,
+ StackScreenProps,
+} from "@react-navigation/stack";
+import { Flex } from "@ledgerhq/native-ui";
+import { useTranslation } from "react-i18next";
+import { ScreenName, NavigatorName } from "../../const";
+import PasswordAddFlowNavigator from "./PasswordAddFlowNavigator";
+import OnboardingWelcome from "../../screens/Onboarding/steps/welcome";
+import OnboardingLanguage from "../../screens/Onboarding/steps/language";
+import OnboardingTerms from "../../screens/Onboarding/steps/terms";
+import OnboardingDeviceSelection from "../../screens/Onboarding/steps/deviceSelection";
+import OnboardingUseCase from "../../screens/Onboarding/steps/useCaseSelection";
+import OnboardingNewDeviceInfo from "../../screens/Onboarding/steps/newDeviceInfo";
+import OnboardingNewDiscoverLiveInfo from "../../screens/Onboarding/steps/discoverLiveInfo";
+import OnboardingNewDevice from "../../screens/Onboarding/steps/setupDevice";
+import OnboardingRecoveryPhrase from "../../screens/Onboarding/steps/recoveryPhrase";
+import OnboardingInfoModal from "../OnboardingStepperView/OnboardingInfoModal";
+
+import OnboardingPairNew from "../../screens/Onboarding/steps/pairNew";
+import OnboardingImportAccounts from "../../screens/Onboarding/steps/importAccounts";
+import OnboardingFinish from "../../screens/Onboarding/steps/finish";
+import OnboardingPreQuizModal from "../../screens/Onboarding/steps/setupDevice/drawers/OnboardingPreQuizModal";
+import OnboardingQuiz from "../../screens/Onboarding/OnboardingQuiz";
+import OnboardingQuizFinal from "../../screens/Onboarding/OnboardingQuizFinal";
+import NavigationHeader from "../NavigationHeader";
+import NavigationOverlay from "../NavigationOverlay";
+import NavigationModalContainer from "../NavigationModalContainer";
+import OnboardingSetupDeviceInformation from "../../screens/Onboarding/steps/setupDevice/drawers/SecurePinCode";
+import OnboardingSetupDeviceRecoveryPhrase from "../../screens/Onboarding/steps/setupDevice/drawers/SecureRecoveryPhrase";
+import OnboardingGeneralInformation from "../../screens/Onboarding/steps/setupDevice/drawers/GeneralInformation";
+import OnboardingBluetoothInformation from "../../screens/Onboarding/steps/setupDevice/drawers/BluetoothConnection";
+import OnboardingWarning from "../../screens/Onboarding/steps/setupDevice/drawers/Warning";
+import OnboardingSyncDesktopInformation from "../../screens/Onboarding/steps/setupDevice/drawers/SyncDesktopInformation";
+import OnboardingRecoveryPhraseWarning from "../../screens/Onboarding/steps/setupDevice/drawers/RecoveryPhraseWarning";
+import PostWelcomeSelection from "../../screens/Onboarding/steps/postWelcomeSelection";
+
+const Stack = createStackNavigator();
+const OnboardingCarefulWarningStack = createStackNavigator();
+const OnboardingPreQuizModalStack = createStackNavigator();
+
+function OnboardingCarefulWarning(props: StackScreenProps<{}>) {
+ const options: Partial = {
+ header: props => (
+ // TODO: Replace this value with constant.purple as soon as the value is fixed in the theme
+
+
+
+ ),
+ headerStyle: { backgroundColor: "transparent" },
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+function OnboardingPreQuizModalNavigator(props: StackScreenProps<{}>) {
+ const options: Partial = {
+ header: props => (
+ // TODO: Replace this value with constant.purple as soon as the value is fixed in the theme
+
+
+
+ ),
+ headerStyle: {},
+ headerShadowVisible: false,
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+const modalOptions: Partial = {
+ presentation: "transparentModal",
+ cardOverlayEnabled: true,
+ cardOverlay: () => ,
+ headerShown: false,
+ ...TransitionPresets.ModalTransition,
+};
+
+const infoModalOptions: Partial = {
+ ...TransitionPresets.ModalTransition,
+ headerShown: true,
+};
+
+export default function OnboardingNavigator() {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/RootNavigator/ReceiveFundsNavigator.js b/src/components/RootNavigator/ReceiveFundsNavigator.js
index 79b409f22f..ff3772d173 100644
--- a/src/components/RootNavigator/ReceiveFundsNavigator.js
+++ b/src/components/RootNavigator/ReceiveFundsNavigator.js
@@ -25,7 +25,6 @@ export default function ReceiveFundsNavigator() {
screenOptions={{
...stackNavigationConfig,
gestureEnabled: Platform.OS === "ios",
- headerMode: "float",
}}
>
getStackNavigatorConfig(colors, true),
+ [colors],
+ );
+ return (
+
+ (
+
+ ),
+ }}
+ initialParams={{
+ next: ScreenName.ReceiveConnectDevice,
+ category: "ReceiveFunds",
+ }}
+ />
+ ({
+ headerTitle: () => (
+
+ ),
+ })}
+ />
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/RequestAccountNavigator.js b/src/components/RootNavigator/RequestAccountNavigator.js
index a211621bde..569baaf12c 100644
--- a/src/components/RootNavigator/RequestAccountNavigator.js
+++ b/src/components/RootNavigator/RequestAccountNavigator.js
@@ -23,7 +23,6 @@ export default function RequestAccountNavigator() {
getStackNavigatorConfig(colors, true),
+ [colors],
+ );
+ return (
+
+ (
+
+ ),
+ }}
+ initialParams={{
+ next: ScreenName.SendSelectRecipient,
+ category: "SendFunds",
+ notEmptyAccounts: true,
+ minBalance: 0,
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ initialParams={{
+ analyticsPropertyFlow: "send",
+ }}
+ />
+
+
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/SettingsNavigator.js b/src/components/RootNavigator/SettingsNavigator.js
index 7a79dfa5f2..211acb7ea5 100644
--- a/src/components/RootNavigator/SettingsNavigator.js
+++ b/src/components/RootNavigator/SettingsNavigator.js
@@ -49,7 +49,7 @@ export default function SettingsNavigator() {
return (
getStackNavigatorConfig(colors), [
+ colors,
+ ]);
+ return (
+
+ ,
+ }}
+ />
+
+
+
+
+
+
+
+
+ ({
+ title: route.params.headerTitle,
+ headerRight: null,
+ })}
+ />
+
+
+
+
+
+
+
+
+
+ ({
+ title: "Debug BLE",
+ headerRight: () => (
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/RootNavigator/SwapFormNavigator.tsx b/src/components/RootNavigator/SwapFormNavigator.tsx
new file mode 100644
index 0000000000..9a4bdcd61e
--- /dev/null
+++ b/src/components/RootNavigator/SwapFormNavigator.tsx
@@ -0,0 +1,70 @@
+import React, { useMemo } from "react";
+import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
+import { useTranslation } from "react-i18next";
+
+import { Account, AccountLike } from "@ledgerhq/live-common/lib/types/account";
+
+import { useTheme } from "styled-components/native";
+import { Text } from "@ledgerhq/native-ui";
+import { ScreenName } from "../../const";
+import Swap from "../../screens/Swap";
+import History from "../../screens/Swap/History";
+import { getLineTabNavigatorConfig } from "../../navigation/tabNavigatorConfig";
+
+type TabLabelProps = {
+ focused: boolean;
+ color: string;
+};
+
+type RouteParams = {
+ defaultAccount?: AccountLike;
+ defaultParentAccount?: Account;
+ providers: any;
+ provider: string;
+};
+
+export default function SwapFormNavigator({
+ route,
+}: {
+ route: { params: RouteParams };
+}) {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const { params: routeParams } = route;
+
+ const tabNavigationConfig = useMemo(() => getLineTabNavigatorConfig(colors), [
+ colors,
+ ]);
+
+ return (
+
+ (
+
+ {t("transfer.swap.form.tab")}
+
+ ),
+ }}
+ >
+ {_props => }
+
+ (
+
+ {t("transfer.swap.history.tab")}
+
+ ),
+ }}
+ />
+
+ );
+}
+
+const Tab = createMaterialTopTabNavigator();
diff --git a/src/components/RootNavigator/SwapNavigator.tsx b/src/components/RootNavigator/SwapNavigator.tsx
new file mode 100644
index 0000000000..8d54e530e3
--- /dev/null
+++ b/src/components/RootNavigator/SwapNavigator.tsx
@@ -0,0 +1,91 @@
+import React, { useMemo } from "react";
+import { createStackNavigator } from "@react-navigation/stack";
+
+import { useTranslation } from "react-i18next";
+import { useTheme } from "styled-components/native";
+import { ScreenName } from "../../const";
+import SwapError from "../../screens/Swap/Error";
+import SwapKYC from "../../screens/Swap/KYC";
+import SwapKYCStates from "../../screens/Swap/KYC/StateSelect";
+import Swap from "../../screens/Swap/SwapEntry";
+import SwapFormNavigator from "./SwapFormNavigator";
+import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
+import StepHeader from "../StepHeader";
+import SwapOperationDetails from "../../screens/Swap/OperationDetails";
+import { BackButton } from "../../screens/OperationDetails";
+import SwapPendingOperation from "../../screens/Swap/PendingOperation";
+
+export default function SwapNavigator() {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const stackNavigationConfig = useMemo(
+ () => getStackNavigatorConfig(colors, true),
+ [colors],
+ );
+
+ return (
+
+
+
+ ,
+ headerRight: null,
+ }}
+ />
+ (
+
+ ),
+ headerRight: null,
+ }}
+ />
+ ,
+ headerLeft: null,
+ }}
+ />
+ ,
+ headerLeft: null,
+ }}
+ />
+ ({
+ headerTitle: () => ,
+ headerLeft: () => ,
+ headerRight: null,
+ })}
+ />
+
+ );
+}
+
+const Stack = createStackNavigator();
diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx
new file mode 100644
index 0000000000..4f749543f0
--- /dev/null
+++ b/src/components/SectionHeader.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import styled from "styled-components/native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import FormatDay from "./FormatDay";
+
+type Props = {
+ section: {
+ day: Date;
+ };
+ withoutMarginBottom?: boolean;
+};
+
+const Container = styled(Flex).attrs(p => ({
+ backgroundColor: "neutral.c30",
+ padding: 5,
+ borderRadius: 2,
+ marginTop: 7,
+ marginBottom: !p.withoutMarginBottom && 3,
+}))``;
+
+export default function SectionHeader({
+ section,
+ withoutMarginBottom = false,
+}: Props) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/SelectDevice/BluetoothEmpty.tsx b/src/components/SelectDevice/BluetoothEmpty.tsx
new file mode 100644
index 0000000000..c74eb3967d
--- /dev/null
+++ b/src/components/SelectDevice/BluetoothEmpty.tsx
@@ -0,0 +1,58 @@
+import React, { memo } from "react";
+import { View, StyleSheet } from "react-native";
+import { Trans } from "react-i18next";
+
+import { Box, Button, Flex, Text } from "@ledgerhq/native-ui";
+import { BluetoothMedium } from "@ledgerhq/native-ui/assets/icons";
+
+import lottie from "../../screens/Onboarding/assets/nanoX/pairDevice/dark.json";
+
+import Animation from "../Animation";
+
+type Props = {
+ onPairNewDevice: () => void;
+ hideAnimation?: boolean;
+};
+
+function BluetoothEmpty({ onPairNewDevice, hideAnimation }: Props) {
+ return (
+ <>
+ {hideAnimation ? null : (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ imageContainer: {
+ minHeight: 200,
+ position: "relative",
+ overflow: "visible",
+ },
+ image: {
+ position: "absolute",
+ left: "5%",
+ top: 0,
+ width: "110%",
+ height: "100%",
+ },
+});
+
+export default memo(BluetoothEmpty);
diff --git a/src/components/SelectDevice/DeviceItem.tsx b/src/components/SelectDevice/DeviceItem.tsx
new file mode 100644
index 0000000000..30b1f7b546
--- /dev/null
+++ b/src/components/SelectDevice/DeviceItem.tsx
@@ -0,0 +1,76 @@
+import React, { memo, useMemo, useCallback } from "react";
+import invariant from "invariant";
+import { TouchableOpacity } from "react-native";
+import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
+import {
+ NanoFoldedMedium,
+ ToolsMedium,
+ OthersMedium,
+} from "@ledgerhq/native-ui/assets/icons";
+import { SelectableList, Text } from "@ledgerhq/native-ui";
+import { IconType } from "@ledgerhq/native-ui/components/Icon/type";
+
+type Props = {
+ deviceMeta: Device;
+ disabled?: boolean;
+ withArrow?: boolean;
+ description?: React.ReactNode;
+ onSelect?: (arg0: Device) => any;
+ onBluetoothDeviceAction?: (arg0: Device) => any;
+};
+
+const iconByFamily: Record = {
+ httpdebug: ToolsMedium,
+};
+
+function DeviceItem({
+ deviceMeta,
+ onSelect,
+ disabled,
+ description,
+ onBluetoothDeviceAction,
+}: Props) {
+ const onPress = useCallback(() => {
+ invariant(onSelect, "onSelect required");
+ return onSelect(deviceMeta);
+ }, [deviceMeta, onSelect]);
+
+ const family = deviceMeta.deviceId.split("|")[0];
+ const CustomIcon = !!family && iconByFamily[family];
+
+ const onMore = useMemo(
+ () =>
+ family !== "usb" && !!onBluetoothDeviceAction
+ ? () => onBluetoothDeviceAction(deviceMeta)
+ : undefined,
+ [family, onBluetoothDeviceAction, deviceMeta],
+ );
+
+ const renderOnMore = (
+
+
+
+ );
+
+ return (
+
+ {deviceMeta.deviceName}
+ {description && (
+
+ {" "}
+ ({description})
+
+ )}
+
+ );
+}
+
+export default memo(DeviceItem);
diff --git a/src/components/SelectDevice/USBEmpty.android.tsx b/src/components/SelectDevice/USBEmpty.android.tsx
new file mode 100644
index 0000000000..e593455f35
--- /dev/null
+++ b/src/components/SelectDevice/USBEmpty.android.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import { Trans } from "react-i18next";
+import { Box, Flex, Text } from "@ledgerhq/native-ui";
+import { UsbMedium } from "@ledgerhq/native-ui/assets/icons";
+
+export default function USBEmpty({ usbOnly }: { usbOnly: boolean }) {
+ return (
+
+
+
+ {!usbOnly && (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/SelectDevice/index.tsx b/src/components/SelectDevice/index.tsx
new file mode 100644
index 0000000000..54e470d403
--- /dev/null
+++ b/src/components/SelectDevice/index.tsx
@@ -0,0 +1,261 @@
+import React, { useCallback, useEffect, useState } from "react";
+import { StyleSheet, View, Platform, NativeModules } from "react-native";
+import Config from "react-native-config";
+import { useSelector, useDispatch } from "react-redux";
+import { Trans } from "react-i18next";
+import { useNavigation } from "@react-navigation/native";
+import { discoverDevices, TransportModule } from "@ledgerhq/live-common/lib/hw";
+import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
+import { Button } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import { ScreenName } from "../../const";
+import { knownDevicesSelector } from "../../reducers/ble";
+import { setHasConnectedDevice } from "../../actions/appstate";
+import DeviceItem from "./DeviceItem";
+import BluetoothEmpty from "./BluetoothEmpty";
+import USBEmpty from "./USBEmpty";
+import LText from "../LText";
+import Animation from "../Animation";
+
+import lottieUsb from "../../screens/Onboarding/assets/nanoS/plugDevice/dark.json";
+import { track } from "../../analytics";
+
+type Props = {
+ onBluetoothDeviceAction?: (device: Device) => void;
+ onSelect: (device: Device) => void;
+ onWithoutDevice?: () => void;
+ withArrows?: boolean;
+ usbOnly?: boolean;
+ filter?: (transportModule: TransportModule) => boolean;
+ autoSelectOnAdd?: boolean;
+ hideAnimation?: boolean;
+};
+
+export default function SelectDevice({
+ usbOnly,
+ withArrows,
+ filter = () => true,
+ onSelect,
+ onWithoutDevice,
+ onBluetoothDeviceAction,
+ autoSelectOnAdd,
+ hideAnimation,
+}: Props) {
+ const { colors } = useTheme();
+ const navigation = useNavigation();
+ const knownDevices = useSelector(knownDevicesSelector);
+ const dispatch = useDispatch();
+
+ const handleOnSelect = useCallback(
+ deviceInfo => {
+ NativeModules.BluetoothHelperModule.prompt()
+ .then(() => {
+ const { modelId, wired } = deviceInfo;
+ track("Device selection", {
+ modelId,
+ connectionType: wired ? "USB" : "BLE",
+ });
+ // Nb consider a device selection enough to show the fw update banner in portfolio
+ dispatch(setHasConnectedDevice(true));
+ onSelect(deviceInfo);
+ })
+ .catch(() => {
+ /* ignore */
+ });
+ },
+ [dispatch, onSelect],
+ );
+
+ const [devices, setDevices] = useState([]);
+
+ const onPairNewDevice = useCallback(() => {
+ NativeModules.BluetoothHelperModule.prompt()
+ .then(() =>
+ // @ts-expect-error navigation issue
+ navigation.navigate(ScreenName.PairDevices, {
+ onDone: autoSelectOnAdd ? handleOnSelect : null,
+ }),
+ )
+ .catch(() => {
+ /* ignore */
+ });
+ }, [autoSelectOnAdd, navigation, handleOnSelect]);
+
+ const renderItem = useCallback(
+ (item: Device) => (
+
+ ),
+ [withArrows, onBluetoothDeviceAction, handleOnSelect],
+ );
+
+ const all: Device[] = getAll({ knownDevices }, { devices });
+
+ const [ble, other] = all.reduce(
+ ([ble, other], device) =>
+ device.wired ? [ble, [...other, device]] : [[...ble, device], other],
+ [[], []],
+ );
+
+ const hasUSBSection = Platform.OS === "android" || other.length > 0;
+
+ useEffect(() => {
+ const subscription = discoverDevices(filter).subscribe(e => {
+ setDevices(devices => {
+ if (e.type !== "add") {
+ return devices.filter(d => d.deviceId !== e.id);
+ }
+
+ if (!devices.find(d => d.deviceId === e.id)) {
+ return [
+ ...devices,
+ {
+ deviceId: e.id,
+ deviceName: e.name || "",
+ modelId:
+ (e.deviceModel && e.deviceModel.id) ||
+ Config?.FALLBACK_DEVICE_MODEL_ID ||
+ "nanoX",
+ wired: e.id.startsWith("httpdebug|")
+ ? Config?.FALLBACK_DEVICE_WIRED === "YES"
+ : e.id.startsWith("usb|"),
+ },
+ ];
+ }
+
+ return devices;
+ });
+ });
+ return () => subscription.unsubscribe();
+ }, [knownDevices, filter]);
+
+ return (
+ <>
+ {usbOnly && withArrows && !hideAnimation ? (
+
+ ) : ble.length === 0 ? (
+
+ ) : (
+
+
+ {ble.map(renderItem)}
+
+
+ )}
+ {hasUSBSection &&
+ !usbOnly &&
+ (ble.length === 0 ? (
+
+ ) : (
+
+ ))}
+ {other.length === 0 ? (
+
+ ) : (
+ other.map(renderItem)
+ )}
+ {onWithoutDevice && (
+
+
+
+
+ )}
+ >
+ );
+}
+
+const BluetoothHeader = () => (
+
+
+
+
+
+);
+
+const USBHeader = () => (
+
+
+
+);
+
+const WithoutDeviceHeader = () => (
+
+
+
+
+
+);
+
+// Fixme Use the illustration instead of the png
+const UsbPlaceholder = () => (
+
+
+
+);
+
+function getAll({ knownDevices }, { devices }): Device[] {
+ return [
+ ...devices,
+ ...knownDevices.map(d => ({
+ deviceId: d.id,
+ deviceName: d.name || "",
+ wired: false,
+ modelId: "nanoX",
+ })),
+ ];
+}
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "flex-start",
+ },
+ headerText: {
+ fontSize: 14,
+ lineHeight: 21,
+ },
+ separator: {
+ width: "100%",
+ height: 1,
+ marginVertical: 24,
+ },
+ imageContainer: {
+ minHeight: 200,
+ position: "relative",
+ overflow: "visible",
+ },
+ image: {
+ position: "absolute",
+ right: "-5%",
+ top: 0,
+ width: "110%",
+ height: "100%",
+ },
+});
diff --git a/src/components/SelectableAccountsList.tsx b/src/components/SelectableAccountsList.tsx
new file mode 100644
index 0000000000..87c136ccf3
--- /dev/null
+++ b/src/components/SelectableAccountsList.tsx
@@ -0,0 +1,357 @@
+import React, {
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { Trans } from "react-i18next";
+import {
+ Animated,
+ View,
+ TouchableOpacity,
+ PanResponder,
+ FlatList,
+} from "react-native";
+import { useNavigation, useTheme } from "@react-navigation/native";
+import { listTokenTypesForCryptoCurrency } from "@ledgerhq/live-common/lib/currencies";
+import { Account } from "@ledgerhq/live-common/lib/types";
+import { FlexBoxProps } from "@ledgerhq/native-ui/components/Layout/Flex";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import Swipeable from "react-native-gesture-handler/Swipeable";
+
+import { ScreenName } from "../const";
+import { track } from "../analytics";
+import AccountCard from "./AccountCard";
+import CheckBox from "./CheckBox";
+import swipedAccountSubject from "../screens/AddAccounts/swipedAccountSubject";
+import Button from "./Button";
+import TouchHintCircle from "./TouchHintCircle";
+import Touchable from "./Touchable";
+
+const selectAllHitSlop = {
+ top: 16,
+ left: 16,
+ right: 16,
+ bottom: 16,
+};
+
+type Props = FlexBoxProps & {
+ accounts: Account[];
+ onPressAccount?: (_: Account) => void;
+ onSelectAll?: (_: Account[]) => void;
+ onUnselectAll?: (_: Account[]) => void;
+ selectedIds: string[];
+ isDisabled?: boolean;
+ forceSelected?: boolean;
+ emptyState?: ReactNode;
+ header: ReactNode;
+ style?: any;
+ index: number;
+ showHint: boolean;
+ onAccountNameChange?: (name: string, changedAccount: Account) => void;
+ useFullBalance?: boolean;
+};
+
+const SelectableAccountsList = ({
+ accounts,
+ onPressAccount,
+ onSelectAll: onSelectAllProp,
+ onUnselectAll: onUnselectAllProp,
+ selectedIds = [],
+ isDisabled = false,
+ forceSelected,
+ emptyState,
+ header,
+ showHint = false,
+ index: listIndex = -1,
+ onAccountNameChange,
+ useFullBalance,
+ ...props
+}: Props) => {
+ const { colors } = useTheme();
+ const navigation = useNavigation();
+
+ const onSelectAll = useCallback(() => {
+ track("SelectAllAccounts");
+ onSelectAllProp && onSelectAllProp(accounts);
+ }, [accounts, onSelectAllProp]);
+
+ const onUnselectAll = useCallback(() => {
+ track("UnselectAllAccounts");
+ onUnselectAllProp && onUnselectAllProp(accounts);
+ }, [accounts, onUnselectAllProp]);
+
+ const areAllSelected = accounts.every(a => selectedIds.indexOf(a.id) > -1);
+
+ return (
+
+ {header ? (
+
+ ) : null}
+ item.id + index}
+ renderItem={({ item, index }) => (
+ -1}
+ isDisabled={isDisabled}
+ onPress={onPressAccount}
+ colors={colors}
+ useFullBalance={useFullBalance}
+ />
+ )}
+ ListEmptyComponent={() => emptyState || null}
+ />
+
+ );
+};
+
+type SelectableAccountProps = {
+ account: Account;
+ onPress?: (_: Account) => void;
+ isDisabled?: boolean;
+ isSelected?: boolean;
+ showHint: boolean;
+ rowIndex: number;
+ listIndex: number;
+ navigation: any;
+ onAccountNameChange?: (name: string, changedAccount: Account) => void;
+ colors: any;
+ useFullBalance?: boolean;
+};
+
+const SelectableAccount = ({
+ account,
+ onPress,
+ isDisabled,
+ isSelected,
+ showHint,
+ rowIndex,
+ listIndex,
+ navigation,
+ onAccountNameChange,
+ useFullBalance,
+}: SelectableAccountProps) => {
+ const [stopAnimation, setStopAnimation] = useState(false);
+
+ const swipeableRow = useRef(null);
+
+ useEffect(() => {
+ const sub = swipedAccountSubject.subscribe(msg => {
+ const { row, list } = msg;
+ setStopAnimation(true);
+ if (swipeableRow.current && (row !== rowIndex || list !== listIndex)) {
+ swipeableRow.current.close();
+ }
+ });
+
+ return () => {
+ sub.unsubscribe();
+ };
+ }, [listIndex, rowIndex, swipeableRow]);
+
+ const panResponder = useMemo(
+ () =>
+ PanResponder.create({
+ // Ask to be the responder:
+ onStartShouldSetPanResponder: () => true,
+ onStartShouldSetPanResponderCapture: () => false,
+ onMoveShouldSetPanResponder: () => false,
+ onMoveShouldSetPanResponderCapture: () => false,
+ onPanResponderGrant: () => {
+ if (swipedAccountSubject) {
+ setStopAnimation(true);
+ swipedAccountSubject.next({ rowIndex, listIndex });
+ }
+ },
+ onShouldBlockNativeResponder: () => false,
+ }),
+ [rowIndex, listIndex],
+ );
+
+ const handlePress = () => {
+ track(isSelected ? "UnselectAccount" : "SelectAccount");
+ if (onPress) {
+ onPress(account);
+ }
+ };
+
+ const renderLeftActions = (
+ progress: Animated.AnimatedInterpolation,
+ dragX: Animated.AnimatedInterpolation,
+ ) => {
+ const translateX = dragX.interpolate({
+ inputRange: [0, 1000],
+ outputRange: [-112, 888],
+ });
+
+ return (
+
+
+ }
+ onPress={editAccountName}
+ paddingLeft={0}
+ paddingRight={0}
+ />
+
+
+ );
+ };
+
+ const editAccountName = () => {
+ if (!onAccountNameChange) return;
+
+ swipedAccountSubject.next({ row: -1, list: -1 });
+ navigation.navigate(ScreenName.EditAccountName, {
+ onAccountNameChange,
+ account,
+ });
+ };
+
+ const subAccountCount = account.subAccounts && account.subAccounts.length;
+ const isToken = listTokenTypesForCryptoCurrency(account.currency).length > 0;
+
+ const inner = (
+
+
+
+
+
+
+
+ ) : null
+ }
+ />
+
+ {!isDisabled && (
+
+
+
+ )}
+
+ );
+
+ if (isDisabled) return inner;
+
+ return (
+
+
+ {inner}
+
+ {showHint && (
+
+
+
+ )}
+
+
+ );
+};
+
+type HeaderProps = {
+ text: ReactNode;
+ areAllSelected: boolean;
+ onSelectAll?: () => void;
+ onUnselectAll?: () => void;
+};
+
+const Header = ({
+ text,
+ areAllSelected,
+ onSelectAll,
+ onUnselectAll,
+}: HeaderProps) => {
+ const shouldDisplaySelectAll = !!onSelectAll && !!onUnselectAll;
+
+ return (
+
+
+ {text}
+
+ {shouldDisplaySelectAll && (
+
+
+
+ {areAllSelected ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+ );
+};
+
+export default SelectableAccountsList;
diff --git a/src/components/SettingsCard.tsx b/src/components/SettingsCard.tsx
new file mode 100644
index 0000000000..7ffe470572
--- /dev/null
+++ b/src/components/SettingsCard.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import { View } from "react-native";
+import styled from "styled-components/native";
+import { RectButton } from "react-native-gesture-handler";
+import { IconType } from "@ledgerhq/native-ui/components/Icon/type";
+import { Box, BoxedIcon, Text } from "@ledgerhq/native-ui";
+import { ChevronRightMedium } from "@ledgerhq/native-ui/assets/icons";
+
+type Props = {
+ title: string;
+ desc: string;
+ Icon: IconType;
+ onClick: Function;
+ arrowRight?: boolean;
+};
+
+function Card({
+ onPress,
+ children,
+ ...otherProps
+}: {
+ children?: React.ReactNode;
+ onPress?: () => void;
+}) {
+ return (
+
+ {onPress ? (
+
+ {children}
+
+ ) : (
+ {children}
+ )}
+
+ );
+}
+
+const StyledCard = styled(Card)`
+ background-color: ${p => p.theme.colors.palette.background.main};
+ padding: ${p => p.theme.space[7]}px 0;
+ flex-direction: row;
+ align-items: center;
+`;
+
+export default function SettingsCard({
+ title,
+ desc,
+ Icon,
+ onClick,
+ arrowRight,
+}: Props) {
+ return (
+
+
+
+
+ {title}
+
+
+ {desc}
+
+
+ {arrowRight && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/SettingsRow.tsx b/src/components/SettingsRow.tsx
new file mode 100644
index 0000000000..9871a69267
--- /dev/null
+++ b/src/components/SettingsRow.tsx
@@ -0,0 +1,135 @@
+import React, { ReactNode } from "react";
+import { StyleProp, TextStyle, ViewStyle } from "react-native";
+import { Box, Checkbox, Flex, Text } from "@ledgerhq/native-ui";
+import {
+ ChevronRightMedium,
+ InfoMedium,
+} from "@ledgerhq/native-ui/assets/icons";
+import styled from "styled-components/native";
+import Touchable from "./Touchable";
+
+const StyledTouchableRow = styled(Touchable)<{ compact?: boolean }>`
+ background-color: ${p => p.theme.colors.palette.background.main};
+ padding: ${p => p.theme.space[p.compact ? 6 : 7]}px 0;
+ flex-direction: row;
+ align-items: center;
+ border-bottom-color: ${p => p.theme.colors.palette.neutral.c40};
+ border-bottom-width: ${p => (p.compact ? 0 : 1)}px;
+`;
+
+export default function SettingsRow({
+ onPress,
+ onHelpPress,
+ title,
+ titleStyle,
+ titleContainerStyle,
+ subtitle,
+ style,
+ desc,
+ selected,
+ arrowRight,
+ iconLeft,
+ centeredIcon,
+ children,
+ noTextDesc,
+ event,
+ eventProperties,
+ compact,
+}: {
+ onPress?: () => void;
+ onHelpPress?: () => void;
+ title: ReactNode;
+ subtitle?: string;
+ titleStyle?: StyleProp;
+ titleContainerStyle?: StyleProp;
+ style?: StyleProp;
+ desc?: ReactNode;
+ selected?: boolean;
+ arrowRight?: boolean;
+ iconLeft?: any;
+ centeredIcon?: boolean;
+ children?: ReactNode;
+ noTextDesc?: boolean;
+ event?: string;
+ eventProperties?: Object;
+ compact?: boolean;
+}) {
+ let title$ = (
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ {!!onHelpPress && (
+
+
+
+ )}
+
+ );
+
+ if (onHelpPress) {
+ title$ = {title$};
+ }
+
+ return (
+
+ {iconLeft && (
+
+ {iconLeft}
+
+ )}
+
+ {title$}
+ {desc && !noTextDesc && (
+
+ {desc}
+
+ )}
+ {desc && noTextDesc && desc}
+
+
+ {children}
+ {arrowRight ? (
+
+
+
+ ) : selected ? (
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/src/components/StepHeader.tsx b/src/components/StepHeader.tsx
new file mode 100644
index 0000000000..afba587cf7
--- /dev/null
+++ b/src/components/StepHeader.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { TouchableWithoutFeedback } from "react-native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import { scrollToTop } from "../navigation/utils";
+
+type Props = {
+ title: React.ReactNode;
+ subtitle?: React.ReactNode;
+};
+
+export default function StepHeader({ title, subtitle }: Props) {
+ return (
+
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {title}
+
+
+
+ );
+}
diff --git a/src/components/SubAccountRow.tsx b/src/components/SubAccountRow.tsx
new file mode 100644
index 0000000000..de1c554b60
--- /dev/null
+++ b/src/components/SubAccountRow.tsx
@@ -0,0 +1,167 @@
+import {
+ getAccountCurrency,
+ getAccountName,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account";
+import React, { memo } from "react";
+import { StyleSheet } from "react-native";
+import {
+ RectButton,
+ LongPressGestureHandler,
+ State,
+} from "react-native-gesture-handler";
+import {
+ SubAccount,
+ TokenAccount,
+ Account,
+} from "@ledgerhq/live-common/lib/types";
+import { createStructuredSelector } from "reselect";
+import { connect, useSelector } from "react-redux";
+import { Box, Flex, Text } from "@ledgerhq/native-ui";
+import CurrencyUnitValue from "./CurrencyUnitValue";
+import CounterValue from "./CounterValue";
+import CurrencyIcon from "./CurrencyIcon";
+import { accountSelector } from "../reducers/accounts";
+import { selectedTimeRangeSelector } from "../reducers/settings";
+import { useBalanceHistoryWithCountervalue } from "../actions/portfolio";
+import Delta from "./Delta";
+
+type Props = {
+ account: SubAccount;
+ parentAccount: Account;
+ onSubAccountPress: (subAccount: SubAccount) => any;
+ onSubAccountLongPress: (tokenAccount: TokenAccount, account: Account) => any;
+ useCounterValue?: boolean;
+};
+
+const placeholderProps = {
+ width: 40,
+ containerHeight: 20,
+};
+
+function SubAccountRow({
+ account,
+ parentAccount,
+ onSubAccountPress,
+ onSubAccountLongPress,
+ useCounterValue,
+}: Props) {
+ const currency = getAccountCurrency(account);
+ const name = getAccountName(account);
+ const unit = getAccountUnit(account);
+ const range = useSelector(selectedTimeRangeSelector);
+ const {
+ countervalueChange,
+ cryptoChange,
+ } = useBalanceHistoryWithCountervalue({ account, range });
+
+ return (
+ {
+ if (nativeEvent.state === State.ACTIVE) {
+ if (account.type === "TokenAccount") {
+ onSubAccountLongPress(account, parentAccount);
+ }
+ }
+ }}
+ minDurationMs={600}
+ >
+ onSubAccountPress(account)}
+ style={{ alignItems: "center" }}
+ >
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+ {countervalueChange && (
+
+ )}
+
+
+
+
+ );
+}
+
+const AccountCv = ({ children }: { children: any }) => (
+
+ {children}
+
+);
+
+const mapStateToProps = createStructuredSelector({
+ parentAccount: accountSelector,
+});
+
+const SubAccountRowComponent = connect(mapStateToProps)(SubAccountRow);
+
+export default memo(SubAccountRowComponent);
+
+const styles = StyleSheet.create({
+ container: {
+ borderRadius: 4,
+ },
+ innerContainer: {
+ paddingHorizontal: 12,
+ paddingVertical: 14,
+ flexDirection: "row",
+ alignItems: "center",
+ overflow: "visible",
+ },
+ inner: {
+ flexGrow: 1,
+ flexShrink: 1,
+ marginLeft: 16,
+ flexDirection: "column",
+ },
+ accountNameText: {
+ fontSize: 16,
+ marginBottom: 4,
+ },
+ balanceContainer: {
+ marginLeft: 16,
+ alignItems: "flex-end",
+ },
+ balanceNumText: {
+ fontSize: 16,
+ },
+ balanceCounterContainer: {
+ marginTop: 5,
+ height: 20,
+ },
+ balanceCounterText: {
+ fontSize: 14,
+ },
+});
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx
new file mode 100644
index 0000000000..5c5602b0c5
--- /dev/null
+++ b/src/components/Switch.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Switch as RNSwitch } from "@ledgerhq/native-ui";
+
+type SwitchProps = {
+ value: boolean;
+ onValueChange?: (value: boolean) => void;
+ disabled?: boolean;
+ label?: string;
+};
+
+export default function Switch({
+ value,
+ onValueChange,
+ ...props
+}: SwitchProps) {
+ return ;
+}
diff --git a/src/components/TabIcon.tsx b/src/components/TabIcon.tsx
new file mode 100644
index 0000000000..6a315457dd
--- /dev/null
+++ b/src/components/TabIcon.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Text } from "@ledgerhq/native-ui";
+import styled from "styled-components/native";
+
+type Props = {
+ color: string;
+ focused: boolean;
+ i18nKey: string;
+ Icon: (props: { size?: number; color?: string }) => React.ReactElement;
+};
+
+const TabIconContainer = styled.View`
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ padding-top: ${p => p.theme.space[2]}px;
+`;
+
+export default function TabIcon({ Icon, i18nKey, color }: Props) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t(i18nKey)}
+
+
+ );
+}
diff --git a/src/components/TextInput.android.tsx b/src/components/TextInput.android.tsx
new file mode 100644
index 0000000000..dfaede729c
--- /dev/null
+++ b/src/components/TextInput.android.tsx
@@ -0,0 +1,4 @@
+// @ts-ignore
+import TextInput from "./TextInput.tsx";
+
+export default TextInput;
diff --git a/src/components/TextInput.ios.tsx b/src/components/TextInput.ios.tsx
new file mode 100644
index 0000000000..dfaede729c
--- /dev/null
+++ b/src/components/TextInput.ios.tsx
@@ -0,0 +1,4 @@
+// @ts-ignore
+import TextInput from "./TextInput.tsx";
+
+export default TextInput;
diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx
new file mode 100644
index 0000000000..46f6e0d984
--- /dev/null
+++ b/src/components/TextInput.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { Platform, TextInput as NativeTextInput } from "react-native";
+import { BaseInput } from "@ledgerhq/native-ui";
+import { InputProps } from "@ledgerhq/native-ui/components/Form/Input/BaseInput";
+
+export interface Props extends InputProps {
+ withSuggestions?: boolean;
+}
+
+function TextInput(
+ { withSuggestions, ...props }: Props,
+ ref: React.ForwardedRef,
+) {
+ const flags: Partial = {};
+
+ if (!withSuggestions) {
+ flags.autoCorrect = false;
+ if (Platform.OS === "android") flags.keyboardType = "visible-password";
+ }
+
+ return ;
+}
+
+export default React.forwardRef(TextInput);
diff --git a/src/components/TouchHintCircle.tsx b/src/components/TouchHintCircle.tsx
new file mode 100644
index 0000000000..139f351237
--- /dev/null
+++ b/src/components/TouchHintCircle.tsx
@@ -0,0 +1,104 @@
+import React, { useCallback, useEffect, useMemo } from "react";
+import { Animated, Easing } from "react-native";
+import { Flex } from "@ledgerhq/native-ui";
+import { FlexBoxProps } from "@ledgerhq/native-ui/components/Layout/Flex";
+
+type Props = FlexBoxProps & {
+ stopAnimation: boolean;
+};
+
+const TouchHintCircle = ({ stopAnimation, ...props }: Props) => {
+ const leftAnimated = useMemo(() => new Animated.Value(0), []);
+ const opacityAnimated = useMemo(() => new Animated.Value(0), []);
+ const growAnimated = useMemo(() => new Animated.Value(0), []);
+
+ const startAnimation = useCallback(() => {
+ const animation = Animated.sequence([
+ Animated.timing(opacityAnimated, {
+ toValue: 1,
+ duration: 400,
+ useNativeDriver: true,
+ }),
+ Animated.timing(growAnimated, {
+ toValue: 1,
+ duration: 100,
+ useNativeDriver: true,
+ }),
+ Animated.timing(leftAnimated, {
+ toValue: 1,
+ duration: 1200,
+ useNativeDriver: true,
+ easing: Easing.in(Easing.cubic),
+ }),
+ Animated.timing(opacityAnimated, {
+ toValue: 0,
+ duration: 400,
+ useNativeDriver: true,
+ }),
+ ]);
+
+ animation.start(() => {
+ if (!stopAnimation) {
+ growAnimated.setValue(0);
+ leftAnimated.setValue(0);
+ startAnimation();
+ }
+ });
+ }, [stopAnimation, leftAnimated, opacityAnimated, growAnimated]);
+
+ useEffect(() => {
+ startAnimation();
+ }, [startAnimation]);
+
+ const translateX = leftAnimated.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 80],
+ });
+
+ const opacity = opacityAnimated.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ });
+
+ const scale = growAnimated.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1.2, 0.8],
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default TouchHintCircle;
diff --git a/src/components/Touchable.js b/src/components/Touchable.js
index 85d76c717e..ef91ab7ff2 100644
--- a/src/components/Touchable.js
+++ b/src/components/Touchable.js
@@ -17,7 +17,7 @@ type Props = {
// the button will toggle in a pending state and
// will wait the promise to complete before enabling the button again
// it also displays a spinner if it takes more than WAIT_TIME_BEFORE_SPINNER
- onPress: ?() => ?Promise,
+ onPress: ?() => ?Promise | void,
children: *,
event?: string,
eventProperties?: { [key: string]: any },
diff --git a/src/components/ValidateOnDevice.tsx b/src/components/ValidateOnDevice.tsx
new file mode 100644
index 0000000000..86f9181865
--- /dev/null
+++ b/src/components/ValidateOnDevice.tsx
@@ -0,0 +1,252 @@
+import React from "react";
+import invariant from "invariant";
+import { ScrollView } from "react-native";
+import { useTranslation } from "react-i18next";
+import {
+ Account,
+ AccountLike,
+ Transaction,
+ TransactionStatus,
+} from "@ledgerhq/live-common/lib/types";
+import {
+ getMainAccount,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account";
+import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
+
+import {
+ getDeviceTransactionConfig,
+ DeviceTransactionField,
+} from "@ledgerhq/live-common/lib/transaction";
+import { getDeviceModel } from "@ledgerhq/devices";
+
+import { useTheme } from "@react-navigation/native";
+import styled from "styled-components/native";
+import { Flex, Log } from "@ledgerhq/native-ui";
+import Alert from "./Alert";
+import perFamilyTransactionConfirmFields from "../generated/TransactionConfirmFields";
+import { DataRowUnitValue, TextValueField } from "./ValidateOnDeviceDataRow";
+import Animation from "./Animation";
+import getDeviceAnimation from "./DeviceAction/getDeviceAnimation";
+
+export type FieldComponentProps = {
+ account: AccountLike;
+ parentAccount: Account | undefined | null;
+ transaction: Transaction;
+ status: TransactionStatus;
+ field: DeviceTransactionField;
+};
+
+export type FieldComponent = React.ComponentType;
+
+function AmountField({
+ account,
+ parentAccount,
+ status,
+ field,
+}: FieldComponentProps) {
+ let unit;
+ if (account.type === "TokenAccount") {
+ unit = getAccountUnit(account);
+ } else {
+ const mainAccount = getMainAccount(account, parentAccount);
+ unit = getAccountUnit(mainAccount);
+ }
+ return (
+
+ );
+}
+
+function FeesField({
+ account,
+ parentAccount,
+ status,
+ field,
+}: FieldComponentProps) {
+ const mainAccount = getMainAccount(account, parentAccount);
+ const { estimatedFees } = status;
+ const feesUnit = getAccountUnit(mainAccount);
+ return (
+
+ );
+}
+
+function AddressField({ field }: FieldComponentProps) {
+ invariant(field.type === "address", "AddressField invalid");
+ return ;
+}
+
+// NB Leaving AddressField although I think it's redundant at this point
+// in case we want specific styles for addresses.
+function TextField({ field }: FieldComponentProps) {
+ invariant(field.type === "text", "TextField invalid");
+ return ;
+}
+
+const commonFieldComponents: { [key: any]: FieldComponent } = {
+ amount: AmountField,
+ fees: FeesField,
+ address: AddressField,
+ text: TextField,
+};
+
+type Props = {
+ device: Device;
+ status: TransactionStatus;
+ transaction: Transaction;
+ account: AccountLike;
+ parentAccount: Account | null | undefined;
+};
+
+export default function ValidateOnDevice({
+ device,
+ account,
+ parentAccount,
+ status,
+ transaction,
+}: Props) {
+ const { dark } = useTheme();
+ const theme = dark ? "dark" : "light";
+ const { t } = useTranslation();
+ const mainAccount = getMainAccount(account, parentAccount);
+ const r = perFamilyTransactionConfirmFields[mainAccount.currency.family];
+
+ const fieldComponents = {
+ ...commonFieldComponents,
+ ...(r && r.fieldComponents),
+ };
+ const Warning = r && r.warning;
+ const Title = r && r.title;
+ const Footer = r && r.footer;
+
+ const fields = getDeviceTransactionConfig({
+ account,
+ parentAccount,
+ transaction,
+ status,
+ });
+
+ const transRecipientWording = t(
+ `ValidateOnDevice.recipientWording.${transaction.mode || "send"}`,
+ );
+ const recipientWording =
+ transRecipientWording !==
+ `ValidateOnDevice.recipientWording.${transaction.mode || "send"}`
+ ? transRecipientWording
+ : t("ValidateOnDevice.recipientWording.send");
+
+ const transTitleWording = t(
+ `ValidateOnDevice.title.${transaction.mode || "send"}`,
+ getDeviceModel(device.modelId),
+ );
+ const titleWording =
+ transTitleWording !== `ValidateOnDevice.title.${transaction.mode || "send"}`
+ ? transTitleWording
+ : t("ValidateOnDevice.title.send", getDeviceModel(device.modelId));
+
+ return (
+
+
+
+
+
+
+ {Title ? (
+
+ ) : (
+ {titleWording}
+ )}
+
+
+ {fields.map((field, i) => {
+ const MaybeComponent = fieldComponents[field.type];
+ if (!MaybeComponent) {
+ console.warn(
+ `TransactionConfirm field ${field.type} is not implemented! add a generic implementation in components/TransactionConfirm.js or inside families/*/TransactionConfirmFields.js`,
+ );
+ return null;
+ }
+ return (
+
+ );
+ })}
+
+ {Warning ? (
+
+ ) : null}
+
+
+
+ {Footer ? (
+
+ ) : (
+
+ {recipientWording}
+
+ )}
+
+ );
+}
+
+const RootContainer = styled(Flex).attrs({
+ flex: 1,
+})``;
+
+const DataRowsContainer = styled(Flex).attrs({
+ marginVertical: 24,
+ alignSelf: "stretch",
+})``;
+
+const InnerContainer = styled(Flex).attrs({
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ flex: 1,
+})``;
+
+const FooterContainer = styled(Flex).attrs({
+ padding: 16,
+})``;
+
+const AnimationContainer = styled(Flex).attrs({
+ marginBottom: 40,
+})``;
+
+const ScrollContainer = styled(ScrollView)`
+ flex: 1;
+ padding: 16px;
+`;
+
+const TitleContainer = styled(Flex).attrs({
+ py: 8,
+})``;
+
+const TitleText = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
diff --git a/src/components/ValidateSuccess.tsx b/src/components/ValidateSuccess.tsx
new file mode 100644
index 0000000000..08fe4ffea4
--- /dev/null
+++ b/src/components/ValidateSuccess.tsx
@@ -0,0 +1,97 @@
+import React, { memo } from "react";
+import { Trans } from "react-i18next";
+import { Icons, IconBox, Text, Flex, Button, Log } from "@ledgerhq/native-ui";
+
+import Alert from "./Alert";
+
+type Props = {
+ onClose?: () => void;
+ onViewDetails?: () => void;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ primaryButton?: React.ReactNode;
+ secondaryButton?: React.ReactNode;
+ icon?: React.ReactNode;
+ iconColor?: string;
+ iconBoxSize: number;
+ iconSize: number;
+ info?: React.ReactNode;
+ onLearnMore?: () => void;
+};
+
+function ValidateSuccess({
+ onClose,
+ onViewDetails,
+ title,
+ description,
+ primaryButton,
+ secondaryButton,
+ icon = Icons.CheckAloneMedium,
+ iconColor = "success.c100",
+ iconBoxSize = 64,
+ iconSize = 24,
+ info,
+ onLearnMore,
+}: Props) {
+ return (
+
+
+
+
+ {title || }
+
+
+ {description || }
+
+ {info && (
+
+ {info}
+
+ )}
+
+
+ {primaryButton ||
+ (onViewDetails && (
+
+ ))}
+ {secondaryButton ||
+ (onClose && (
+
+ ))}
+
+
+ );
+}
+
+export default memo(ValidateSuccess);
diff --git a/src/components/WebPlatformPlayer/InfoPanel.js b/src/components/WebPlatformPlayer/InfoPanel.js
index 2cbf8fc9f1..7d53f5ec4e 100644
--- a/src/components/WebPlatformPlayer/InfoPanel.js
+++ b/src/components/WebPlatformPlayer/InfoPanel.js
@@ -11,7 +11,6 @@ import type { TranslatableString } from "@ledgerhq/live-common/lib/platform/type
import { languageSelector } from "../../reducers/settings";
import ExternalLinkIcon from "../../icons/ExternalLink";
import AppIcon from "../../screens/Platform/AppIcon";
-import CloseIcon from "../../icons/Close";
import BottomModal from "../BottomModal";
import LText from "../LText";
@@ -52,9 +51,6 @@ const InfoPanel = ({
isOpened={isOpened}
onClose={onClose}
>
-
-
-
{icon ? (
@@ -147,12 +143,6 @@ const styles = StyleSheet.create({
externalLinkIcon: {
paddingLeft: 6,
},
- closeIcon: {
- position: "absolute",
- top: 8,
- right: 16,
- opacity: 0.5,
- },
});
export default InfoPanel;
diff --git a/src/components/useExportLogs.tsx b/src/components/useExportLogs.tsx
new file mode 100644
index 0000000000..684db511a5
--- /dev/null
+++ b/src/components/useExportLogs.tsx
@@ -0,0 +1,35 @@
+import { useCallback } from "react";
+import Share from "react-native-share";
+import logger from "../logger";
+import logReport from "../log-report";
+import getFullAppVersion from "../logic/version";
+
+export default function useExportLogs() {
+ return useCallback(() => {
+ const exportLogs = async () => {
+ const logs = logReport.getLogs();
+ const base64 = Buffer.from(JSON.stringify(logs)).toString("base64");
+ const version = getFullAppVersion(undefined, undefined, "-");
+ const date = new Date().toISOString().split("T")[0];
+
+ const humanReadableName = `ledger-live-mob-${version}-${date}-logs`;
+
+ const options = {
+ failOnCancel: false,
+ saveToFiles: true,
+ type: "application/json",
+ filename: humanReadableName,
+ url: `data:application/json;base64,${base64}`,
+ };
+
+ try {
+ await Share.open(options);
+ } catch (err) {
+ if (err.error.code !== "ECANCELLED500") {
+ logger.critical(err);
+ }
+ }
+ };
+ exportLogs();
+ }, []);
+}
diff --git a/src/components/wrappedUi/Button.tsx b/src/components/wrappedUi/Button.tsx
index bcdbc80d8f..eb52819b56 100644
--- a/src/components/wrappedUi/Button.tsx
+++ b/src/components/wrappedUi/Button.tsx
@@ -3,7 +3,7 @@ import { ButtonProps } from "@ledgerhq/native-ui/components/cta/Button";
import { Button as UiButton } from "@ledgerhq/native-ui";
import { track } from "../../analytics";
-export type WrapperButtonProps = ButtonProps & {
+export type WrappedButtonProps = ButtonProps & {
event?: string;
eventProperties?: Object;
};
@@ -13,7 +13,7 @@ export default function Button({
event,
eventProperties,
...othersProps
-}: WrapperButtonProps) {
+}: WrappedButtonProps) {
const onPressHandler = useCallback(
async pressEvent => {
if (!onPress) return;
diff --git a/src/components/wrappedUi/Link.tsx b/src/components/wrappedUi/Link.tsx
new file mode 100644
index 0000000000..ee44ed03c4
--- /dev/null
+++ b/src/components/wrappedUi/Link.tsx
@@ -0,0 +1,29 @@
+import React, { useCallback } from "react";
+import { Link as UiLink } from "@ledgerhq/native-ui";
+import { LinkProps } from "@ledgerhq/native-ui/components/cta/Link";
+import { track } from "../../analytics";
+
+export type WrappedLinkProps = LinkProps & {
+ event?: string;
+ eventProperties?: Object;
+};
+
+export default function Link({
+ onPress,
+ event,
+ eventProperties,
+ ...othersProps
+}: WrappedLinkProps) {
+ const onPressHandler = useCallback(
+ async pressEvent => {
+ if (!onPress) return;
+ if (event) {
+ track(event, eventProperties);
+ }
+ onPress(pressEvent);
+ },
+ [event, eventProperties, onPress],
+ );
+
+ return ;
+}
diff --git a/src/config/urls.js b/src/config/urls.js
index 1f310910aa..3ca417a259 100644
--- a/src/config/urls.js
+++ b/src/config/urls.js
@@ -25,6 +25,9 @@ export const urls = {
"https://shop.ledger.com/pages/politica-de-privacidad?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
ru:
"https://shop.ledger.com/pages/nasha-politika-konfidentsialnosti?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
+ // TODO: Add the correct zh link
+ zh:
+ "https://shop.ledger.com/pages/privacy-policy?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
},
buyNanoX:
"https://www.ledger.com/products/ledger-nano-x?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=manager_emptystate",
diff --git a/src/config/urls.tsx b/src/config/urls.tsx
new file mode 100644
index 0000000000..e05d327f27
--- /dev/null
+++ b/src/config/urls.tsx
@@ -0,0 +1,166 @@
+export const urls = {
+ faq:
+ "https://support.ledgerwallet.com/hc/en-us?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=faq",
+ contact:
+ "https://support.ledger.com/hc/en-us/requests/new?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=contact",
+ terms: {
+ en: "https://github.com/LedgerHQ/ledger-live-mobile/blob/master/TERMS.md",
+ fr:
+ "https://github.com/LedgerHQ/ledger-live-mobile/blob/master/TERMS.fr.md",
+ es:
+ "https://github.com/LedgerHQ/ledger-live-mobile/blob/master/TERMS.es.md",
+ zh:
+ "https://github.com/LedgerHQ/ledger-live-mobile/blob/master/TERMS.zh.md",
+ ru:
+ "https://github.com/LedgerHQ/ledger-live-mobile/blob/master/TERMS.ru.md",
+ },
+ privacyPolicy: {
+ en:
+ "https://shop.ledger.com/pages/privacy-policy?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
+ fr:
+ "https://shop.ledger.com/pages/politique-de-confidentialite?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
+ es:
+ "https://shop.ledger.com/pages/politica-de-privacidad?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
+ ru:
+ "https://shop.ledger.com/pages/nasha-politika-konfidentsialnosti?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
+ // TODO: Add the correct zh link
+ zh:
+ "https://shop.ledger.com/pages/privacy-policy?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=privacy",
+ },
+ buyNanoX:
+ "https://www.ledger.com/products/ledger-nano-x?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=manager_emptystate",
+ playstore: "https://play.google.com/store/apps/details?id=com.ledger.live",
+ applestore:
+ "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=1361671700&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software",
+ feesMoreInfo:
+ "https://support.ledgerwallet.com/hc/en-us/articles/360006535873?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=fees",
+ feesEthereum:
+ "https://support.ledger.com/hc/en-us/articles/115005197845?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=eth_fees",
+ feesPolkadot:
+ "https://support.ledger.com/hc/en-us/articles/360016289919?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=dot_fees",
+ verifyTransactionDetails:
+ "https://support.ledger.com/hc/en-us/articles/4404389453841-Receive-crypto-assets?docs=true&utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=tx_device_check",
+ erc20:
+ "https://support.ledger.com/hc/en-us/articles/4404389645329-Manage-ERC20-tokens?docs=true&utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=receive_erc20",
+ errors: {
+ PairingFailed:
+ "https://support.ledger.com/hc/en-us/articles/360025864773?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=pairing_failed",
+ SyncError:
+ "https://support.ledger.com/hc/en-us/articles/360012207759-Solve-a-synchronization-error?support=true",
+ LedgerAPIErrorWithMessage:
+ "https://status.ledger.com?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=error_apierrorwithmessage",
+ StratisDown2021Warning:
+ "https://support.ledger.com/hc/en-us/articles/115005175329",
+ },
+ multipleAddresses:
+ "https://support.ledger.com/hc/en-us/articles/360033802154?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=ops_details_change",
+ delegation:
+ "https://www.ledger.com/staking-tezos?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=delegation_tezos",
+ appSupport:
+ "https://support.ledger.com/hc/en-us/categories/115000811829-Apps?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=appsupport",
+ goToManager:
+ "https://support.ledger.com/hc/en-us/articles/360020436654?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=gotomanager",
+ addAccount:
+ "https://support.ledger.com/hc/en-us/articles/360020435654?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=add_account",
+ tronStaking:
+ "https://www.ledger.com/staking-tron?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=tron",
+ supportLinkByTokenType: {
+ erc20:
+ "https://support.ledger.com/hc/en-us/articles/115005197845-Manage-ERC20-tokens?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=receive_account_flow",
+ trc10:
+ "https://support.ledger.com/hc/en-us/articles/360013062159?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=receive_account_flow_trc10",
+ trc20:
+ "https://support.ledger.com/hc/en-us/articles/360013062159?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=receive_account_flow_trc20",
+ asa:
+ "https://support.ledger.com/hc/en-us/articles/360015896040?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=receive_account_flow",
+ },
+ cosmosStaking:
+ "https://www.ledger.com/staking-cosmos?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=cosmos",
+ cosmosStakingRewards:
+ "https://support.ledger.com/hc/en-us/articles/360014339340-Earn-Cosmos-staking-rewards?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=cosmos",
+ algorandStaking:
+ "https://support.ledger.com/hc/en-us/articles/360015897740?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=algorand",
+ polkadotStaking:
+ "https://support.ledger.com/hc/en-us/articles/360019187260?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=polkadot",
+
+ swap: {
+ info:
+ "https://www.ledger.com/swap?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=swap_intro",
+ learnMore:
+ "https://www.ledger.com/swap?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=swap_form",
+ providers: {
+ changelly: {
+ main: "https://changelly.com/",
+ tos: "https://changelly.com/terms-of-use",
+ },
+ wyre: {
+ main: "https://support.sendwyre.com/hc/en-us/requests/new",
+ tos: "https://www.sendwyre.com/user-agreement/",
+ },
+ },
+ },
+ // Banners
+ banners: {
+ blackfriday:
+ "https://shop.ledger.com/pages/black-friday?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=banner_carousel",
+ familyPack:
+ "https://shop.ledger.com/products/ledger-nano-s-3pack?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=banner_carousel",
+ ledgerAcademy:
+ "https://www.ledger.com/academy?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=banner_carousel",
+ valentine:
+ "https://shop.ledger.com/pages/valentines-day-special-offers?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=banner_carousel",
+ twitterIntent: "https://twitter.com/intent/tweet",
+ },
+ platform: {
+ developerPage:
+ "https://developers.ledger.com?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=catalog",
+ },
+ compound:
+ "https://support.ledger.com/hc/en-us/articles/4404389208721-Lend-crypto-assets-with-Ledger-Live?docs=true&utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=compound",
+ compoundTnC:
+ "https://shop.ledger.com/pages/ledger-live-terms-of-use?utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=compoundTnC",
+ approvedOperation:
+ "https://support.ledger.com/hc/en-us/articles/360020849134-Track-your-transaction?docs=true&utm_source=ledger_live_mobile&utm_medium=self_referral&utm_content=compoundTX",
+ recoveryPhraseInfo:
+ "https://www.ledger.com/academy/crypto/what-is-a-recovery-phrase",
+ fixConnectionIssues:
+ "https://support.ledger.com/hc/en-us/articles/360025864773",
+ otgCable:
+ "https://support.ledger.com/hc/en-us/articles/115005463729-OTG-Kit-adapters-for-your-Ledger-devices",
+ ledgerStatus: "https://status.ledger.com/",
+ bitcoinAddressType:
+ "https://www.ledger.com/academy/difference-between-segwit-and-native-segwit",
+ supportPage:
+ "https://support.ledgerwallet.com/hc/en-us?utm_source=ledger_live_mobile&utm_medium=self_referral",
+ maxSpendable:
+ "https://support.ledger.com/hc/en-us/articles/360012960679?utm_source=ledger_live_mobile&utm_medium=self_referral",
+ elrond: {
+ website: "https://elrond.com",
+ },
+ cryptoOrg: {
+ website: "https://crypto.org",
+ },
+ solana: {
+ supportPage: "https://support.ledger.com",
+ },
+ resources: {
+ gettingStarted:
+ "https://www.ledger.com/start?utm_source=ledger_live&utm_medium=self_referral&utm_content=help_mobile",
+ helpCenter:
+ "https://support.ledger.com/hc/en-us?utm_source=ledger_live&utm_medium=self_referral&utm_content=help_mobile",
+ ledgerAcademy:
+ "https://www.ledger.com/academy/?utm_source=ledger_live&utm_medium=self_referral&utm_content=help_mobile",
+ facebook: "https://facebook.com/Ledger",
+ twitter: "https://twitter.com/Ledger",
+ github: "https://github.com/LedgerHQ",
+ status: "https://status.ledger.com",
+ },
+ discover: {
+ ledgerApps:
+ "https://support.ledger.com/hc/en-us/articles/4404599625233-Discover-Live-Apps?docs=true&utm_source=ledger_live&utm_medium=self_referral&utm_content=help_mobile",
+ earn:
+ "https://www.ledger.com/grow-your-assets?utm_source=ledger_live&utm_medium=self_referral&utm_content=help_mobile",
+ academy:
+ "https://www.ledger.com/academy?utm_source=ledger_live&utm_medium=self_referral&utm_content=help_mobile",
+ },
+};
diff --git a/src/const/navigation.js b/src/const/navigation.js
index 30ef7ddb77..82ad9e8077 100644
--- a/src/const/navigation.js
+++ b/src/const/navigation.js
@@ -85,7 +85,6 @@ export const ScreenName = {
GeneralSettings: "GeneralSettings",
HelpSettings: "HelpSettings",
Manager: "Manager",
- ManagerAppsList: "ManagerAppsList",
ManagerDevice: "ManagerDevice",
ManagerMain: "ManagerMain",
MigrateAccountsConnectDevice: "MigrateAccountsConnectDevice",
@@ -123,7 +122,7 @@ export const ScreenName = {
SendSummary: "SendSummary",
SendValidationError: "SendValidationError",
SendValidationSuccess: "SendValidationSuccess",
- Settings: "Settings",
+ SettingsScreen: "SettingsScreen",
SignConnectDevice: "SignConnectDevice",
SignSelectDevice: "SignSelectDevice",
SignSummary: "SignSummary",
@@ -278,13 +277,20 @@ export const ScreenName = {
LendingWithdrawValidationSuccess: "Lend WithdrawValidationSuccess",
OnboardingWelcome: "OnboardingWelcome",
+ OnboardingPostWelcomeSelection: "OnboardingPostWelcomeSelection",
OnboardingLanguage: "OnboardingLanguage",
+ OnboardingStepLanguageGetStarted: "OnboardingStepLanguageGetStarted",
OnboardingTermsOfUse: "OnboardingTermsOfUse",
OnboardingDeviceSelection: "OnboardingDeviceSelection",
OnboardingUseCase: "OnboardingUseCase",
+ OnboardingModalDiscoverLive: "OnboardingModalDiscoverLive",
OnboardingSetNewDeviceInfo: "OnboardingSetNewDeviceInfo",
OnboardingSetNewDevice: "OnboardingSetNewDevice",
+ OnboardingSetupDeviceInformation: "OnboardingSetupDeviceInformation",
+ OnboardingSetupDeviceRecoveryPhrase: "OnboardingSetupDeviceRecoveryPhrase",
+ OnboardingGeneralInformation: "OnboardingGeneralInformation",
+ OnboardingBluetoothInformation: "OnboardingBluetoothInformation",
OnboardingInfoModal: "OnboardingInfoModal",
OnboardingRecoveryPhrase: "OnboardingRecoveryPhrase",
@@ -299,6 +305,17 @@ export const ScreenName = {
OnboardingFinish: "OnboardingFinish",
+ OnboardingLanguageModal: "OnboardingLanguageModal",
+ OnboardingModalSetupNewDevice: "OnboardingModalSetupNewDevice",
+ OnboardingModalSetupSteps: "OnboardingModalSetupSteps",
+ OnboardingModalSetupSecureRecovery: "OnboardingModalSetupSecureRecovery",
+ OnboardingModalWarning: "OnboardingModalWarning",
+ OnboardingModalGeneralInformation: "OnboardingModalGeneralInformation",
+ OnboardingPreQuizModal: "OnboardingPreQuizModal",
+ OnboardingModalSyncDesktopInformation:
+ "OnboardingModalSyncDesktopInformation",
+ OnboardingModalRecoveryPhraseWarning: "OnboardingModalRecoveryPhraseWarning",
+
PlatformCatalog: "PlatformCatalog",
PlatformApp: "PlatformApp",
@@ -324,8 +341,16 @@ export const ScreenName = {
VerifyAccount: "VerifyAccount",
+ AnalyticsAllocation: "AnalyticsAllocation",
+ AnalyticsOperations: "AnalyticsOperations",
+
// solana
SolanaEditMemo: "SolanaEditMemo",
+
+ BuyDeviceScreen: "BuyDeviceScreen",
+
+ DiscoverScreen: "DiscoverScreen",
+ Learn: "Learn",
};
export const NavigatorName = {
@@ -335,6 +360,7 @@ export const NavigatorName = {
Accounts: "AccountsNavigator",
AccountSettings: "AccountSettings",
AddAccounts: "AddAccounts",
+ Analytics: "Analytics",
Exchange: "Exchange",
ExchangeBuyFlow: "ExchangeBuyFlow",
ExchangeSellFlow: "ExchangeSellFlow",
@@ -391,6 +417,5 @@ export const NavigatorName = {
// Root
RootNavigator: "RootNavigator",
-
- Learn: "Learn",
+ Discover: "Discover",
};
diff --git a/src/context/AuthPass/AuthScreen.js b/src/context/AuthPass/AuthScreen.js
index 80fd5ed93e..a37987b722 100644
--- a/src/context/AuthPass/AuthScreen.js
+++ b/src/context/AuthPass/AuthScreen.js
@@ -29,6 +29,8 @@ import FailBiometrics from "./FailBiometrics";
import KeyboardBackgroundDismiss from "../../components/KeyboardBackgroundDismiss";
import { VIBRATION_PATTERN_ERROR } from "../../constants";
import { withTheme } from "../../colors";
+import { Flex, Logos } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
type State = {
passwordError: ?Error,
@@ -52,23 +54,25 @@ type Props = {
colors: *,
};
-class NormalHeader extends PureComponent<{}> {
- render() {
- return (
-
-
-
-
-
-
-
-
-
- );
- }
+function NormalHeader() {
+ const { colors } = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
}
class FormFooter extends PureComponent<*> {
diff --git a/src/context/Locale.js b/src/context/Locale.js
index 9864fba005..1f3013f1e6 100644
--- a/src/context/Locale.js
+++ b/src/context/Locale.js
@@ -34,10 +34,13 @@ export { i18next as i18n };
type Props = {
children: React$Node,
};
+
+export type SupportedLanguages = "fr" | "en" | "es" | "zh" | "ru";
+
type LocaleState = {
i18n: any,
t: TFunction,
- locale: string,
+ locale: SupportedLanguages,
};
function getLocaleState(i18n): LocaleState {
@@ -71,7 +74,16 @@ export default function LocaleProvider({ children }: Props) {
);
}
-export function useLocale() {
+/**
+ * This returns an object containing the "language setting" locale, to be used for
+ * translation purposes.
+ *
+ * /!\ Do not use this for number or date formatting.
+ * For number or date formatting (according to the "region setting" locale),
+ * use instead `useSelector(localeSelector)` where `localeSelector` comes from
+ * `src/reducers/settings.js`.
+ * */
+export function useTranslationLocale() {
return useContext(LocaleContext);
}
diff --git a/src/families/algorand/Rewards/ClaimRewardsFlow/01-Info.js b/src/families/algorand/Rewards/ClaimRewardsFlow/01-Info.js
index 0c22b303b3..3082d2151d 100644
--- a/src/families/algorand/Rewards/ClaimRewardsFlow/01-Info.js
+++ b/src/families/algorand/Rewards/ClaimRewardsFlow/01-Info.js
@@ -4,6 +4,7 @@ import { View, StyleSheet, Linking } from "react-native";
import SafeAreaView from "react-native-safe-area-view";
import { Trans } from "react-i18next";
import { useTheme } from "@react-navigation/native";
+import { Flex } from "@ledgerhq/native-ui";
import { ScreenName, NavigatorName } from "../../../../const";
import Button from "../../../../components/Button";
import LText from "../../../../components/LText";
@@ -13,9 +14,9 @@ import BulletList, {
BulletGreenCheck,
} from "../../../../components/BulletList";
import NavigationScrollView from "../../../../components/NavigationScrollView";
-import IlluRewards from "../../../../icons/images/Rewards";
import { urls } from "../../../../config/urls";
import { TrackScreen } from "../../../../analytics";
+import Illustration from "../../../../images/illustration/Illustration";
const forceInset = { bottom: "always" };
@@ -52,7 +53,13 @@ export default function ClaimRewardsStarted({ navigation, route }: Props) {
contentContainerStyle={styles.scrollContainer}
>
-
+
+
+
@@ -83,7 +90,7 @@ export default function ClaimRewardsStarted({ navigation, route }: Props) {
/>
-
+
-
+
{warning && warning instanceof Error ? (
{
+ invariant(
+ account && account.algorandResources,
+ "algorand resources required",
+ );
+ const { rewards } = account.algorandResources;
+
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+ const { t } = useTranslation();
+ const navigation = useNavigation();
+
+ const onRewardsInfoClick = useCallback(() => {
+ navigation.navigate(NavigatorName.AlgorandClaimRewardsFlow, {
+ screen: ScreenName.AlgorandClaimRewardsInfo,
+ params: { accountId: account.id },
+ });
+ }, [navigation, account]);
+
+ const onRewardsClick = useCallback(() => {
+ navigation.navigate(NavigatorName.AlgorandClaimRewardsFlow, {
+ screen: ScreenName.AlgorandClaimRewardsStarted,
+ params: { accountId: account.id },
+ });
+ }, [account, navigation]);
+
+ const rewardsDisabled = rewards.lte(0);
+
+ return (
+
+ }
+ onPress={onRewardsInfoClick}
+ />
+
+
+
+
+
+
+ {currency && }
+
+
+
+
+
+ );
+};
+
+const Rewards = ({ account }: Props) => {
+ const { algorandResources } = account;
+
+ if (!algorandResources) return null;
+
+ return ;
+};
+
+export default Rewards;
diff --git a/src/families/algorand/operationDetails.js b/src/families/algorand/operationDetails.js
index 8afa33ce42..07a5137900 100644
--- a/src/families/algorand/operationDetails.js
+++ b/src/families/algorand/operationDetails.js
@@ -11,12 +11,11 @@ import { formatCurrencyUnit } from "@ledgerhq/live-common/lib/currencies/formatC
import { BigNumber } from "bignumber.js";
import { getAccountUnit } from "@ledgerhq/live-common/lib/account/helpers";
-import * as Animatable from "react-native-animatable";
-import { useTheme } from "@react-navigation/native";
import { useSelector } from "react-redux";
import Section from "../../screens/OperationDetails/Section";
import OperationStatusIcon from "../../icons/OperationStatusIcon";
import { discreetModeSelector, localeSelector } from "../../reducers/settings";
+import { Icons } from "@ledgerhq/native-ui";
type Props = {
extra: {
@@ -71,54 +70,22 @@ type OperationIconProps = {
operation: Operation,
};
-const anim = (size, negated) => ({
- from: {
- transform: [{ translateX: 0 }],
- },
- to: {
- transform: [{ translateX: (negated ? -size : size) / 4 }],
- },
-});
-
const OperationIcon = ({
type,
size,
confirmed,
operation: { hasFailed, extra },
}: OperationIconProps) => {
- const { colors } = useTheme();
const rewards = extra.rewards && extra.rewards.gt(0) ? extra.rewards : null;
return rewards ? (
-
-
-
-
-
-
+
) : (
-
+
+
-
+
);
}
diff --git a/src/families/cosmos/DelegationFlow/01-Started.js b/src/families/cosmos/DelegationFlow/01-Started.js
index 0651a70ed6..ce0a3d57ea 100644
--- a/src/families/cosmos/DelegationFlow/01-Started.js
+++ b/src/families/cosmos/DelegationFlow/01-Started.js
@@ -4,17 +4,18 @@ import { View, StyleSheet, Linking } from "react-native";
import SafeAreaView from "react-native-safe-area-view";
import { Trans } from "react-i18next";
import { useTheme } from "@react-navigation/native";
+import { Alert, Button, Flex } from "@ledgerhq/native-ui";
import { ScreenName } from "../../../const";
-import Button from "../../../components/Button";
import LText from "../../../components/LText";
import ExternalLink from "../../../components/ExternalLink";
import BulletList, { BulletGreenCheck } from "../../../components/BulletList";
import NavigationScrollView from "../../../components/NavigationScrollView";
-import IlluRewards from "../../../icons/images/Rewards";
import { urls } from "../../../config/urls";
import { TrackScreen } from "../../../analytics";
-import Alert from "../../../components/Alert";
+import Illustration from "../../../images/illustration/Illustration";
+import EarnLight from "../../../images/illustration/Earn.light.png";
+import EarnDark from "../../../images/illustration/Earn.dark.png";
const forceInset = { bottom: "always" };
@@ -39,21 +40,22 @@ export default function DelegationStarted({ navigation, route }: Props) {
Linking.openURL(urls.cosmosStakingRewards);
}, []);
- const onCancel = useCallback(() => {
- navigation.getParent().pop();
- }, [navigation]);
-
return (
-
-
+
+
+
@@ -82,28 +84,26 @@ export default function DelegationStarted({ navigation, route }: Props) {
}}
/>
-
-
-
-
-
+
+
+ }
+ />
+
}
- type="primary"
- />
- }
- type="darkSecondary"
- outline={false}
- />
+ type="main"
+ mt={6}
+ >
+
+
-
+
);
}
@@ -145,10 +145,6 @@ const styles = StyleSheet.create({
flexDirection: "row",
},
- warning: {
- width: "100%",
- marginTop: 16,
- },
learnMoreBtn: {
alignSelf: "flex-start",
paddingHorizontal: 0,
diff --git a/src/families/cosmos/Delegations/Row.tsx b/src/families/cosmos/Delegations/Row.tsx
new file mode 100644
index 0000000000..d350cd60cf
--- /dev/null
+++ b/src/families/cosmos/Delegations/Row.tsx
@@ -0,0 +1,111 @@
+import React from "react";
+import { View, StyleSheet, TouchableOpacity } from "react-native";
+import { useTranslation } from "react-i18next";
+import {
+ CosmosMappedDelegation,
+ CosmosMappedUnbonding,
+} from "@ledgerhq/live-common/lib/families/cosmos/types";
+import { Currency } from "@ledgerhq/live-common/lib/types";
+import { useTheme } from "@react-navigation/native";
+import { Text } from "@ledgerhq/native-ui";
+import CounterValue from "../../../components/CounterValue";
+import ArrowRight from "../../../icons/ArrowRight";
+import LText from "../../../components/LText";
+import FirstLetterIcon from "../../../components/FirstLetterIcon";
+
+type Props = {
+ delegation: CosmosMappedDelegation | CosmosMappedUnbonding;
+ currency: Currency;
+ onPress: (delegation: CosmosMappedDelegation | CosmosMappedUnbonding) => void;
+ isLast?: boolean;
+};
+
+export default function DelegationRow({
+ delegation,
+ currency,
+ onPress,
+ isLast = false,
+}: Props) {
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+ const { validator, validatorAddress, formattedAmount, amount } = delegation;
+
+ return (
+ onPress(delegation)}
+ >
+
+
+
+
+
+
+ {validator?.name ?? validatorAddress}
+
+
+
+
+ {t("common.seeMore")}
+
+
+
+
+
+
+
+ {formattedAmount}
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: {
+ paddingVertical: 16,
+ },
+ borderBottom: {
+ borderBottomWidth: 1,
+ },
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ seeMore: {
+ fontSize: 14,
+ },
+ icon: {
+ alignItems: "center",
+ justifyContent: "center",
+ width: 36,
+ height: 36,
+ borderRadius: 5,
+
+ marginRight: 12,
+ },
+ nameWrapper: {
+ flex: 1,
+ marginRight: 8,
+ },
+ rightWrapper: {
+ alignItems: "flex-end",
+ },
+});
diff --git a/src/families/cosmos/Delegations/index.tsx b/src/families/cosmos/Delegations/index.tsx
new file mode 100644
index 0000000000..ae9e77484d
--- /dev/null
+++ b/src/families/cosmos/Delegations/index.tsx
@@ -0,0 +1,537 @@
+import { BigNumber } from "bignumber.js";
+import React, { useCallback, useState, useMemo , ElementProps } from "react";
+import { View, StyleSheet, Linking } from "react-native";
+import { useNavigation, useTheme } from "@react-navigation/native";
+import { useTranslation } from "react-i18next";
+import {
+ getAccountCurrency,
+ getAccountUnit,
+ getMainAccount,
+} from "@ledgerhq/live-common/lib/account";
+import {
+ getDefaultExplorerView,
+ getAddressExplorer,
+} from "@ledgerhq/live-common/lib/explorers";
+import {
+ useCosmosMappedDelegations,
+ useCosmosPreloadData,
+} from "@ledgerhq/live-common/lib/families/cosmos/react";
+import type {
+ CosmosMappedDelegation,
+ CosmosMappedUnbonding,
+} from "@ledgerhq/live-common/lib/families/cosmos/types";
+import type { Account } from "@ledgerhq/live-common/lib/types";
+import {
+ mapUnbondings,
+ canRedelegate,
+ getRedelegation,
+ canUndelegate,
+ canDelegate,
+} from "@ledgerhq/live-common/lib/families/cosmos/logic";
+import { Text } from "@ledgerhq/native-ui";
+import AccountDelegationInfo from "../../../components/AccountDelegationInfo";
+import IlluRewards from "../../../icons/images/Rewards";
+import { urls } from "../../../config/urls";
+import AccountSectionLabel from "../../../components/AccountSectionLabel";
+import DelegationDrawer from "../../../components/DelegationDrawer";
+import type { IconProps } from "../../../components/DelegationDrawer";
+import Touchable from "../../../components/Touchable";
+import { rgba } from "../../../colors";
+import { ScreenName, NavigatorName } from "../../../const";
+import Circle from "../../../components/Circle";
+import LText from "../../../components/LText";
+import Button from "../../../components/Button";
+import FirstLetterIcon from "../../../components/FirstLetterIcon";
+import RedelegateIcon from "../../../icons/Redelegate";
+import UndelegateIcon from "../../../icons/Undelegate";
+import ClaimRewardIcon from "../../../icons/ClaimReward";
+import DelegationRow from "./Row";
+import DelegationLabelRight from "./LabelRight";
+import CurrencyUnitValue from "../../../components/CurrencyUnitValue";
+import CounterValue from "../../../components/CounterValue";
+import DateFromNow from "../../../components/DateFromNow";
+
+type Props = {
+ account: Account,
+};
+
+type DelegationDrawerProps = ElementProps;
+type DelegationDrawerActions = DelegationDrawerProps["actions"];
+
+function Delegations({ account }: Props) {
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+ const mainAccount = getMainAccount(account);
+ const delegations: CosmosMappedDelegation[] = useCosmosMappedDelegations(
+ mainAccount,
+ );
+
+ const currency = getAccountCurrency(mainAccount);
+ const unit = getAccountUnit(mainAccount);
+ const navigation = useNavigation();
+
+ const { validators } = useCosmosPreloadData();
+
+ const { cosmosResources } = mainAccount;
+
+ const undelegations =
+ cosmosResources &&
+ cosmosResources.unbondings &&
+ mapUnbondings(cosmosResources.unbondings, validators, unit);
+
+ const [delegation, setDelegation] = useState();
+ const [undelegation, setUndelegation] = useState();
+
+ const totalRewardsAvailable = delegations.reduce(
+ (sum, d) => sum.plus(d.pendingRewards || 0),
+ BigNumber(0),
+ );
+
+ const onNavigate = useCallback(
+ ({
+ route,
+ screen,
+ params,
+ }: {
+ route: typeof NavigatorName | typeof ScreenName,
+ screen?: typeof ScreenName,
+ params?: { [key: string]: any },
+ }) => {
+ setDelegation();
+ navigation.navigate(route, {
+ screen,
+ params: { ...params, accountId: account.id },
+ });
+ },
+ [navigation, account.id],
+ );
+
+ const onDelegate = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.CosmosDelegationFlow,
+ screen: ScreenName.CosmosDelegationStarted,
+ });
+ }, [onNavigate]);
+
+ const onRedelegate = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.CosmosRedelegationFlow,
+ screen: ScreenName.CosmosRedelegationValidator,
+ params: {
+ accountId: account.id,
+ validatorSrcAddress: delegation?.validatorAddress,
+ },
+ });
+ }, [onNavigate, delegation, account]);
+
+ const onCollectRewards = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.CosmosClaimRewardsFlow,
+ screen: delegation
+ ? ScreenName.CosmosClaimRewardsMethod
+ : ScreenName.CosmosClaimRewardsValidator,
+ params: delegation
+ ? {
+ validator: delegation.validator,
+ value: delegation.pendingRewards,
+ }
+ : {},
+ });
+ }, [onNavigate, delegation]);
+
+ const onUndelegate = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.CosmosUndelegationFlow,
+ screen: ScreenName.CosmosUndelegationAmount,
+ params: {
+ accountId: account.id,
+ delegation,
+ },
+ });
+ }, [onNavigate, delegation, account]);
+
+ const onCloseDrawer = useCallback(() => {
+ setDelegation();
+ setUndelegation();
+ }, []);
+
+ const onOpenExplorer = useCallback(
+ (address: string) => {
+ const url = getAddressExplorer(
+ getDefaultExplorerView(account.currency),
+ address,
+ );
+ if (url) Linking.openURL(url);
+ },
+ [account.currency],
+ );
+
+ const data = useMemo(() => {
+ const d = delegation || undelegation;
+
+ const redelegation = delegation && getRedelegation(account, delegation);
+
+ return d
+ ? [
+ {
+ label: t("delegation.validator"),
+ Component: (
+
+ {d.validator?.name ?? d.validatorAddress ?? ""}
+
+ ),
+ },
+ {
+ label: t("delegation.validatorAddress"),
+ Component: (
+ onOpenExplorer(d.validatorAddress)}
+ event="DelegationOpenExplorer"
+ >
+
+ {d.validatorAddress}
+
+
+ ),
+ },
+ {
+ label: t("delegation.delegatedAccount"),
+ Component: (
+
+ {account.name}{" "}
+
+ ),
+ },
+ {
+ label: t("cosmos.delegation.drawer.status"),
+ Component: (
+
+ {d.status === "bonded"
+ ? t("cosmos.delegation.drawer.active")
+ : t("cosmos.delegation.drawer.inactive")}
+
+ ),
+ },
+ ...(delegation
+ ? [
+ {
+ label: t("cosmos.delegation.drawer.rewards"),
+ Component: (
+
+ {delegation.formattedPendingRewards ?? ""}
+
+ ),
+ },
+ ]
+ : []),
+ ...(undelegation
+ ? [
+ {
+ label: t("cosmos.delegation.drawer.completionDate"),
+ Component: (
+
+
+
+ ),
+ },
+ ]
+ : []),
+ ...(redelegation
+ ? [
+ {
+ label: t("cosmos.delegation.drawer.redelegatedFrom"),
+ Component: (
+
+ onOpenExplorer(redelegation.validatorSrcAddress)
+ }
+ event="DelegationOpenExplorer"
+ >
+
+ {redelegation.validatorSrcAddress}
+
+
+ ),
+ },
+ {
+ label: t("cosmos.delegation.drawer.completionDate"),
+ Component: (
+
+
+
+ ),
+ },
+ ]
+ : []),
+ ]
+ : [];
+ }, [delegation, t, account, onOpenExplorer, undelegation]);
+
+ const actions = useMemo(() => {
+ const rewardsDisabled =
+ !delegation ||
+ !delegation.pendingRewards ||
+ delegation.pendingRewards.isZero();
+
+ const redelegateEnabled = delegation && canRedelegate(account, delegation);
+
+ const undelegationEnabled = canUndelegate(account);
+
+ return delegation
+ ? [
+ {
+ label: t("delegation.actions.redelegate"),
+ Icon: (props: IconProps) => (
+
+
+
+ ),
+ disabled: !redelegateEnabled,
+ onPress: onRedelegate,
+ event: "DelegationActionRedelegate",
+ },
+ {
+ label: t("delegation.actions.collectRewards"),
+ Icon: (props: IconProps) => (
+
+
+
+ ),
+ disabled: rewardsDisabled,
+ onPress: onCollectRewards,
+ event: "DelegationActionCollectRewards",
+ },
+ {
+ label: t("delegation.actions.undelegate"),
+ Icon: (props: IconProps) => (
+
+
+
+ ),
+ disabled: !undelegationEnabled,
+ onPress: onUndelegate,
+ event: "DelegationActionUndelegate",
+ },
+ ]
+ : [];
+ }, [t, onRedelegate, onCollectRewards, onUndelegate, delegation, account]);
+
+ const delegationDisabled = delegations.length <= 0 || !canDelegate(account);
+
+ return (
+
+ 0}
+ onClose={onCloseDrawer}
+ account={account}
+ ValidatorImage={({ size }) => (
+
+ )}
+ amount={delegation?.amount ?? BigNumber(0)}
+ data={data}
+ actions={actions}
+ />
+ {totalRewardsAvailable.gt(0) && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {delegations.length === 0 ? (
+ }
+ description={t("cosmos.delegation.delegationEarn", {
+ name: account.currency.name,
+ })}
+ infoUrl={urls.cosmosStaking}
+ infoTitle={t("cosmos.delegation.info")}
+ onPress={onDelegate}
+ ctaTitle={t("account.delegation.info.cta")}
+ />
+ ) : (
+
+
+ }
+ />
+ {delegations.map((d, i) => (
+
+ setDelegation(d)}
+ isLast={i === delegations.length - 1}
+ />
+
+ ))}
+
+ )}
+
+ {undelegations && undelegations.length > 0 && (
+
+
+ {undelegations.map((d, i) => (
+
+ setUndelegation(d)}
+ isLast={i === undelegations.length - 1}
+ />
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default function CosmosDelegations({ account }: Props) {
+ if (!account.cosmosResources) return null;
+ return ;
+}
+
+const styles = StyleSheet.create({
+ root: {
+ marginHorizontal: 16,
+ },
+ illustration: { alignSelf: "center", marginBottom: 16 },
+ rewardsWrapper: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignContent: "center",
+ paddingVertical: 16,
+ marginBottom: 16,
+
+ borderRadius: 4,
+ },
+ label: {
+ fontSize: 20,
+ flex: 1,
+ },
+ subLabel: {
+ fontSize: 14,
+
+ flex: 1,
+ },
+ column: {
+ flexDirection: "column",
+ },
+ wrapper: {
+ marginBottom: 16,
+ },
+ delegationsWrapper: {
+ borderRadius: 4,
+ },
+ valueText: {
+ fontSize: 14,
+ },
+});
diff --git a/src/families/cosmos/shared/02-SelectAmount.js b/src/families/cosmos/shared/02-SelectAmount.js
index 9309cbf04d..bf6166be54 100644
--- a/src/families/cosmos/shared/02-SelectAmount.js
+++ b/src/families/cosmos/shared/02-SelectAmount.js
@@ -16,7 +16,7 @@ import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge";
import { getAccountUnit } from "@ledgerhq/live-common/lib/account";
import { formatCurrencyUnit } from "@ledgerhq/live-common/lib/currencies";
-import { useTheme } from "@react-navigation/native";
+import { useTheme } from "styled-components/native";
import { accountScreenSelector } from "../../../reducers/accounts";
import { localeSelector } from "../../../reducers/settings";
import Button from "../../../components/Button";
@@ -126,7 +126,7 @@ function DelegationAmount({ navigation, route }: Props) {
);
return (
-
+
{ratioButtons.map(({ label, value: v }) => (
@@ -145,10 +144,9 @@ function DelegationAmount({ navigation, route }: Props) {
styles.ratioButton,
value.eq(v)
? {
- borderColor: colors.live,
- backgroundColor: colors.live,
+ backgroundColor: colors.primary.c80,
}
- : { borderColor: colors.grey },
+ : { borderColor: colors.neutral.c60 },
]}
onPress={() => {
Keyboard.dismiss();
@@ -157,7 +155,7 @@ function DelegationAmount({ navigation, route }: Props) {
>
{label}
@@ -166,11 +164,13 @@ function DelegationAmount({ navigation, route }: Props) {
-
+
{error && !value.eq(0) && (
-
-
+
+
-
-
+
+
@@ -253,7 +256,7 @@ function DelegationAmount({ navigation, route }: Props) {
/>
-
+
);
}
diff --git a/src/families/crypto_org/AccountSubHeader.tsx b/src/families/crypto_org/AccountSubHeader.tsx
new file mode 100644
index 0000000000..907a6b39c9
--- /dev/null
+++ b/src/families/crypto_org/AccountSubHeader.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Flex, Link, Text } from "@ledgerhq/native-ui";
+import { InfoMedium } from "@ledgerhq/native-ui/assets/icons";
+import AccountSubHeaderDrawer from "./AccountSubHeaderDrawer";
+
+function AccountSubHeader() {
+ const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
+ const { t } = useTranslation();
+
+ function openDrawer() {
+ setIsDrawerOpen(true);
+ }
+
+ function closeDrawer() {
+ setIsDrawerOpen(false);
+ }
+
+ return (
+
+
+ {t("cryptoOrg.account.subHeader.cardTitle")}
+
+
+ {t("cryptoOrg.account.subHeader.moreInfo")}
+
+
+
+ );
+}
+
+export default AccountSubHeader;
diff --git a/src/families/crypto_org/AccountSubHeaderDrawer.tsx b/src/families/crypto_org/AccountSubHeaderDrawer.tsx
new file mode 100644
index 0000000000..f79d4078b5
--- /dev/null
+++ b/src/families/crypto_org/AccountSubHeaderDrawer.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { Linking } from "react-native";
+import { useTranslation } from "react-i18next";
+import { BottomDrawer, Box, Flex, Text } from "@ledgerhq/native-ui";
+import { urls } from "../../config/urls";
+import ExternalLink from "../../components/ExternalLink";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+export default function AccountSubHeaderDrawer({ isOpen, onClose }: Props) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t("cryptoOrg.account.subHeader.description")}
+
+
+ {t("cryptoOrg.account.subHeader.description2")}
+
+
+ Linking.openURL(urls.cryptoOrg.website)}
+ fontSize={14}
+ event={"OpenCryptoOrgWebsite"}
+ />
+
+
+
+ );
+}
diff --git a/src/families/elrond/AccountSubHeader.tsx b/src/families/elrond/AccountSubHeader.tsx
new file mode 100644
index 0000000000..8b1a91c151
--- /dev/null
+++ b/src/families/elrond/AccountSubHeader.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Flex, Link, Text } from "@ledgerhq/native-ui";
+import { InfoMedium } from "@ledgerhq/native-ui/assets/icons";
+import AccountSubHeaderDrawer from "./AccountSubHeaderDrawer";
+
+function AccountSubHeader() {
+ const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
+ const { t } = useTranslation();
+
+ function openDrawer() {
+ setIsDrawerOpen(true);
+ }
+
+ function closeDrawer() {
+ setIsDrawerOpen(false);
+ }
+
+ return (
+
+
+ {t("elrond.account.subHeader.cardTitle")}
+
+
+ {t("elrond.account.subHeader.moreInfo")}
+
+
+
+ );
+}
+export default AccountSubHeader;
diff --git a/src/families/elrond/AccountSubHeaderDrawer.tsx b/src/families/elrond/AccountSubHeaderDrawer.tsx
new file mode 100644
index 0000000000..b277726e55
--- /dev/null
+++ b/src/families/elrond/AccountSubHeaderDrawer.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { Linking } from "react-native";
+import { useTranslation } from "react-i18next";
+import { BottomDrawer, Box, Flex, Text } from "@ledgerhq/native-ui";
+import { urls } from "../../config/urls";
+import ExternalLink from "../../components/ExternalLink";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+export default function AccountSubHeaderDrawer({ isOpen, onClose }: Props) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t("elrond.account.subHeader.description")}
+
+
+ {t("elrond.account.subHeader.description2")}
+
+
+ {t("elrond.account.subHeader.description3")}
+
+
+ Linking.openURL(urls.elrond.website)}
+ fontSize={14}
+ event={"OpenElrondWebsite"}
+ />
+
+
+
+ );
+}
diff --git a/src/families/ethereum/ScreenEditGasLimit.js b/src/families/ethereum/ScreenEditGasLimit.js
index ecf68402c8..910a8b6870 100644
--- a/src/families/ethereum/ScreenEditGasLimit.js
+++ b/src/families/ethereum/ScreenEditGasLimit.js
@@ -50,7 +50,7 @@ function EthereumEditGasLimit({ navigation, route }: Props) {
-
+
{
- navigation.popToTop();
- }, [navigation]);
-
const onHelp = useCallback(() => {
Linking.openURL(urls.polkadotStaking);
}, []);
@@ -47,7 +45,13 @@ export default function PolkadotBondStarted({ navigation, route }: Props) {
contentContainerStyle={styles.scrollContainer}
>
-
+
+
+
@@ -65,14 +69,7 @@ export default function PolkadotBondStarted({ navigation, route }: Props) {
))}
/>
-
+
-
-
-
-
-
+
+
+
}
- type="primary"
- />
- }
- type="secondary"
- outline={false}
- containerStyle={styles.buttonContainer}
- />
+ type="main"
+ mt={6}
+ >
+
+
);
@@ -144,20 +133,8 @@ const styles = StyleSheet.create({
},
help: {
marginTop: 32,
- borderRadius: 32,
- paddingVertical: 8,
- paddingHorizontal: 16,
- borderWidth: 1,
- flexDirection: "row",
- },
- warning: {
- width: "100%",
- marginTop: 16,
},
footer: {
padding: 16,
},
- buttonContainer: {
- marginTop: 4,
- },
});
diff --git a/src/families/polkadot/BondFlow/02-Amount.js b/src/families/polkadot/BondFlow/02-Amount.js
index 6061a8ff2c..afb4da65f0 100644
--- a/src/families/polkadot/BondFlow/02-Amount.js
+++ b/src/families/polkadot/BondFlow/02-Amount.js
@@ -248,7 +248,6 @@ export default function PolkadotBondAmount({ navigation, route }: Props) {
{unit.code}
}
- autoFocus
style={styles.inputContainer}
inputStyle={[
styles.inputStyle,
diff --git a/src/families/polkadot/NominateFlow/01-Validators.js b/src/families/polkadot/NominateFlow/01-Validators.js
index 18a29a4185..57da2e8c43 100644
--- a/src/families/polkadot/NominateFlow/01-Validators.js
+++ b/src/families/polkadot/NominateFlow/01-Validators.js
@@ -12,7 +12,7 @@ import {
import SafeAreaView from "react-native-safe-area-view";
import { Trans, useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
-import { useTheme } from "@react-navigation/native";
+import { useTheme } from "styled-components/native";
import { Polkadot as PolkadotIdenticon } from "@polkadot/reactnative-identicon/icons";
import type {
@@ -308,7 +308,7 @@ function NominateSelectValidator({ navigation, route }: Props) {
error instanceof PolkadotValidatorsRequired && !nominations.length; // Do not show error on first nominate
return (
-
+
0}
onClose={onCloseDrawer}
@@ -320,22 +320,24 @@ function NominateSelectValidator({ navigation, route }: Props) {
}
data={drawerInfo}
/>
- {!hasMinBondBalance ? (
-
-
-
- ) : null}
- {nonValidators.length ? (
-
-
-
- ) : null}
+
+ {!hasMinBondBalance ? (
+
+
+
+ ) : null}
+ {nonValidators.length ? (
+
+
+
+ ) : null}
+
(
{title}
@@ -370,26 +372,26 @@ function NominateSelectValidator({ navigation, route }: Props) {
{!ignoreError && maybeChill ? (
-
+
) : (
<>
- {maxSelected && }
+ {maxSelected && }
{!ignoreError && (error || warning) ? (
diff --git a/src/families/polkadot/NominateFlow/ValidatorItem.js b/src/families/polkadot/NominateFlow/ValidatorItem.js
index 2e35674a5d..b3e70514d2 100644
--- a/src/families/polkadot/NominateFlow/ValidatorItem.js
+++ b/src/families/polkadot/NominateFlow/ValidatorItem.js
@@ -6,7 +6,7 @@ import { Polkadot as PolkadotIdenticon } from "@polkadot/reactnative-identicon/i
import type { PolkadotValidator } from "@ledgerhq/live-common/lib/families/polkadot/types";
-import { useTheme } from "@react-navigation/native";
+import { useTheme } from "styled-components/native";
import CheckBox from "../../../components/CheckBox";
import LText from "../../../components/LText";
import Touchable from "../../../components/Touchable";
@@ -30,11 +30,9 @@ function Item({ item, selected, disabled, onSelect, onClick }: Props) {
isElected,
} = item;
- const onPress = useCallback(() => onSelect(item, selected), [
- onSelect,
- item,
- selected,
- ]);
+ const onPress = useCallback(() => {
+ onSelect(item, selected);
+ }, [onSelect, item, selected]);
const isDisabled = disabled && !selected;
@@ -46,7 +44,7 @@ function Item({ item, selected, disabled, onSelect, onClick }: Props) {
@@ -69,7 +67,7 @@ function Item({ item, selected, disabled, onSelect, onClick }: Props) {
{identity || address || ""}
@@ -79,7 +77,7 @@ function Item({ item, selected, disabled, onSelect, onClick }: Props) {
{isElected ? (
{isOversubscribed ? (
) : (
-
+
)}
-
+
{formattedCommission}
-
+
-
-
-
+
);
}
diff --git a/src/families/polkadot/Nominations/Actions.tsx b/src/families/polkadot/Nominations/Actions.tsx
new file mode 100644
index 0000000000..369063fbf4
--- /dev/null
+++ b/src/families/polkadot/Nominations/Actions.tsx
@@ -0,0 +1,122 @@
+import React, { useState, useCallback } from "react";
+import { TouchableOpacity } from "react-native";
+import { useTranslation } from "react-i18next";
+import LText from "../../../components/LText";
+import InfoModal from "../../../modals/Info";
+import Link from "../../../components/wrappedUi/Link";
+
+type Props = {
+ electionOpen?: boolean;
+ disabled?: boolean;
+ onPress: () => void;
+};
+
+export function NominateAction({ onPress, electionOpen, disabled }: Props) {
+ const { t } = useTranslation();
+
+ const [disabledModalOpen, setDisabledModalOpen] = useState(false);
+
+ const onClick = useCallback(() => {
+ if (disabled) setDisabledModalOpen(true);
+ else onPress();
+ }, [onPress, disabled]);
+
+ const onCloseModal = useCallback(() => setDisabledModalOpen(false), []);
+
+ return (
+ <>
+
+ {t("polkadot.nomination.nominate")}
+
+
+ >
+ );
+}
+
+export function SetControllerAction({ onPress }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t("polkadot.nomination.setController")}
+
+
+ );
+}
+
+export function RebondAction({ onPress, disabled }: Props) {
+ const { t } = useTranslation();
+
+ const [disabledModalOpen, setDisabledModalOpen] = useState(false);
+
+ const onClick = useCallback(() => {
+ if (disabled) setDisabledModalOpen(true);
+ else onPress();
+ }, [onPress, disabled]);
+
+ const onCloseModal = useCallback(() => setDisabledModalOpen(false), []);
+
+ return (
+
+
+ {t("polkadot.unlockings.rebond")}
+
+
+
+ );
+}
+
+export function WithdrawAction({ onPress, disabled }: Props) {
+ const { t } = useTranslation();
+
+ const [disabledModalOpen, setDisabledModalOpen] = useState(false);
+
+ const onClick = useCallback(() => {
+ if (disabled) setDisabledModalOpen(true);
+ else onPress();
+ }, [onPress, disabled]);
+
+ const onCloseModal = useCallback(() => setDisabledModalOpen(false), []);
+
+ return (
+
+
+ {t("polkadot.unlockings.withdrawUnbonded")}
+
+
+
+ );
+}
diff --git a/src/families/polkadot/Nominations/NominationRow.tsx b/src/families/polkadot/Nominations/NominationRow.tsx
new file mode 100644
index 0000000000..86109f2068
--- /dev/null
+++ b/src/families/polkadot/Nominations/NominationRow.tsx
@@ -0,0 +1,167 @@
+import React from "react";
+import { View, StyleSheet, TouchableOpacity } from "react-native";
+import { useTranslation } from "react-i18next";
+import { Polkadot as PolkadotIdenticon } from "@polkadot/reactnative-identicon/icons";
+
+import {
+ PolkadotNomination,
+ PolkadotValidator,
+} from "@ledgerhq/live-common/lib/families/polkadot/types";
+import { Account } from "@ledgerhq/live-common/lib/types";
+import {
+ getAccountCurrency,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account";
+
+import { useTheme } from "styled-components/native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import CurrencyUnitValue from "../../../components/CurrencyUnitValue";
+import CounterValue from "../../../components/CounterValue";
+import LText from "../../../components/LText";
+import ArrowRight from "../../../icons/ArrowRight";
+
+type Props = {
+ nomination: PolkadotNomination;
+ validator?: PolkadotValidator;
+ account: Account;
+ onPress: (nomination: PolkadotNomination) => void;
+ isLast?: boolean;
+};
+
+export default function NominationRow({
+ nomination,
+ validator,
+ account,
+ onPress,
+}: Props) {
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+
+ const { value, address, status } = nomination;
+ const name = validator?.identity || address;
+ // const total = validator?.totalBonded ?? null;
+ // const commission = validator?.commission ?? null;
+
+ const unit = getAccountUnit(account);
+ const currency = getAccountCurrency(account);
+
+ return (
+ onPress(nomination)}>
+
+
+
+
+
+
+
+ {name}
+
+
+
+ {status === "active" && (
+
+ {t("polkadot.nomination.active")}
+
+ )}
+ {status === "inactive" && (
+
+ {t("polkadot.nomination.inactive")}
+
+ )}
+ {status === "waiting" && (
+
+ {t("polkadot.nomination.waiting")}
+
+ )}
+ {!status && (
+
+ {t("polkadot.nomination.notValidator")}
+
+ )}
+
+
+ {t("common.seeMore")}
+
+
+
+
+
+
+ {status === "active" || status === "inactive" ? (
+
+
+
+
+
+
+
+
+
+ ) : null}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: {
+ paddingVertical: 16,
+ },
+ borderBottom: {
+ borderBottomWidth: 1,
+ },
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ icon: {
+ alignItems: "center",
+ justifyContent: "center",
+ width: 36,
+ height: 36,
+ marginRight: 12,
+ },
+ nameWrapper: {
+ flex: 1,
+ marginRight: 8,
+ },
+ statusWrapper: {
+ flex: 1,
+ marginRight: 8,
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ rightWrapper: {
+ alignItems: "flex-end",
+ },
+ seeMore: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginLeft: 8,
+ paddingLeft: 8,
+ borderLeftWidth: 1,
+ },
+ seeMoreText: {
+ fontSize: 14,
+ textAlignVertical: "top",
+ },
+});
diff --git a/src/families/polkadot/Nominations/index.tsx b/src/families/polkadot/Nominations/index.tsx
new file mode 100644
index 0000000000..e453c8c9b4
--- /dev/null
+++ b/src/families/polkadot/Nominations/index.tsx
@@ -0,0 +1,419 @@
+import isAfter from "date-fns/isAfter";
+
+import React, { useCallback, useState, useMemo } from "react";
+import { View, StyleSheet, Linking } from "react-native";
+import { useNavigation } from "@react-navigation/native";
+import { useTranslation } from "react-i18next";
+import { Polkadot as PolkadotIdenticon } from "@polkadot/reactnative-identicon/icons";
+
+import { getMainAccount } from "@ledgerhq/live-common/lib/account";
+import {
+ getDefaultExplorerView,
+ getAddressExplorer,
+} from "@ledgerhq/live-common/lib/explorers";
+import { Account } from "@ledgerhq/live-common/lib/types";
+import {
+ canNominate,
+ isStash,
+ hasExternalController,
+ hasExternalStash,
+ hasPendingOperationType,
+} from "@ledgerhq/live-common/lib/families/polkadot/logic";
+import { usePolkadotPreloadData } from "@ledgerhq/live-common/lib/families/polkadot/react";
+import type { PolkadotNomination } from "@ledgerhq/live-common/lib/families/polkadot/types";
+
+import { Flex } from "@ledgerhq/native-ui";
+import { ScreenName, NavigatorName } from "../../../const";
+import AccountDelegationInfo from "../../../components/AccountDelegationInfo";
+import { urls } from "../../../config/urls";
+import AccountSectionLabel from "../../../components/AccountSectionLabel";
+import Alert from "../../../components/Alert";
+
+import CollapsibleList from "../components/CollapsibleList";
+import NominationDrawer from "../components/NominationDrawer";
+import { NominateAction, RebondAction, SetControllerAction } from "./Actions";
+import { getDrawerInfo } from "./drawerInfo";
+import NominationRow from "./NominationRow";
+import UnlockingRow from "./UnlockingRow";
+import {
+ ExternalControllerUnsupportedWarning,
+ ExternalStashUnsupportedWarning,
+} from "./UnsupportedWarning";
+import Illustration from "../../../images/illustration/Illustration";
+import EarnLight from "../../../images/illustration/Earn.light.png";
+import EarnDark from "../../../images/illustration/Earn.dark.png";
+
+type Props = {
+ account: Account,
+};
+
+export default function Nominations({ account }: Props) {
+ const { t } = useTranslation();
+ const mainAccount = getMainAccount(account);
+
+ const navigation = useNavigation();
+
+ const { staking, validators } = usePolkadotPreloadData();
+
+ const { polkadotResources } = mainAccount;
+
+ const { lockedBalance, unlockedBalance, nominations, unlockings } =
+ polkadotResources || {};
+
+ const [nomination, setNomination] = useState();
+
+ const mappedNominations = useMemo(() => {
+ const all =
+ nominations?.map(nomination => {
+ const validator = validators.find(
+ v => v.address === nomination.address,
+ );
+ return {
+ nomination,
+ validator,
+ };
+ }) || [];
+
+ return all.reduce(
+ (sections, mapped) => {
+ if (mapped.nomination.status === "active") {
+ sections.uncollapsed.push(mapped);
+ } else {
+ sections.collapsed.push(mapped);
+ }
+ return sections;
+ },
+ { uncollapsed: [], collapsed: [] },
+ );
+ }, [nominations, validators]);
+
+ const mappedNomination = useMemo(() => {
+ if (nomination) {
+ const validator = validators.find(v => v.address === nomination.address);
+ return {
+ nomination,
+ validator,
+ };
+ }
+ return null;
+ }, [nomination, validators]);
+
+ const mappedUnlockings = useMemo(() => {
+ const now = new Date(Date.now());
+ const withoutUnlocked =
+ unlockings?.filter(({ completionDate }) =>
+ isAfter(completionDate, now),
+ ) ?? [];
+
+ const [firstRow, ...otherRows] =
+ unlockedBalance && unlockedBalance.gt(0)
+ ? [{ amount: unlockedBalance, completionDate: now }, ...withoutUnlocked]
+ : withoutUnlocked;
+
+ return { uncollapsed: firstRow ? [firstRow] : [], collapsed: otherRows };
+ }, [unlockings, unlockedBalance]);
+
+ const onNavigate = useCallback(
+ ({
+ route,
+ screen,
+ params,
+ }: {
+ route: typeof NavigatorName | typeof ScreenName,
+ screen?: typeof ScreenName,
+ params?: { [key: string]: any },
+ }) => {
+ setNomination();
+ navigation.navigate(route, {
+ screen,
+ params: { ...params, accountId: account.id },
+ });
+ },
+ [navigation, account.id],
+ );
+
+ const onEarnRewards = useCallback(() => {
+ isStash(account)
+ ? onNavigate({
+ route: NavigatorName.PolkadotNominateFlow,
+ screen: ScreenName.PolkadotNominateSelectValidators,
+ })
+ : onNavigate({
+ route: NavigatorName.PolkadotBondFlow,
+ screen: ScreenName.PolkadotBondStarted,
+ });
+ }, [account, onNavigate]);
+
+ const onNominate = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.PolkadotNominateFlow,
+ screen: ScreenName.PolkadotNominateSelectValidators,
+ });
+ }, [onNavigate]);
+
+ const onSetController = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.PolkadotSimpleOperationFlow,
+ screen: ScreenName.PolkadotSimpleOperationStarted,
+ params: {
+ mode: "setController",
+ },
+ });
+ }, [onNavigate]);
+
+ const onRebond = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.PolkadotRebondFlow,
+ screen: ScreenName.PolkadotRebondAmount,
+ });
+ }, [onNavigate]);
+
+ const onWithdraw = useCallback(() => {
+ onNavigate({
+ route: NavigatorName.PolkadotSimpleOperationFlow,
+ screen: ScreenName.PolkadotSimpleOperationStarted,
+ params: {
+ mode: "withdrawUnbonded",
+ },
+ });
+ }, [onNavigate]);
+
+ const onCloseDrawer = useCallback(() => {
+ setNomination();
+ }, []);
+
+ const onOpenExplorer = useCallback(
+ (address: string) => {
+ const url = getAddressExplorer(
+ getDefaultExplorerView(account.currency),
+ address,
+ );
+ if (url) Linking.openURL(url);
+ },
+ [account.currency],
+ );
+
+ const drawerInfo = useMemo(
+ () =>
+ mappedNomination
+ ? getDrawerInfo({
+ t,
+ account,
+ onOpenExplorer,
+ nomination: mappedNomination?.nomination,
+ validator: mappedNomination?.validator,
+ })
+ : [],
+ [mappedNomination, t, account, onOpenExplorer],
+ );
+
+ const electionOpen =
+ staking?.electionClosed !== undefined ? !staking?.electionClosed : false;
+
+ const hasBondedBalance = lockedBalance && lockedBalance.gt(0);
+ const hasUnlockedBalance = unlockedBalance && unlockedBalance.gt(0);
+ const hasNominations = nominations && nominations?.length > 0;
+ const hasUnlockings = unlockings && unlockings.length > 0;
+ const hasPendingBondOperation = hasPendingOperationType(account, "BOND");
+ const hasPendingWithdrawUnbondedOperation = hasPendingOperationType(
+ account,
+ "WITHDRAW_UNBONDED",
+ );
+
+ const nominateEnabled = !electionOpen && canNominate(account);
+ const rebondEnabled = !electionOpen && !!hasUnlockings;
+ const withdrawEnabled =
+ !electionOpen && hasUnlockedBalance && !hasPendingWithdrawUnbondedOperation;
+ const earnRewardsEnabled =
+ !electionOpen && !hasBondedBalance && !hasPendingBondOperation;
+
+ const renderNomination = useCallback(
+ ({ nomination, validator }, i, isLast) => (
+
+ setNomination(nomination)}
+ isLast={isLast}
+ />
+
+ ),
+ [account],
+ );
+
+ const renderShowInactiveNominations = useCallback(
+ collapsed =>
+ collapsed
+ ? t("polkadot.nomination.showInactiveNominations", {
+ count: mappedNominations.collapsed.length,
+ })
+ : t("polkadot.nomination.hideInactiveNominations"),
+ [t, mappedNominations],
+ );
+
+ const renderUnlocking = useCallback(
+ (unlocking, i, isLast) => (
+
+
+
+ ),
+ [account, onWithdraw, withdrawEnabled],
+ );
+
+ const renderShowAllUnlockings = useCallback(
+ collapsed =>
+ collapsed
+ ? t("polkadot.nomination.showAllUnlockings", {
+ count: mappedUnlockings.collapsed.length,
+ })
+ : t("polkadot.nomination.hideAllUnlockings"),
+ [t, mappedUnlockings],
+ );
+
+ if (hasExternalController(account)) {
+ return (
+
+
+ }
+ />
+
+
+ );
+ }
+ if (hasExternalStash(account)) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+ 0}
+ onClose={onCloseDrawer}
+ account={account}
+ ValidatorImage={({ size }) =>
+ mappedNomination?.nomination.address ? (
+
+ ) : null
+ }
+ data={drawerInfo}
+ isNominated
+ />
+ {electionOpen && (
+
+ {t("polkadot.info.electionOpen.description")}
+
+ )}
+ {!hasBondedBalance && hasPendingBondOperation && (
+
+ {t("polkadot.nomination.hasPendingBondOperation")}
+
+ )}
+ {!hasNominations ? (
+
+
+ }
+ description={t("polkadot.nomination.emptyState.description", {
+ name: account.currency.name,
+ })}
+ infoUrl={urls.polkadotStaking}
+ infoTitle={t("polkadot.nomination.emptyState.info")}
+ onPress={onEarnRewards}
+ disabled={!(earnRewardsEnabled || nominateEnabled)}
+ ctaTitle={
+ !hasBondedBalance && !hasPendingBondOperation
+ ? t("polkadot.nomination.emptyState.cta")
+ : t("polkadot.nomination.nominate")
+ }
+ />
+ ) : (
+
+
+ }
+ />
+
+ {!mappedNominations.uncollapsed.length && (
+
+ {t("polkadot.nomination.noActiveNominations")}
+
+ )}
+
+
+ )}
+
+ {hasUnlockings ? (
+
+
+ }
+ />
+
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ margin: 16,
+ },
+ illustration: { alignSelf: "center", marginBottom: 16 },
+ wrapper: {
+ marginBottom: 16,
+ },
+});
diff --git a/src/families/polkadot/SimpleOperationFlow/01-Started.js b/src/families/polkadot/SimpleOperationFlow/01-Started.js
index 7d78d62856..2bcadedbc1 100644
--- a/src/families/polkadot/SimpleOperationFlow/01-Started.js
+++ b/src/families/polkadot/SimpleOperationFlow/01-Started.js
@@ -1,7 +1,7 @@
// @flow
import invariant from "invariant";
import React, { useCallback } from "react";
-import { StyleSheet, View, SafeAreaView } from "react-native";
+import { StyleSheet, View } from "react-native";
import { useTranslation, Trans } from "react-i18next";
import { useSelector } from "react-redux";
import { useTheme } from "@react-navigation/native";
@@ -14,9 +14,7 @@ import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTran
import { accountScreenSelector } from "../../../reducers/accounts";
import { ScreenName } from "../../../const";
import { TrackScreen } from "../../../analytics";
-import Alert from "../../../components/Alert";
-import Button from "../../../components/Button";
-import LText from "../../../components/LText";
+import { Button, Alert, Text, Log } from "@ledgerhq/native-ui";
import TranslatedError from "../../../components/TranslatedError";
import FlowErrorBottomModal from "../components/FlowErrorBottomModal";
@@ -94,7 +92,7 @@ export default function PolkadotSimpleOperationStarted({
return (
<>
-
-
+
-
- {infoTranslated ? (
- {infoTranslated}
- ) : null}
+
+ {infoTranslated ? : null}
{(error || warning) && (
-
-
+
)}
- }
+ type="main"
onPress={onContinue}
disabled={!!error || bridgePending}
- />
+ >
+
+
-
+
}
- autoFocus
style={styles.inputContainer}
inputStyle={[
styles.inputStyle,
diff --git a/src/families/polkadot/components/CollapsibleList.tsx b/src/families/polkadot/components/CollapsibleList.tsx
new file mode 100644
index 0000000000..9cd91cbf92
--- /dev/null
+++ b/src/families/polkadot/components/CollapsibleList.tsx
@@ -0,0 +1,98 @@
+import { useTheme } from "@react-navigation/native";
+import React, { useState, useCallback } from "react";
+import { View, StyleSheet } from "react-native";
+
+import { DropdownMedium, DropupMedium } from "@ledgerhq/native-ui/assets/icons";
+import Button from "../../../components/wrappedUi/Button";
+
+type Props = {
+ children?: React.ReactNode;
+ uncollapsedItems: Array;
+ collapsedItems: Array;
+ renderItem: (item: any, index: number, isLast: boolean) => React.ReactNode;
+ renderShowMore: (collapsed: boolean) => React.ReactNode;
+};
+
+const CollapsibleList = ({
+ children,
+ uncollapsedItems,
+ collapsedItems,
+ renderItem,
+ renderShowMore,
+}: Props) => {
+ const { colors } = useTheme();
+ const [collapsed, setCollapsed] = useState(true);
+
+ const toggleCollapsed = useCallback(() => {
+ setCollapsed(collapsed => !collapsed);
+ }, []);
+
+ return (
+ <>
+
+ {children}
+ {uncollapsedItems.map((item, i) =>
+ renderItem(item, i, collapsed && i === uncollapsedItems.length - 1),
+ )}
+ {collapsedItems.length !== 0 ? (
+ <>
+
+ {collapsedItems.map((item, i) =>
+ renderItem(item, i, i === collapsedItems.length - 1),
+ )}
+
+
+
+
+ >
+ ) : null}
+
+ {!!collapsed && collapsedItems.length ? (
+
+ ) : null}
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ borderTopLeftRadius: 4,
+ borderTopRightRadius: 4,
+ },
+ list: {},
+ elevated: {},
+ visible: {
+ display: "flex",
+ },
+ hidden: {
+ display: "none",
+ },
+ showMoreIndicator: {
+ zIndex: 1,
+ height: 7,
+ marginRight: 5,
+ marginLeft: 5,
+ },
+ showMore: {
+ borderTopWidth: 1,
+ },
+ buttonIcon: { paddingLeft: 6 },
+});
+
+export default React.memo(CollapsibleList);
diff --git a/src/families/polkadot/components/NominationDrawer.js b/src/families/polkadot/components/NominationDrawer.js
index 578edebbf3..faf2d906b4 100644
--- a/src/families/polkadot/components/NominationDrawer.js
+++ b/src/families/polkadot/components/NominationDrawer.js
@@ -8,12 +8,10 @@ import type { AccountLike } from "@ledgerhq/live-common/lib/types";
// TODO move to component
import { useTheme } from "@react-navigation/native";
import DelegatingContainer from "../../tezos/DelegatingContainer";
-import Close from "../../../icons/Close";
import { rgba } from "../../../colors";
import getWindowDimensions from "../../../logic/getWindowDimensions";
import BottomModal from "../../../components/BottomModal";
import Circle from "../../../components/Circle";
-import Touchable from "../../../components/Touchable";
import LText from "../../../components/LText";
import CurrencyIcon from "../../../components/CurrencyIcon";
import IconHelp from "../../../icons/Info";
@@ -41,7 +39,6 @@ export default function NominationDrawer({
icon,
isNominated,
}: Props) {
- const { colors } = useTheme();
const currency = getAccountCurrency(account);
const color = getCurrencyColor(currency);
@@ -55,16 +52,6 @@ export default function NominationDrawer({
onClose={onClose}
>
-
-
-
-
-
-
{isNominated ? (
-
+
(
+
+ {children}
+
+);
+
+const placeholderProps = {
+ width: 40,
+ containerHeight: 20,
+};
+
+export default function TezosAccountBodyHeader({
+ account,
+ parentAccount,
+}: {
+ account: AccountLike;
+ parentAccount?: Account;
+}) {
+ const [openedModal, setOpenedModal] = useState(false);
+
+ const onModalClose = useCallback(() => {
+ setOpenedModal(false);
+ }, []);
+
+ const onViewDetails = useCallback(() => {
+ setOpenedModal(true);
+ }, []);
+
+ const delegation = useDelegation(account);
+
+ if (!delegation) return null;
+
+ const name = delegation.baker
+ ? delegation.baker.name
+ : shortAddressPreview(delegation.address);
+ const amount = account.balance;
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+ const days = differenceInCalendarDays(Date.now(), delegation.operation.date);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+ {days ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/families/tezos/AccountHeader.tsx b/src/families/tezos/AccountHeader.tsx
new file mode 100644
index 0000000000..3646db0814
--- /dev/null
+++ b/src/families/tezos/AccountHeader.tsx
@@ -0,0 +1,77 @@
+import React, { useCallback } from "react";
+import { Trans } from "react-i18next";
+import { StyleSheet, View } from "react-native";
+import { useNavigation } from "@react-navigation/native";
+import { AccountLike, Account } from "@ledgerhq/live-common/lib/types";
+import { getMainAccount } from "@ledgerhq/live-common/lib/account";
+import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+import { isAccountDelegating } from "@ledgerhq/live-common/lib/families/tezos/bakers";
+import { Text } from "@ledgerhq/native-ui";
+import { ScreenName } from "../../const";
+import IlluStaking from "./IlluStaking";
+import Button from "../../components/wrappedUi/Button";
+
+const styles = StyleSheet.create({
+ banner: {
+ margin: 16,
+ marginBottom: 0,
+ minHeight: 128,
+ padding: 16,
+ position: "relative",
+ borderRadius: 4,
+ overflow: "hidden",
+ },
+ bannerImage: {
+ position: "absolute",
+ right: 24,
+ bottom: -38,
+ },
+ title: {
+ marginRight: 90,
+ },
+ btn: {
+ marginTop: 16,
+ width: 160,
+ },
+});
+
+type Props = {
+ account: AccountLike;
+ parentAccount?: Account;
+};
+
+export default function TezosAccountHeader({ account, parentAccount }: Props) {
+ const navigation = useNavigation();
+
+ const onEarnRewards = useCallback(() => {
+ navigation.navigate(ScreenName.TezosDelegationFlow, {
+ screen: "DelegationStarted",
+ params: {
+ accountId: account.id,
+ parentId: parentAccount ? parentAccount.id : undefined,
+ },
+ });
+ }, [navigation, account, parentAccount]);
+
+ const mainAccount = getMainAccount(account, parentAccount);
+ const backgroundColor = getCurrencyColor(mainAccount.currency);
+
+ if (isAccountDelegating(account) || account.type !== "Account") return null;
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/families/tezos/DelegationDetailsModal.js b/src/families/tezos/DelegationDetailsModal.js
index 70786fb546..53eeb81599 100644
--- a/src/families/tezos/DelegationDetailsModal.js
+++ b/src/families/tezos/DelegationDetailsModal.js
@@ -30,7 +30,6 @@ import Touchable from "../../components/Touchable";
import BottomModal from "../../components/BottomModal";
import Circle from "../../components/Circle";
import NavigationScrollView from "../../components/NavigationScrollView";
-import Close from "../../icons/Close";
import { rgba } from "../../colors";
import { NavigatorName, ScreenName } from "../../const";
import BakerImage from "./BakerImage";
@@ -48,11 +47,6 @@ const styles = StyleSheet.create({
modal: {
position: "relative",
},
- close: {
- position: "absolute",
- top: 8,
- right: 16,
- },
root: {
padding: 16,
},
@@ -229,16 +223,6 @@ export default function DelegationDetailsModal({
style={styles.modal}
>
-
-
-
-
-
-
diff --git a/src/families/tezos/DelegationFlow/Started.tsx b/src/families/tezos/DelegationFlow/Started.tsx
new file mode 100644
index 0000000000..641f97392e
--- /dev/null
+++ b/src/families/tezos/DelegationFlow/Started.tsx
@@ -0,0 +1,77 @@
+import React, { useCallback } from "react";
+import { Linking } from "react-native";
+import { Trans } from "react-i18next";
+import { Flex, Text, Icons, List, Link, Button, Log } from "@ledgerhq/native-ui";
+import { ScreenName } from "../../../const";
+import { TrackScreen } from "../../../analytics";
+import { urls } from "../../../config/urls";
+import Illustration from "../../../images/illustration/Illustration";
+import EarnLight from "../../../images/illustration/Earn.light.png";
+import EarnDark from "../../../images/illustration/Earn.dark.png";
+
+type Props = {
+ navigation: any;
+ route: { params: any };
+};
+
+const Check = ;
+
+export default function DelegationStarted({ navigation, route }: Props) {
+ const onNext = useCallback(() => {
+ navigation.navigate(ScreenName.DelegationSummary, {
+ ...route.params,
+ });
+ }, [navigation, route.params]);
+
+ const howDelegationWorks = useCallback(() => {
+ Linking.openURL(urls.delegation);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+ ,
+ ,
+ ].map(wording => ({ title: wording, bullet: Check }))}
+ itemContainerProps={{
+ alignItems: "center",
+ }}
+ my={8}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/families/tezos/DelegationFlow/Summary.tsx b/src/families/tezos/DelegationFlow/Summary.tsx
new file mode 100644
index 0000000000..4e15a9d43a
--- /dev/null
+++ b/src/families/tezos/DelegationFlow/Summary.tsx
@@ -0,0 +1,467 @@
+/* @flow */
+import React, { useCallback, useEffect, useState } from "react";
+import { View, StyleSheet, Animated } from "react-native";
+import SafeAreaView from "react-native-safe-area-view";
+import { useSelector } from "react-redux";
+import { Trans } from "react-i18next";
+import invariant from "invariant";
+import Icon from "react-native-vector-icons/dist/Feather";
+import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge";
+import {
+ getAccountCurrency,
+ getAccountName,
+ getAccountUnit,
+ shortAddressPreview,
+} from "@ledgerhq/live-common/lib/account";
+import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction";
+import {
+ useDelegation,
+ useBaker,
+ useBakers,
+ useRandomBaker,
+} from "@ledgerhq/live-common/lib/families/tezos/bakers";
+import whitelist from "@ledgerhq/live-common/lib/families/tezos/bakers.whitelist-default";
+import type { AccountLike } from "@ledgerhq/live-common/lib/types";
+import { useTheme } from "@react-navigation/native";
+import { Alert } from "@ledgerhq/native-ui";
+import { accountScreenSelector } from "../../../reducers/accounts";
+import { rgba } from "../../../colors";
+import { ScreenName } from "../../../const";
+import { TrackScreen } from "../../../analytics";
+import { useTransactionChangeFromNavigation } from "../../../logic/screenTransactionHooks";
+import Button from "../../../components/Button";
+import LText from "../../../components/LText";
+import Circle from "../../../components/Circle";
+import CurrencyIcon from "../../../components/CurrencyIcon";
+import CurrencyUnitValue from "../../../components/CurrencyUnitValue";
+import Touchable from "../../../components/Touchable";
+import DelegatingContainer from "../DelegatingContainer";
+import BakerImage from "../BakerImage";
+
+const forceInset = { bottom: "always" };
+
+type Props = {
+ navigation: any,
+ route: { params: RouteParams },
+};
+
+type RouteParams = {
+ mode?: "delegate" | "undelegate",
+ accountId: string,
+ parentId?: string,
+};
+
+const AccountBalanceTag = ({ account }: { account: AccountLike }) => {
+ const unit = getAccountUnit(account);
+ const { colors } = useTheme();
+ return (
+
+
+
+
+
+ );
+};
+
+const ChangeDelegator = () => {
+ const { colors } = useTheme();
+ return (
+
+
+
+ );
+};
+
+const Line = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+const Words = ({
+ children,
+ highlighted,
+ style,
+}: {
+ children: React.ReactNode,
+ highlighted?: boolean,
+ style?: any,
+}) => (
+
+ {children}
+
+);
+
+const BakerSelection = ({
+ name,
+ readOnly,
+}: {
+ name: string,
+ readOnly?: boolean,
+}) => {
+ const { colors } = useTheme();
+ return (
+
+
+ {name}
+
+ {readOnly ? null : (
+
+
+
+ )}
+
+ );
+};
+
+export default function DelegationSummary({ navigation, route }: Props) {
+ const { colors } = useTheme();
+ const { account, parentAccount } = useSelector(accountScreenSelector(route));
+ const bakers = useBakers(whitelist);
+ const randomBaker = useRandomBaker(bakers);
+
+ const {
+ transaction,
+ setTransaction,
+ status,
+ bridgePending,
+ bridgeError,
+ } = useBridgeTransaction(() => ({
+ account,
+ parentAccount,
+ }));
+
+ invariant(account, "account must be defined");
+ invariant(transaction, "transaction must be defined");
+ invariant(transaction.family === "tezos", "transaction tezos");
+
+ // make sure tx is in sync
+ useEffect(() => {
+ if (!transaction || !account) return;
+ invariant(transaction.family === "tezos", "tezos tx");
+
+ // make sure the mode is in sync (an account changes can reset it)
+ const patch: Object = {
+ mode: route.params?.mode ?? "delegate",
+ };
+
+ // make sure that in delegate mode, a transaction recipient is set (random pick)
+ if (patch.mode === "delegate" && !transaction.recipient && randomBaker) {
+ patch.recipient = randomBaker.address;
+ }
+
+ // when changes, we set again
+ if (patch.mode !== transaction.mode || "recipient" in patch) {
+ setTransaction(
+ getAccountBridge(account, parentAccount).updateTransaction(
+ transaction,
+ patch,
+ ),
+ );
+ }
+ }, [
+ account,
+ randomBaker,
+ navigation,
+ parentAccount,
+ setTransaction,
+ transaction,
+ route.params,
+ ]);
+
+ const [rotateAnim] = useState(() => new Animated.Value(0));
+ useEffect(() => {
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(rotateAnim, {
+ toValue: 1,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ Animated.timing(rotateAnim, {
+ toValue: -1,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ Animated.timing(rotateAnim, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ Animated.delay(1000),
+ ]),
+ ).start();
+ return () => {
+ rotateAnim.setValue(0);
+ };
+ }, [rotateAnim]);
+
+ const rotate = rotateAnim.interpolate({
+ inputRange: [0, 1],
+ // $FlowFixMe
+ outputRange: ["0deg", "30deg"],
+ });
+
+ const onChangeDelegator = useCallback(() => {
+ rotateAnim.setValue(0);
+ navigation.navigate(ScreenName.DelegationSelectValidator, {
+ ...route.params,
+ transaction,
+ });
+ }, [rotateAnim, navigation, transaction, route.params]);
+
+ const delegation = useDelegation(account);
+ const addr =
+ transaction.mode === "undelegate"
+ ? (delegation && delegation.address) || ""
+ : transaction.recipient;
+ const baker = useBaker(addr);
+ const bakerName = baker ? baker.name : shortAddressPreview(addr);
+ const currency = getAccountCurrency(account);
+ const color = getCurrencyColor(currency);
+ const accountName = getAccountName(account);
+
+ // handle any edit screen changes
+ useTransactionChangeFromNavigation(setTransaction);
+
+ const onContinue = useCallback(async () => {
+ navigation.navigate(ScreenName.DelegationSelectDevice, {
+ accountId: account.id,
+ parentId: parentAccount && parentAccount.id,
+ transaction,
+ status,
+ });
+ }, [status, account, parentAccount, navigation, transaction]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ right={
+ transaction.mode === "delegate" ? (
+
+
+
+
+
+
+
+
+ ) : (
+
+ )
+ }
+ />
+
+
+
+
+ {transaction.mode === "delegate" ? (
+
+ ) : (
+
+ )}
+
+
+ {accountName}
+
+
+
+ {transaction.mode === "delegate" ? (
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+ {baker && transaction.mode === "delegate" ? (
+ baker.capacityStatus === "full" ? null : (
+ /*
+
+
+
+
+
+
+ */
+
+
+
+
+
+
+
+ )
+ ) : null}
+
+
+
+
+ {transaction.mode === "undelegate" ? (
+ } />
+ ) : (
+ } />
+ )}
+ }
+ containerStyle={styles.continueButton}
+ onPress={onContinue}
+ disabled={bridgePending || !!bridgeError}
+ pending={bridgePending}
+ />
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ flexDirection: "column",
+ },
+ body: {
+ flex: 1,
+ paddingHorizontal: 16,
+ justifyContent: "space-around",
+ },
+ bakerCircle: {
+ borderWidth: 1,
+ borderStyle: "dashed",
+ },
+ changeDelegator: {
+ position: "absolute",
+ right: -4,
+ top: -4,
+ },
+ delegatingAccount: {
+ paddingTop: 26,
+ },
+ accountBalanceTag: {
+ marginTop: 8,
+ borderRadius: 4,
+ padding: 4,
+ alignItems: "center",
+ },
+ accountBalanceTagText: {
+ fontSize: 11,
+ },
+ accountName: {
+ maxWidth: 180,
+ },
+ summary: {
+ alignItems: "center",
+ marginVertical: 30,
+ },
+ summaryLine: {
+ marginVertical: 10,
+ flexDirection: "row",
+ height: 40,
+ alignItems: "center",
+ },
+ summaryWords: {
+ marginRight: 6,
+ fontSize: 18,
+ },
+ bakerSelection: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 4,
+ height: 40,
+ },
+ bakerSelectionText: {
+ paddingHorizontal: 8,
+ fontSize: 18,
+ maxWidth: 240,
+ },
+ bakerSelectionIcon: {
+ borderTopRightRadius: 4,
+ borderBottomRightRadius: 4,
+ alignItems: "center",
+ justifyContent: "center",
+ width: 32,
+ height: 40,
+ },
+ footer: {
+ flexDirection: "column",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ },
+ continueButton: {
+ alignSelf: "stretch",
+ marginTop: 16,
+ },
+});
diff --git a/src/families/tezos/DelegationFlow/index.js b/src/families/tezos/DelegationFlow/index.js
index aadcde4502..55a8a307df 100644
--- a/src/families/tezos/DelegationFlow/index.js
+++ b/src/families/tezos/DelegationFlow/index.js
@@ -35,9 +35,7 @@ function DelegationFlow() {
name={ScreenName.DelegationStarted}
component={DelegationStarted}
options={{
- headerTitle: () => (
-
- ),
+ headerTitle: () => ,
}}
/>
void;
+ selected: boolean;
+};
+
+function Item({
+ item,
+ selected,
+ disabled,
+ onSelectSuperRepresentative,
+}: Props) {
+ const locale = useSelector(localeSelector);
+ const { colors } = useTheme();
+ const { sr, isSR, rank, address } = item;
+
+ const onSelect = useCallback(
+ () => onSelectSuperRepresentative(item, selected),
+ [onSelectSuperRepresentative, item, selected],
+ );
+
+ const isDisabled = !selected && disabled;
+
+ return (
+
+
+
+
+
+
+
+ {rank}. {(sr && sr.name) || address}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: {
+ flexDirection: "row",
+ alignItems: "center",
+ padding: 16,
+ },
+ iconWrapper: {
+ height: 36,
+ width: 36,
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: 5,
+ marginRight: 12,
+ },
+ nameWrapper: {
+ flex: 1,
+ paddingRight: 16,
+ },
+ nameText: {
+ fontSize: 15,
+ },
+ subText: {
+ fontSize: 13,
+ },
+});
+
+export default memo(Item);
diff --git a/src/families/tron/VoteFlow/01-SelectValidator/SearchBox.tsx b/src/families/tron/VoteFlow/01-SelectValidator/SearchBox.tsx
new file mode 100644
index 0000000000..f3af2bb015
--- /dev/null
+++ b/src/families/tron/VoteFlow/01-SelectValidator/SearchBox.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Box, SearchInput } from "@ledgerhq/native-ui";
+
+export default function SelectValidatorSearchBox({
+ searchQuery,
+ setSearchQuery,
+}: {
+ searchQuery: string;
+ setSearchQuery: (query: string) => void;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ );
+}
diff --git a/src/families/tron/VoteFlow/02-VoteModal.js b/src/families/tron/VoteFlow/02-VoteModal.js
index 0ca01f4675..f655b58047 100644
--- a/src/families/tron/VoteFlow/02-VoteModal.js
+++ b/src/families/tron/VoteFlow/02-VoteModal.js
@@ -17,7 +17,6 @@ import Switch from "../../../components/Switch";
import BottomModal from "../../../components/BottomModal";
import Button from "../../../components/Button";
import LText from "../../../components/LText";
-import Close from "../../../icons/Close";
import Trash from "../../../icons/Trash";
import Check from "../../../icons/Check";
@@ -120,10 +119,6 @@ const VoteModal = ({
{name || address}
-
-
-
-
void;
+ onRemove: (vote: Vote) => void;
+ onClose: () => void;
+ votes: Vote[];
+};
+
+const VoteModal = ({
+ vote,
+ name,
+ tronPower,
+ onChange,
+ onClose,
+ onRemove,
+ votes,
+}: Props) => {
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const { address, voteCount } = vote || {};
+
+ const [value, setValue] = useState(voteCount);
+
+ const [useAllAmount, setUseAllAmount] = useState(false);
+
+ const inputRef = useRef();
+
+ const { current: votesAvailable } = useRef(
+ tronPower -
+ votes
+ .filter(v => v.address !== address)
+ .reduce((sum, { voteCount }) => sum + voteCount, 0),
+ );
+
+ const focusInput = useCallback(() => {
+ if (inputRef && inputRef.current) inputRef.current.focus();
+ }, [inputRef]);
+
+ const handleChange = useCallback(
+ text => {
+ setValue(parseInt(text || "0", 10));
+ setUseAllAmount(false);
+ },
+ [setValue],
+ );
+
+ const toggleUseAllAmount = useCallback(() => {
+ handleChange(!useAllAmount ? votesAvailable : 0);
+ setUseAllAmount(!useAllAmount);
+ }, [handleChange, useAllAmount, votesAvailable]);
+
+ const onContinue = useCallback(
+ () => onChange({ address, voteCount: value }),
+ [address, onChange, value],
+ );
+
+ const remove = useCallback(() => onRemove(vote), [onRemove, vote]);
+
+ const votesRemaining = useMemo(() => Math.max(0, votesAvailable - value), [
+ value,
+ votesAvailable,
+ ]);
+
+ const error = value <= 0 || value > votesAvailable;
+
+ return (
+
+
+
+
+
+
+
+ {t("vote.castVotes.removeVotes")}
+
+
+
+
+
+ {error && value <= 0 ? (
+
+
+
+ ) : null}
+ {error ? (
+
+
+
+ text
+
+
+
+ ) : votesRemaining === 0 ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+ text
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ rootKeyboard: {
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ height,
+ flexShrink: 1,
+ },
+ topButton: {
+ width: 40,
+ height: 40,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ topContainer: {
+ width: "100%",
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ flexBasis: 50,
+ flexShrink: 0,
+ },
+ topLabel: {
+ flex: 1,
+ paddingTop: 16,
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ topSubTitle: {
+ fontSize: 13,
+ },
+ topTitle: {
+ fontSize: 15,
+ },
+ bottomWrapper: {
+ alignSelf: "stretch",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ paddingHorizontal: 16,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ },
+ button: {
+ flex: 1,
+ marginHorizontal: 8,
+ },
+ buttonRight: {
+ marginLeft: 8,
+ },
+ continueWrapper: {
+ alignSelf: "stretch",
+ alignItems: "stretch",
+ justifyContent: "flex-end",
+ },
+ container: {
+ flex: 1,
+ paddingTop: 16,
+ paddingHorizontal: 16,
+ alignItems: "stretch",
+ },
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ flexWrap: "nowrap",
+ },
+ availableRow: {
+ width: "100%",
+ },
+ available: {
+ flex: 1,
+ flexDirection: "column",
+ display: "flex",
+ alignItems: "flex-start",
+ justifyContent: "flex-end",
+ paddingVertical: 8,
+ marginBottom: 8,
+ height: 50,
+ },
+ availableRight: {
+ alignItems: "center",
+ justifyContent: "flex-end",
+ flexDirection: "row",
+ },
+ availableAmount: {
+ marginHorizontal: 3,
+ },
+ wrapper: {
+ flexDirection: "row",
+ alignItems: "center",
+ flexGrow: 1,
+ flexShrink: 1,
+ },
+ inputStyle: {
+ flex: 1,
+ ...getFontStyle(),
+ textAlign: "center",
+
+ fontSize: 32,
+ },
+ maxLabel: {
+ marginRight: 4,
+ },
+ switch: {
+ opacity: 0.99,
+ },
+ availableSuccess: { marginLeft: 10 },
+});
+
+export default memo(VoteModal);
diff --git a/src/families/tron/VoteFlow/02-VoteRow.tsx b/src/families/tron/VoteFlow/02-VoteRow.tsx
new file mode 100644
index 0000000000..01e9a0f1fc
--- /dev/null
+++ b/src/families/tron/VoteFlow/02-VoteRow.tsx
@@ -0,0 +1,265 @@
+import React, { useCallback, memo, useRef, useEffect } from "react";
+import { View, StyleSheet, TouchableOpacity, Animated } from "react-native";
+import { Trans } from "react-i18next";
+
+import {
+ Vote,
+ SuperRepresentative,
+} from "@ledgerhq/live-common/lib/families/tron/types";
+
+import Swipeable from "react-native-gesture-handler/Swipeable";
+import * as Animatable from "react-native-animatable";
+
+import { useTheme } from "@react-navigation/native";
+import { Box, BoxedIcon, Text } from "@ledgerhq/native-ui";
+import {
+ MedalMedium,
+ TrophyMedium,
+ PenMedium,
+} from "@ledgerhq/native-ui/assets/icons";
+import getWindowDimensions from "../../../logic/getWindowDimensions";
+import Trash from "../../../icons/Trash";
+
+const { width } = getWindowDimensions();
+
+const RightAction = ({
+ dragX,
+ onRemove,
+}: {
+ dragX: any;
+ onRemove: () => void;
+}) => {
+ const { colors } = useTheme();
+ const scale = dragX.interpolate({
+ inputRange: [-57, -56, -16, 0],
+ outputRange: [1, 1, 0.5, 0],
+ });
+
+ return (
+
+
+
+
+
+ );
+};
+
+type VoteRowProps = {
+ vote: Vote & {
+ isSR: boolean;
+ rank: number;
+ validator?: SuperRepresentative;
+ };
+ onEdit: (vote: Vote, name: string) => void;
+ onRemove: (vote: Vote) => void;
+ index: number;
+ onOpen: (i: number) => void;
+ openIndex: number;
+};
+
+const VoteRow = ({
+ vote,
+ onEdit,
+ onRemove,
+ index,
+ onOpen,
+ openIndex,
+}: VoteRowProps) => {
+ const { colors } = useTheme();
+ const rowRef = useRef();
+ const swipeRef = useRef();
+ const { address, voteCount, isSR, rank, validator } = vote;
+ const { name } = validator || {};
+
+ /** Animate swipe gesture at the begining */
+ useEffect(() => {
+ if (index === 0 && swipeRef && swipeRef.current) {
+ setTimeout(() => {
+ if (swipeRef.current && swipeRef.current.openRight) {
+ swipeRef.current.openRight();
+ setTimeout(() => {
+ if (swipeRef.current && swipeRef.current.close)
+ swipeRef.current.close();
+ }, 1000);
+ }
+ }, 400);
+ }
+ }, [index, swipeRef]);
+
+ const removeVote = useCallback(() => onRemove({ address, voteCount }), [
+ address,
+ voteCount,
+ onRemove,
+ ]);
+
+ useEffect(() => {
+ if (openIndex !== index && swipeRef.current && swipeRef.current.close)
+ swipeRef.current.close();
+ }, [index, openIndex, swipeRef]);
+
+ const removeVoteAnimStart = useCallback(() => {
+ if (rowRef && rowRef.current && rowRef.current.transitionTo)
+ rowRef.current.transitionTo(
+ { opacity: 0, height: 0, marginVertical: 0 },
+ 400,
+ );
+ else removeVote();
+ }, [rowRef, removeVote]);
+
+ return (
+
+ (
+
+ )}
+ onSwipeableRightWillOpen={() => onOpen(index)}
+ >
+ onEdit({ address, voteCount }, name || address)}
+ >
+
+
+
+
+
+
+ {name || address}
+
+
+
+
+ text
+
+
+
+
+
+
+
+ {voteCount}
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ root: {
+ height: 80,
+ width: "100%",
+ marginVertical: 5,
+ overflow: "visible",
+ },
+ srRow: {
+ height: 75,
+ width: width - 32,
+ left: 16,
+ borderRadius: 4,
+ flexDirection: "column",
+ zIndex: 10,
+ paddingHorizontal: 16,
+ borderWidth: StyleSheet.hairlineWidth,
+ },
+ row: {
+ flex: 1,
+ flexDirection: "row",
+ alignItems: "center",
+
+ paddingVertical: 8,
+ },
+ rowIcon: {
+ alignItems: "center",
+ justifyContent: "center",
+ width: 36,
+ height: 36,
+ borderRadius: 5,
+
+ marginRight: 12,
+ },
+ rowTitle: {
+ fontSize: 14,
+ lineHeight: 16,
+
+ paddingBottom: 4,
+ },
+ rowLabelContainer: {
+ flexDirection: "column",
+ alignItems: "flex-start",
+ justifyContent: "flex-end",
+ flex: 1,
+ },
+ editButton: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ },
+ editVoteCount: {
+ fontSize: 17,
+
+ marginLeft: 6,
+ marginBottom: 2,
+ },
+ rowLabel: {
+ fontSize: 13,
+ },
+ rightDrawer: {
+ width: 56,
+ paddingRight: 16,
+ height: "100%",
+ alignItems: "flex-start",
+ justifyContent: "center",
+ },
+ removeButton: {
+ alignItems: "center",
+ justifyContent: "center",
+ width: 40,
+ height: 40,
+ borderRadius: 4,
+ },
+});
+
+export default memo(VoteRow);
diff --git a/src/families/tron/Votes/Header.tsx b/src/families/tron/Votes/Header.tsx
new file mode 100644
index 0000000000..41a97e4046
--- /dev/null
+++ b/src/families/tron/Votes/Header.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { Trans, useTranslation } from "react-i18next";
+import AccountSectionLabel from "../../../components/AccountSectionLabel";
+import Link from "../../../components/wrappedUi/Link";
+
+type Props = {
+ count: number;
+ onPress: () => void;
+};
+
+export default function Header({ count, onPress }: Props) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ }
+ />
+ );
+}
diff --git a/src/families/tron/Votes/Row.tsx b/src/families/tron/Votes/Row.tsx
new file mode 100644
index 0000000000..af75bbfe4a
--- /dev/null
+++ b/src/families/tron/Votes/Row.tsx
@@ -0,0 +1,77 @@
+import React, { useCallback } from "react";
+import { Linking, TouchableOpacity } from "react-native";
+
+import { getAddressExplorer } from "@ledgerhq/live-common/lib/explorers";
+
+import { ExplorerView } from "@ledgerhq/live-common/lib/types";
+
+import { Box, BoxedIcon, Flex, Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import { MedalMedium, TrophyMedium } from "@ledgerhq/native-ui/assets/icons";
+import Clock from "../../../icons/Clock";
+
+type Props = {
+ validator: any;
+ address: string;
+ amount: number;
+ duration?: React.ReactNode;
+ explorerView?: ExplorerView;
+ isSR: boolean;
+ isLast?: boolean;
+};
+
+const Row = ({
+ validator,
+ address,
+ amount,
+ duration,
+ explorerView,
+ isSR,
+}: Props) => {
+ const { colors } = useTheme();
+ const srURL = explorerView && getAddressExplorer(explorerView, address);
+
+ const openSR = useCallback(() => {
+ if (srURL) Linking.openURL(srURL);
+ }, [srURL]);
+
+ return (
+
+
+
+
+
+
+
+
+ {validator ? validator.name : address}
+
+
+
+
+
+ {duration}
+
+
+
+
+
+ {amount}
+
+
+
+
+ );
+};
+
+export default Row;
diff --git a/src/families/tron/Votes/index.tsx b/src/families/tron/Votes/index.tsx
new file mode 100644
index 0000000000..371b2c8b87
--- /dev/null
+++ b/src/families/tron/Votes/index.tsx
@@ -0,0 +1,373 @@
+import React, { useCallback, useState, useMemo } from "react";
+import { View, TouchableOpacity, StyleSheet } from "react-native";
+import { useNavigation, useTheme } from "@react-navigation/native";
+import { Trans, useTranslation } from "react-i18next";
+import { BigNumber } from "bignumber.js";
+import {
+ getAccountUnit,
+ getAccountCurrency,
+} from "@ledgerhq/live-common/lib/account/helpers";
+import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction";
+import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge";
+import {
+ useTronSuperRepresentatives,
+ formatVotes,
+ getNextRewardDate,
+ getLastVotedDate,
+ MIN_TRANSACTION_AMOUNT,
+} from "@ledgerhq/live-common/lib/families/tron/react";
+import { getDefaultExplorerView } from "@ledgerhq/live-common/lib/explorers";
+import { Account } from "@ledgerhq/live-common/lib/types";
+import { Box, Button, Text } from "@ledgerhq/native-ui";
+import { urls } from "../../../config/urls";
+import Row from "./Row";
+import Header from "./Header";
+import LText from "../../../components/LText";
+import { NavigatorName, ScreenName } from "../../../const";
+import Info from "../../../icons/Info";
+import ArrowRight from "../../../icons/ArrowRight";
+import DateFromNow from "../../../components/DateFromNow";
+import CurrencyUnitValue from "../../../components/CurrencyUnitValue";
+import CounterValue from "../../../components/CounterValue";
+import IlluRewards from "../../../icons/images/Rewards";
+import ProgressCircle from "../../../components/ProgressCircle";
+import InfoModal from "../../../modals/Info";
+import ClaimRewards from "../../../icons/ClaimReward";
+import AccountDelegationInfo from "../../../components/AccountDelegationInfo";
+import AccountSectionLabel from "../../../components/AccountSectionLabel";
+
+type Props = {
+ account: Account;
+ parentAccount?: Account;
+};
+
+const Delegation = ({ account, parentAccount }: Props) => {
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+ const navigation = useNavigation();
+ const [infoRewardsModal, setRewardsInfoModal] = useState();
+
+ const superRepresentatives = useTronSuperRepresentatives();
+ const lastVotedDate = useMemo(() => getLastVotedDate(account), [account]);
+
+ const infoRewardsModalData = useMemo(
+ () => [
+ {
+ Icon: () => ,
+ title: ,
+ description: ,
+ },
+ ],
+ [colors.darkBlue],
+ );
+
+ const lastDate = lastVotedDate ? (
+
+ ) : null;
+
+ const currency = getAccountCurrency(account);
+ const unit = getAccountUnit(account);
+ const explorerView = getDefaultExplorerView(account.currency);
+ const accountId = account.id;
+ const parentId = parentAccount && parentAccount.id;
+
+ const { spendableBalance, tronResources } = account;
+
+ const canFreeze =
+ spendableBalance && spendableBalance.gt(MIN_TRANSACTION_AMOUNT);
+
+ const { votes, tronPower, unwithdrawnReward } = tronResources || {};
+
+ const formattedUnwidthDrawnReward = BigNumber(unwithdrawnReward || 0);
+
+ const formattedVotes = formatVotes(votes, superRepresentatives);
+
+ const totalVotesUsed = votes.reduce(
+ (sum, { voteCount }) => sum + voteCount,
+ 0,
+ );
+
+ const openRewardsInfoModal = useCallback(() => setRewardsInfoModal(true), [
+ setRewardsInfoModal,
+ ]);
+
+ const closeRewardsInfoModal = useCallback(() => setRewardsInfoModal(false), [
+ setRewardsInfoModal,
+ ]);
+
+ const bridge = getAccountBridge(account, undefined);
+
+ const { transaction, status } = useBridgeTransaction(() => {
+ const t = bridge.createTransaction(account);
+
+ const transaction = bridge.updateTransaction(t, {
+ mode: "claimReward",
+ });
+
+ return { account, transaction };
+ });
+
+ const claimRewards = useCallback(() => {
+ navigation.navigate(NavigatorName.ClaimRewards, {
+ screen: ScreenName.ClaimRewardsSelectDevice,
+ params: {
+ accountId,
+ parentId,
+ transaction,
+ status,
+ },
+ });
+ }, [accountId, navigation, parentId, transaction, status]);
+
+ const onDelegateFreeze = useCallback(() => {
+ navigation.navigate(NavigatorName.Freeze, {
+ screen: ScreenName.FreezeInfo,
+ params: {
+ accountId,
+ parentId,
+ },
+ });
+ }, [accountId, navigation, parentId]);
+
+ const onManageVotes = useCallback(() => {
+ navigation.navigate(NavigatorName.TronVoteFlow, {
+ screen: ScreenName.VoteCast,
+ params: {
+ accountId,
+ parentId,
+ },
+ });
+ }, [navigation, accountId, parentId]);
+
+ const onDelegate = useCallback(() => {
+ const screenName = lastVotedDate
+ ? ScreenName.VoteSelectValidator
+ : ScreenName.VoteStarted;
+ navigation.navigate(NavigatorName.TronVoteFlow, {
+ screen: screenName,
+ params: {
+ accountId,
+ parentId,
+ },
+ });
+ }, [lastVotedDate, navigation, accountId, parentId]);
+
+ const hasRewards = BigNumber(unwithdrawnReward).gt(0);
+ const nextRewardDate = getNextRewardDate(account);
+
+ const canClaimRewards = hasRewards && !nextRewardDate;
+
+ const percentVotesUsed = totalVotesUsed / tronPower;
+
+ return (
+
+ {(hasRewards || (tronPower > 0 && formattedVotes.length > 0)) && (
+ <>
+ }
+ onPress={openRewardsInfoModal}
+ />
+
+
+
+
+
+
+ {currency && (
+
+ )}
+
+
+
+
+ >
+ )}
+ {tronPower > 0 ? (
+ formattedVotes.length > 0 ? (
+ <>
+
+
+
+ {formattedVotes.map(
+ ({ validator, address, voteCount, isSR }, index) => (
+
+ ),
+ )}
+
+ {percentVotesUsed < 1 && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : (
+ canFreeze && (
+ }
+ description={t("tron.voting.delegationEarn", {
+ name: account.currency.name,
+ })}
+ infoUrl={urls.tronStaking}
+ infoTitle={t("tron.voting.howItWorks")}
+ disabled={!canFreeze}
+ onPress={onDelegateFreeze}
+ ctaTitle={t("account.delegation.info.cta")}
+ />
+ )
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ root: {
+ padding: 16,
+ },
+ container: {
+ padding: 16,
+ borderRadius: 4,
+ flexDirection: "column",
+ alignItems: "stretch",
+ },
+ noPadding: {
+ padding: 0,
+ },
+ rewardSection: {
+ paddingVertical: 16,
+ borderRadius: 4,
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: 16,
+ },
+ illustration: { alignSelf: "center", marginBottom: 16 },
+ collectButton: {
+ flexBasis: "auto",
+ flexGrow: 0.5,
+ },
+ labelSection: {
+ flex: 1,
+ flexDirection: "column",
+ alignItems: "flex-start",
+ justifyContent: "center",
+ },
+ warn: {
+ flexDirection: "row",
+ padding: 8,
+
+ borderRadius: 4,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ warnSection: {
+ flexDirection: "column",
+ flex: 1,
+ marginHorizontal: 6,
+ justifyContent: "center",
+ alignItems: "flex-start",
+ },
+ warnTitle: {
+ fontSize: 14,
+ },
+ warnText: {
+ marginLeft: 0,
+ fontSize: 13,
+ },
+ cta: {
+ flex: 1,
+ flexGrow: 0.5,
+ },
+ title: {
+ fontSize: 18,
+ lineHeight: 22,
+ textAlign: "center",
+ paddingVertical: 4,
+ },
+ subtitle: {
+ fontSize: 16,
+ lineHeight: 18,
+ textAlign: "left",
+ },
+});
+
+export default function Votes({ account, parentAccount }: Props) {
+ if (!account || !account.tronResources) return null;
+
+ return ;
+}
diff --git a/src/helpers/getCurrencyColor.tsx b/src/helpers/getCurrencyColor.tsx
new file mode 100644
index 0000000000..43f89a9276
--- /dev/null
+++ b/src/helpers/getCurrencyColor.tsx
@@ -0,0 +1,14 @@
+import { useMemo } from "react";
+import { getCurrencyColor as commonGetCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+import { Currency } from "@ledgerhq/live-common/lib/types/currencies";
+
+import { ensureContrast } from "../colors";
+
+export const getCurrencyColor = (currency: Currency, bg?: string) => {
+ const currencyColor = commonGetCurrencyColor(currency);
+
+ return bg ? ensureContrast(currencyColor, bg) : currencyColor;
+};
+
+export const useCurrencyColor = (currency: Currency, bg?: string) =>
+ useMemo(() => getCurrencyColor(currency, bg), [currency, bg]);
diff --git a/src/icons/AppTree.tsx b/src/icons/AppTree.tsx
new file mode 100644
index 0000000000..24ca74b061
--- /dev/null
+++ b/src/icons/AppTree.tsx
@@ -0,0 +1,84 @@
+// @flow
+import React from "react";
+import Svg, { Path, G, Image } from "react-native-svg";
+import manager from "@ledgerhq/live-common/lib/manager";
+import AppIcon from "../screens/Manager/AppsList/AppIcon";
+
+import { Flex } from "@ledgerhq/native-ui";
+
+type Props = {
+ size: number;
+ color: string;
+ icon: string;
+ app: any;
+};
+
+export default function AppTree({ size = 150, color, icon, app }: Props) {
+ const uri = manager.getIconUrl(icon);
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/icons/Carousel/Academy.tsx b/src/icons/Carousel/Academy.tsx
new file mode 100644
index 0000000000..646839187b
--- /dev/null
+++ b/src/icons/Carousel/Academy.tsx
@@ -0,0 +1,40 @@
+import React from "react"
+import Svg, {
+ SvgProps,
+ Path,
+ Defs,
+ Pattern,
+ Use,
+ Image,
+} from "react-native-svg"
+
+const SvgComponent = (props: SvgProps) => (
+
+)
+
+export default SvgComponent
diff --git a/src/icons/Carousel/BuyCrypto.tsx b/src/icons/Carousel/BuyCrypto.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/icons/Ledger.tsx b/src/icons/Ledger.tsx
new file mode 100644
index 0000000000..c794e62ad5
--- /dev/null
+++ b/src/icons/Ledger.tsx
@@ -0,0 +1,20 @@
+import * as React from "react";
+import { SvgProps } from "react-native-svg";
+import { StyledPath, StyledSvg } from "./StyledSvg";
+
+type Props = SvgProps & { size?: number; color?: string };
+
+const SvgComponent = ({
+ color = "neutral.c00",
+ size = 27,
+ ...props
+}: Props) => (
+
+
+
+);
+
+export default SvgComponent;
diff --git a/src/icons/NoAppsInstalled.tsx b/src/icons/NoAppsInstalled.tsx
new file mode 100644
index 0000000000..a7e16929b1
--- /dev/null
+++ b/src/icons/NoAppsInstalled.tsx
@@ -0,0 +1,40 @@
+import * as React from "react"
+import Svg, {
+ SvgProps,
+ Path,
+ Defs,
+ Pattern,
+ Use,
+ Image,
+} from "react-native-svg"
+
+const SvgComponent = (props: SvgProps) => (
+
+)
+
+export default SvgComponent;
diff --git a/src/icons/NoResultsFound.tsx b/src/icons/NoResultsFound.tsx
new file mode 100644
index 0000000000..fe93af0b09
--- /dev/null
+++ b/src/icons/NoResultsFound.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+import Illustration from "../images/illustration/Illustration";
+
+const dark = require("../images/illustration/Dark/_051.png");
+const light = require("../images/illustration/Light/_051.png");
+
+export default function NoResultsFound(props: any) {
+ return (
+
+ );
+}
diff --git a/src/icons/OperationStatusIcon/index.tsx b/src/icons/OperationStatusIcon/index.tsx
new file mode 100644
index 0000000000..f029ef44f9
--- /dev/null
+++ b/src/icons/OperationStatusIcon/index.tsx
@@ -0,0 +1,82 @@
+import React from "react";
+import { OperationType } from "@ledgerhq/live-common/lib/types";
+import { Icons, BoxedIcon } from "@ledgerhq/native-ui";
+import {
+ DEFAULT_BOX_SIZE,
+ DEFAULT_ICON_SIZE,
+ DEFAULT_BADGE_SIZE,
+} from "@ledgerhq/native-ui/components/Icon/BoxedIcon";
+
+const iconsComponent = {
+ OUT: Icons.ArrowTopMedium,
+ IN: Icons.ArrowBottomMedium,
+ DELEGATE: Icons.HandshakeMedium,
+ REDELEGATE: Icons.DelegateMedium,
+ UNDELEGATE: Icons.UndelegateMedium,
+ REVEAL: Icons.EyeMedium,
+ CREATE: Icons.PlusMedium,
+ NONE: Icons.ArrowFromBottomMedium,
+ FREEZE: Icons.FreezeMedium,
+ UNFREEZE: Icons.UnfreezeMedium,
+ VOTE: Icons.VoteMedium,
+ REWARD: Icons.StarMedium,
+ FEES: Icons.FeesMedium,
+ OPT_IN: Icons.PlusMedium,
+ OPT_OUT: Icons.TrashMedium,
+ CLOSE_ACCOUNT: Icons.TrashMedium,
+ REDEEM: Icons.MinusMedium,
+ SUPPLY: Icons.ArrowRightMedium,
+ APPROVE: Icons.PlusMedium,
+ BOND: Icons.LinkMedium,
+ UNBOND: Icons.LinkNoneMedium,
+ WITHDRAW_UNBONDED: Icons.CoinsMedium,
+ SLASH: Icons.TrashMedium,
+ NOMINATE: Icons.VoteMedium,
+ CHILL: Icons.VoteMedium,
+ REWARD_PAYOUT: Icons.ClaimRewardsMedium,
+ SET_CONTROLLER: Icons.ArrowFromBottomMedium,
+ NFT_IN: undefined, // TODO: get an icon from design team
+ NFT_OUT: undefined, // TODO: get an icon from design team
+};
+
+export default ({
+ type,
+ confirmed,
+ failed,
+ Badge,
+ size = 24,
+}: {
+ size?: number;
+ type: OperationType;
+ confirmed?: boolean;
+ Badge?: React.ComponentType<{ size: number }>;
+ failed?: boolean;
+}) => {
+ const Icon = iconsComponent[type] || iconsComponent.NONE;
+ const BadgeIcon =
+ Badge ||
+ (failed
+ ? Icons.CircledCrossSolidMedium
+ : confirmed
+ ? undefined
+ : Icons.CircledCrossSolidMedium);
+ const borderColor = failed ? "error.c40" : "neutral.c40";
+ const iconColor = failed
+ ? "error.c100"
+ : confirmed
+ ? "neutral.c100"
+ : "neutral.c50";
+ const badgeColor = failed ? "error.c100" : "neutral.c70";
+ return (
+
+ );
+};
diff --git a/src/icons/Planet.js b/src/icons/Planet.js
new file mode 100644
index 0000000000..29a0190a5c
--- /dev/null
+++ b/src/icons/Planet.js
@@ -0,0 +1,37 @@
+// @flow
+import React from "react";
+import Svg, { Path, G } from "react-native-svg";
+
+type Props = {
+ size: number,
+ color: string,
+};
+
+export default function Planet({ size = 16, color }: Props) {
+ return (
+
+ );
+}
diff --git a/src/icons/StyledSvg.tsx b/src/icons/StyledSvg.tsx
new file mode 100644
index 0000000000..bed6ac3de1
--- /dev/null
+++ b/src/icons/StyledSvg.tsx
@@ -0,0 +1,18 @@
+import * as React from "react";
+import Svg, { Path } from "react-native-svg";
+import { system } from "styled-system";
+import styled from "styled-components/native";
+
+const fillSystem = system({
+ fill: {
+ property: "fill",
+ scale: "colors",
+ },
+});
+
+export const StyledSvg = styled(Svg).attrs(props => ({
+ ...fillSystem(props),
+}))``;
+export const StyledPath = styled(Path).attrs(props => ({
+ ...fillSystem(props),
+}))``;
diff --git a/src/images/devices/NanoS.tsx b/src/images/devices/NanoS.tsx
new file mode 100644
index 0000000000..a055a47ec6
--- /dev/null
+++ b/src/images/devices/NanoS.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import Svg, { Path } from "react-native-svg";
+
+const NanoS = ({ color }: { color: string }) => (
+
+);
+
+export default NanoS;
diff --git a/src/images/devices/NanoX.tsx b/src/images/devices/NanoX.tsx
new file mode 100644
index 0000000000..f02ee2820c
--- /dev/null
+++ b/src/images/devices/NanoX.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import Svg, { Path } from "react-native-svg";
+
+const NanoX = ({ color }: { color: string }) => (
+
+);
+
+export default NanoX;
diff --git a/src/images/devices/NanoXFolded.tsx b/src/images/devices/NanoXFolded.tsx
new file mode 100644
index 0000000000..c884c3c44a
--- /dev/null
+++ b/src/images/devices/NanoXFolded.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { StyledPath, StyledSvg } from "../../icons/StyledSvg";
+
+const NanoXFolded = ({
+ size = 64,
+ color = "neutral.c100",
+}: {
+ size?: number;
+ color?: string;
+}) => (
+
+
+
+
+
+);
+
+export default NanoXFolded;
diff --git a/src/images/illustration/Academy.dark.png b/src/images/illustration/Academy.dark.png
new file mode 100644
index 0000000000..b9bcb51635
Binary files /dev/null and b/src/images/illustration/Academy.dark.png differ
diff --git a/src/images/illustration/Academy.light.png b/src/images/illustration/Academy.light.png
new file mode 100644
index 0000000000..7710793d6a
Binary files /dev/null and b/src/images/illustration/Academy.light.png differ
diff --git a/src/images/illustration/BuyCrypto.dark.png b/src/images/illustration/BuyCrypto.dark.png
new file mode 100644
index 0000000000..56a8cfa39c
Binary files /dev/null and b/src/images/illustration/BuyCrypto.dark.png differ
diff --git a/src/images/illustration/BuyCrypto.light.png b/src/images/illustration/BuyCrypto.light.png
new file mode 100644
index 0000000000..2750e01da4
Binary files /dev/null and b/src/images/illustration/BuyCrypto.light.png differ
diff --git a/src/images/illustration/Dark/_000_PLACEHOLDER.png b/src/images/illustration/Dark/_000_PLACEHOLDER.png
new file mode 100644
index 0000000000..a60f1cf641
Binary files /dev/null and b/src/images/illustration/Dark/_000_PLACEHOLDER.png differ
diff --git a/src/images/illustration/Dark/_001.png b/src/images/illustration/Dark/_001.png
new file mode 100644
index 0000000000..e2127dabf2
Binary files /dev/null and b/src/images/illustration/Dark/_001.png differ
diff --git a/src/images/illustration/Dark/_002.png b/src/images/illustration/Dark/_002.png
new file mode 100644
index 0000000000..979eed78ea
Binary files /dev/null and b/src/images/illustration/Dark/_002.png differ
diff --git a/src/images/illustration/Dark/_003.png b/src/images/illustration/Dark/_003.png
new file mode 100644
index 0000000000..45662725cc
Binary files /dev/null and b/src/images/illustration/Dark/_003.png differ
diff --git a/src/images/illustration/Dark/_004.png b/src/images/illustration/Dark/_004.png
new file mode 100644
index 0000000000..0f703b23f7
Binary files /dev/null and b/src/images/illustration/Dark/_004.png differ
diff --git a/src/images/illustration/Dark/_005.png b/src/images/illustration/Dark/_005.png
new file mode 100644
index 0000000000..cfc17a3c80
Binary files /dev/null and b/src/images/illustration/Dark/_005.png differ
diff --git a/src/images/illustration/Dark/_006.png b/src/images/illustration/Dark/_006.png
new file mode 100644
index 0000000000..cb2e287ce8
Binary files /dev/null and b/src/images/illustration/Dark/_006.png differ
diff --git a/src/images/illustration/Dark/_007.png b/src/images/illustration/Dark/_007.png
new file mode 100644
index 0000000000..d86772943a
Binary files /dev/null and b/src/images/illustration/Dark/_007.png differ
diff --git a/src/images/illustration/Dark/_008.png b/src/images/illustration/Dark/_008.png
new file mode 100644
index 0000000000..1e821565be
Binary files /dev/null and b/src/images/illustration/Dark/_008.png differ
diff --git a/src/images/illustration/Dark/_009.png b/src/images/illustration/Dark/_009.png
new file mode 100644
index 0000000000..c6c05ab3a9
Binary files /dev/null and b/src/images/illustration/Dark/_009.png differ
diff --git a/src/images/illustration/Dark/_010.png b/src/images/illustration/Dark/_010.png
new file mode 100644
index 0000000000..781a165d12
Binary files /dev/null and b/src/images/illustration/Dark/_010.png differ
diff --git a/src/images/illustration/Dark/_011.png b/src/images/illustration/Dark/_011.png
new file mode 100644
index 0000000000..20566fff3b
Binary files /dev/null and b/src/images/illustration/Dark/_011.png differ
diff --git a/src/images/illustration/Dark/_012.png b/src/images/illustration/Dark/_012.png
new file mode 100644
index 0000000000..05a027e2b5
Binary files /dev/null and b/src/images/illustration/Dark/_012.png differ
diff --git a/src/images/illustration/Dark/_013.png b/src/images/illustration/Dark/_013.png
new file mode 100644
index 0000000000..7f4078629f
Binary files /dev/null and b/src/images/illustration/Dark/_013.png differ
diff --git a/src/images/illustration/Dark/_014.png b/src/images/illustration/Dark/_014.png
new file mode 100644
index 0000000000..af62901b76
Binary files /dev/null and b/src/images/illustration/Dark/_014.png differ
diff --git a/src/images/illustration/Dark/_015.png b/src/images/illustration/Dark/_015.png
new file mode 100644
index 0000000000..f5bbf88a59
Binary files /dev/null and b/src/images/illustration/Dark/_015.png differ
diff --git a/src/images/illustration/Dark/_016.png b/src/images/illustration/Dark/_016.png
new file mode 100644
index 0000000000..ab0139690e
Binary files /dev/null and b/src/images/illustration/Dark/_016.png differ
diff --git a/src/images/illustration/Dark/_017.png b/src/images/illustration/Dark/_017.png
new file mode 100644
index 0000000000..8ed5c1a66b
Binary files /dev/null and b/src/images/illustration/Dark/_017.png differ
diff --git a/src/images/illustration/Dark/_018.png b/src/images/illustration/Dark/_018.png
new file mode 100644
index 0000000000..27e9d2a020
Binary files /dev/null and b/src/images/illustration/Dark/_018.png differ
diff --git a/src/images/illustration/Dark/_019.png b/src/images/illustration/Dark/_019.png
new file mode 100644
index 0000000000..69f189775f
Binary files /dev/null and b/src/images/illustration/Dark/_019.png differ
diff --git a/src/images/illustration/Dark/_020.png b/src/images/illustration/Dark/_020.png
new file mode 100644
index 0000000000..6250ec06d3
Binary files /dev/null and b/src/images/illustration/Dark/_020.png differ
diff --git a/src/images/illustration/Dark/_021.png b/src/images/illustration/Dark/_021.png
new file mode 100644
index 0000000000..a0e00611a5
Binary files /dev/null and b/src/images/illustration/Dark/_021.png differ
diff --git a/src/images/illustration/Dark/_022.png b/src/images/illustration/Dark/_022.png
new file mode 100644
index 0000000000..0d3b5aa1a1
Binary files /dev/null and b/src/images/illustration/Dark/_022.png differ
diff --git a/src/images/illustration/Dark/_023.png b/src/images/illustration/Dark/_023.png
new file mode 100644
index 0000000000..26e8921727
Binary files /dev/null and b/src/images/illustration/Dark/_023.png differ
diff --git a/src/images/illustration/Dark/_024.png b/src/images/illustration/Dark/_024.png
new file mode 100644
index 0000000000..364e073088
Binary files /dev/null and b/src/images/illustration/Dark/_024.png differ
diff --git a/src/images/illustration/Dark/_025.png b/src/images/illustration/Dark/_025.png
new file mode 100644
index 0000000000..ffed0516cc
Binary files /dev/null and b/src/images/illustration/Dark/_025.png differ
diff --git a/src/images/illustration/Dark/_026.png b/src/images/illustration/Dark/_026.png
new file mode 100644
index 0000000000..d4fdcb41f8
Binary files /dev/null and b/src/images/illustration/Dark/_026.png differ
diff --git a/src/images/illustration/Dark/_027.png b/src/images/illustration/Dark/_027.png
new file mode 100644
index 0000000000..09611719b7
Binary files /dev/null and b/src/images/illustration/Dark/_027.png differ
diff --git a/src/images/illustration/Dark/_028.png b/src/images/illustration/Dark/_028.png
new file mode 100644
index 0000000000..4645193f3b
Binary files /dev/null and b/src/images/illustration/Dark/_028.png differ
diff --git a/src/images/illustration/Dark/_029.png b/src/images/illustration/Dark/_029.png
new file mode 100644
index 0000000000..8023ee5cd9
Binary files /dev/null and b/src/images/illustration/Dark/_029.png differ
diff --git a/src/images/illustration/Dark/_030.png b/src/images/illustration/Dark/_030.png
new file mode 100644
index 0000000000..bef8d208b4
Binary files /dev/null and b/src/images/illustration/Dark/_030.png differ
diff --git a/src/images/illustration/Dark/_031.png b/src/images/illustration/Dark/_031.png
new file mode 100644
index 0000000000..c075d4f371
Binary files /dev/null and b/src/images/illustration/Dark/_031.png differ
diff --git a/src/images/illustration/Dark/_032.png b/src/images/illustration/Dark/_032.png
new file mode 100644
index 0000000000..b2804c9528
Binary files /dev/null and b/src/images/illustration/Dark/_032.png differ
diff --git a/src/images/illustration/Dark/_033.png b/src/images/illustration/Dark/_033.png
new file mode 100644
index 0000000000..7ffae3ec48
Binary files /dev/null and b/src/images/illustration/Dark/_033.png differ
diff --git a/src/images/illustration/Dark/_034.png b/src/images/illustration/Dark/_034.png
new file mode 100644
index 0000000000..3b2c42d684
Binary files /dev/null and b/src/images/illustration/Dark/_034.png differ
diff --git a/src/images/illustration/Dark/_035.png b/src/images/illustration/Dark/_035.png
new file mode 100644
index 0000000000..1aecd90c2a
Binary files /dev/null and b/src/images/illustration/Dark/_035.png differ
diff --git a/src/images/illustration/Dark/_036.png b/src/images/illustration/Dark/_036.png
new file mode 100644
index 0000000000..ee520f30fc
Binary files /dev/null and b/src/images/illustration/Dark/_036.png differ
diff --git a/src/images/illustration/Dark/_037.png b/src/images/illustration/Dark/_037.png
new file mode 100644
index 0000000000..6d0374c04a
Binary files /dev/null and b/src/images/illustration/Dark/_037.png differ
diff --git a/src/images/illustration/Dark/_038.png b/src/images/illustration/Dark/_038.png
new file mode 100644
index 0000000000..d330cc18d6
Binary files /dev/null and b/src/images/illustration/Dark/_038.png differ
diff --git a/src/images/illustration/Dark/_039.png b/src/images/illustration/Dark/_039.png
new file mode 100644
index 0000000000..ca60cf0638
Binary files /dev/null and b/src/images/illustration/Dark/_039.png differ
diff --git a/src/images/illustration/Dark/_040.png b/src/images/illustration/Dark/_040.png
new file mode 100644
index 0000000000..67c5b3f11a
Binary files /dev/null and b/src/images/illustration/Dark/_040.png differ
diff --git a/src/images/illustration/Dark/_041.png b/src/images/illustration/Dark/_041.png
new file mode 100644
index 0000000000..0fdd0d6a5b
Binary files /dev/null and b/src/images/illustration/Dark/_041.png differ
diff --git a/src/images/illustration/Dark/_042.png b/src/images/illustration/Dark/_042.png
new file mode 100644
index 0000000000..69b210b29e
Binary files /dev/null and b/src/images/illustration/Dark/_042.png differ
diff --git a/src/images/illustration/Dark/_043.png b/src/images/illustration/Dark/_043.png
new file mode 100644
index 0000000000..72051979d0
Binary files /dev/null and b/src/images/illustration/Dark/_043.png differ
diff --git a/src/images/illustration/Dark/_044.png b/src/images/illustration/Dark/_044.png
new file mode 100644
index 0000000000..270d3d531f
Binary files /dev/null and b/src/images/illustration/Dark/_044.png differ
diff --git a/src/images/illustration/Dark/_045.png b/src/images/illustration/Dark/_045.png
new file mode 100644
index 0000000000..57d08e2c54
Binary files /dev/null and b/src/images/illustration/Dark/_045.png differ
diff --git a/src/images/illustration/Dark/_047.png b/src/images/illustration/Dark/_047.png
new file mode 100644
index 0000000000..0fd10da3a2
Binary files /dev/null and b/src/images/illustration/Dark/_047.png differ
diff --git a/src/images/illustration/Dark/_048.png b/src/images/illustration/Dark/_048.png
new file mode 100644
index 0000000000..22f248d80e
Binary files /dev/null and b/src/images/illustration/Dark/_048.png differ
diff --git a/src/images/illustration/Dark/_049.png b/src/images/illustration/Dark/_049.png
new file mode 100644
index 0000000000..45c7e53a71
Binary files /dev/null and b/src/images/illustration/Dark/_049.png differ
diff --git a/src/images/illustration/Dark/_050.png b/src/images/illustration/Dark/_050.png
new file mode 100644
index 0000000000..f86258e882
Binary files /dev/null and b/src/images/illustration/Dark/_050.png differ
diff --git a/src/images/illustration/Dark/_051.png b/src/images/illustration/Dark/_051.png
new file mode 100644
index 0000000000..7a3df9e563
Binary files /dev/null and b/src/images/illustration/Dark/_051.png differ
diff --git a/src/images/illustration/Dark/_052.png b/src/images/illustration/Dark/_052.png
new file mode 100644
index 0000000000..b1011f74af
Binary files /dev/null and b/src/images/illustration/Dark/_052.png differ
diff --git a/src/images/illustration/Dark/_053.png b/src/images/illustration/Dark/_053.png
new file mode 100644
index 0000000000..b370f944a8
Binary files /dev/null and b/src/images/illustration/Dark/_053.png differ
diff --git a/src/images/illustration/Dark/_054.png b/src/images/illustration/Dark/_054.png
new file mode 100644
index 0000000000..4fdf6bb9eb
Binary files /dev/null and b/src/images/illustration/Dark/_054.png differ
diff --git a/src/images/illustration/Dark/_055.png b/src/images/illustration/Dark/_055.png
new file mode 100644
index 0000000000..dbbf4fbd21
Binary files /dev/null and b/src/images/illustration/Dark/_055.png differ
diff --git a/src/images/illustration/Dark/_056.png b/src/images/illustration/Dark/_056.png
new file mode 100644
index 0000000000..7031686c58
Binary files /dev/null and b/src/images/illustration/Dark/_056.png differ
diff --git a/src/images/illustration/Dark/_057.png b/src/images/illustration/Dark/_057.png
new file mode 100644
index 0000000000..fc2cb95b6a
Binary files /dev/null and b/src/images/illustration/Dark/_057.png differ
diff --git a/src/images/illustration/Dark/_058.png b/src/images/illustration/Dark/_058.png
new file mode 100644
index 0000000000..85b5b6df35
Binary files /dev/null and b/src/images/illustration/Dark/_058.png differ
diff --git a/src/images/illustration/Dark/_059.png b/src/images/illustration/Dark/_059.png
new file mode 100644
index 0000000000..962c1d172a
Binary files /dev/null and b/src/images/illustration/Dark/_059.png differ
diff --git a/src/images/illustration/Dark/_060.png b/src/images/illustration/Dark/_060.png
new file mode 100644
index 0000000000..6c544217ba
Binary files /dev/null and b/src/images/illustration/Dark/_060.png differ
diff --git a/src/images/illustration/Dark/_061.png b/src/images/illustration/Dark/_061.png
new file mode 100644
index 0000000000..75ce5b422e
Binary files /dev/null and b/src/images/illustration/Dark/_061.png differ
diff --git a/src/images/illustration/Dark/_062.png b/src/images/illustration/Dark/_062.png
new file mode 100644
index 0000000000..2c6c446f26
Binary files /dev/null and b/src/images/illustration/Dark/_062.png differ
diff --git a/src/images/illustration/Dark/_063.png b/src/images/illustration/Dark/_063.png
new file mode 100644
index 0000000000..6df7154ef8
Binary files /dev/null and b/src/images/illustration/Dark/_063.png differ
diff --git a/src/images/illustration/Dark/_064.png b/src/images/illustration/Dark/_064.png
new file mode 100644
index 0000000000..b8eb561206
Binary files /dev/null and b/src/images/illustration/Dark/_064.png differ
diff --git a/src/images/illustration/Dark/_065.png b/src/images/illustration/Dark/_065.png
new file mode 100644
index 0000000000..a3c7624454
Binary files /dev/null and b/src/images/illustration/Dark/_065.png differ
diff --git a/src/images/illustration/Dark/_066.png b/src/images/illustration/Dark/_066.png
new file mode 100644
index 0000000000..abe53b84c2
Binary files /dev/null and b/src/images/illustration/Dark/_066.png differ
diff --git a/src/images/illustration/Dark/_067.png b/src/images/illustration/Dark/_067.png
new file mode 100644
index 0000000000..653bebcd71
Binary files /dev/null and b/src/images/illustration/Dark/_067.png differ
diff --git a/src/images/illustration/Dark/_068.png b/src/images/illustration/Dark/_068.png
new file mode 100644
index 0000000000..600e3c462b
Binary files /dev/null and b/src/images/illustration/Dark/_068.png differ
diff --git a/src/images/illustration/Dark/_069.png b/src/images/illustration/Dark/_069.png
new file mode 100644
index 0000000000..80da154e8d
Binary files /dev/null and b/src/images/illustration/Dark/_069.png differ
diff --git a/src/images/illustration/Dark/_070.png b/src/images/illustration/Dark/_070.png
new file mode 100644
index 0000000000..0385df2ff1
Binary files /dev/null and b/src/images/illustration/Dark/_070.png differ
diff --git a/src/images/illustration/Dark/_071.png b/src/images/illustration/Dark/_071.png
new file mode 100644
index 0000000000..df37c53d39
Binary files /dev/null and b/src/images/illustration/Dark/_071.png differ
diff --git a/src/images/illustration/Dark/_072.png b/src/images/illustration/Dark/_072.png
new file mode 100644
index 0000000000..76b1e2fc91
Binary files /dev/null and b/src/images/illustration/Dark/_072.png differ
diff --git a/src/images/illustration/Dark/_073.png b/src/images/illustration/Dark/_073.png
new file mode 100644
index 0000000000..6b64465f46
Binary files /dev/null and b/src/images/illustration/Dark/_073.png differ
diff --git a/src/images/illustration/Dark/_074.png b/src/images/illustration/Dark/_074.png
new file mode 100644
index 0000000000..fd666b5e8d
Binary files /dev/null and b/src/images/illustration/Dark/_074.png differ
diff --git a/src/images/illustration/Dark/_075.png b/src/images/illustration/Dark/_075.png
new file mode 100644
index 0000000000..6e60413749
Binary files /dev/null and b/src/images/illustration/Dark/_075.png differ
diff --git a/src/images/illustration/Dark/_076.png b/src/images/illustration/Dark/_076.png
new file mode 100644
index 0000000000..ae8e77d384
Binary files /dev/null and b/src/images/illustration/Dark/_076.png differ
diff --git a/src/images/illustration/Dark/_077.png b/src/images/illustration/Dark/_077.png
new file mode 100644
index 0000000000..2f1c904762
Binary files /dev/null and b/src/images/illustration/Dark/_077.png differ
diff --git a/src/images/illustration/Dark/_078.png b/src/images/illustration/Dark/_078.png
new file mode 100644
index 0000000000..610e132f5e
Binary files /dev/null and b/src/images/illustration/Dark/_078.png differ
diff --git a/src/images/illustration/Dark/_079.png b/src/images/illustration/Dark/_079.png
new file mode 100644
index 0000000000..90051a9f36
Binary files /dev/null and b/src/images/illustration/Dark/_079.png differ
diff --git a/src/images/illustration/Dark/_Apps.png b/src/images/illustration/Dark/_Apps.png
new file mode 100644
index 0000000000..9c154d327d
Binary files /dev/null and b/src/images/illustration/Dark/_Apps.png differ
diff --git a/src/images/illustration/Dark/_Earn.png b/src/images/illustration/Dark/_Earn.png
new file mode 100644
index 0000000000..8e1f547ec4
Binary files /dev/null and b/src/images/illustration/Dark/_Earn.png differ
diff --git a/src/images/illustration/Dark/_Learn.png b/src/images/illustration/Dark/_Learn.png
new file mode 100644
index 0000000000..f625da3f0d
Binary files /dev/null and b/src/images/illustration/Dark/_Learn.png differ
diff --git a/src/images/illustration/Earn.dark.png b/src/images/illustration/Earn.dark.png
new file mode 100644
index 0000000000..2161349d7e
Binary files /dev/null and b/src/images/illustration/Earn.dark.png differ
diff --git a/src/images/illustration/Earn.light.png b/src/images/illustration/Earn.light.png
new file mode 100644
index 0000000000..3214fcefff
Binary files /dev/null and b/src/images/illustration/Earn.light.png differ
diff --git a/src/images/illustration/FamilyPack.dark.png b/src/images/illustration/FamilyPack.dark.png
new file mode 100644
index 0000000000..fa07e41660
Binary files /dev/null and b/src/images/illustration/FamilyPack.dark.png differ
diff --git a/src/images/illustration/FamilyPack.light.png b/src/images/illustration/FamilyPack.light.png
new file mode 100644
index 0000000000..c0fe70c88e
Binary files /dev/null and b/src/images/illustration/FamilyPack.light.png differ
diff --git a/src/images/illustration/Illustration.tsx b/src/images/illustration/Illustration.tsx
new file mode 100644
index 0000000000..f42acd0541
--- /dev/null
+++ b/src/images/illustration/Illustration.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { Image, ImageProps, ImageSourcePropType } from "react-native";
+import { useTheme } from "styled-components/native";
+
+export const Illustration = ({
+ lightSource,
+ darkSource,
+ size,
+ ...othersProps
+}: Omit & {
+ lightSource: ImageSourcePropType;
+ darkSource: ImageSourcePropType;
+ size: number;
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+ );
+};
+
+export default Illustration;
diff --git a/src/images/illustration/Light/_000_PLACEHOLDER.png b/src/images/illustration/Light/_000_PLACEHOLDER.png
new file mode 100644
index 0000000000..5e917dd574
Binary files /dev/null and b/src/images/illustration/Light/_000_PLACEHOLDER.png differ
diff --git a/src/images/illustration/Light/_001.png b/src/images/illustration/Light/_001.png
new file mode 100644
index 0000000000..0373ce2b9e
Binary files /dev/null and b/src/images/illustration/Light/_001.png differ
diff --git a/src/images/illustration/Light/_002.png b/src/images/illustration/Light/_002.png
new file mode 100644
index 0000000000..327bea4f4f
Binary files /dev/null and b/src/images/illustration/Light/_002.png differ
diff --git a/src/images/illustration/Light/_003.png b/src/images/illustration/Light/_003.png
new file mode 100644
index 0000000000..37c751cd00
Binary files /dev/null and b/src/images/illustration/Light/_003.png differ
diff --git a/src/images/illustration/Light/_004.png b/src/images/illustration/Light/_004.png
new file mode 100644
index 0000000000..d36964738d
Binary files /dev/null and b/src/images/illustration/Light/_004.png differ
diff --git a/src/images/illustration/Light/_005.png b/src/images/illustration/Light/_005.png
new file mode 100644
index 0000000000..70fd065e8b
Binary files /dev/null and b/src/images/illustration/Light/_005.png differ
diff --git a/src/images/illustration/Light/_006.png b/src/images/illustration/Light/_006.png
new file mode 100644
index 0000000000..14141e9e78
Binary files /dev/null and b/src/images/illustration/Light/_006.png differ
diff --git a/src/images/illustration/Light/_007.png b/src/images/illustration/Light/_007.png
new file mode 100644
index 0000000000..8c878d137e
Binary files /dev/null and b/src/images/illustration/Light/_007.png differ
diff --git a/src/images/illustration/Light/_008.png b/src/images/illustration/Light/_008.png
new file mode 100644
index 0000000000..645ce70e6c
Binary files /dev/null and b/src/images/illustration/Light/_008.png differ
diff --git a/src/images/illustration/Light/_009.png b/src/images/illustration/Light/_009.png
new file mode 100644
index 0000000000..e356f7de9d
Binary files /dev/null and b/src/images/illustration/Light/_009.png differ
diff --git a/src/images/illustration/Light/_010.png b/src/images/illustration/Light/_010.png
new file mode 100644
index 0000000000..69dd380edd
Binary files /dev/null and b/src/images/illustration/Light/_010.png differ
diff --git a/src/images/illustration/Light/_011.png b/src/images/illustration/Light/_011.png
new file mode 100644
index 0000000000..63ba69a5f4
Binary files /dev/null and b/src/images/illustration/Light/_011.png differ
diff --git a/src/images/illustration/Light/_012.png b/src/images/illustration/Light/_012.png
new file mode 100644
index 0000000000..537859c286
Binary files /dev/null and b/src/images/illustration/Light/_012.png differ
diff --git a/src/images/illustration/Light/_013.png b/src/images/illustration/Light/_013.png
new file mode 100644
index 0000000000..b406e017ca
Binary files /dev/null and b/src/images/illustration/Light/_013.png differ
diff --git a/src/images/illustration/Light/_014.png b/src/images/illustration/Light/_014.png
new file mode 100644
index 0000000000..6dbbe9ab9e
Binary files /dev/null and b/src/images/illustration/Light/_014.png differ
diff --git a/src/images/illustration/Light/_015.png b/src/images/illustration/Light/_015.png
new file mode 100644
index 0000000000..95c5ce901d
Binary files /dev/null and b/src/images/illustration/Light/_015.png differ
diff --git a/src/images/illustration/Light/_016.png b/src/images/illustration/Light/_016.png
new file mode 100644
index 0000000000..cb364ea1cf
Binary files /dev/null and b/src/images/illustration/Light/_016.png differ
diff --git a/src/images/illustration/Light/_017.png b/src/images/illustration/Light/_017.png
new file mode 100644
index 0000000000..69fa91bf3c
Binary files /dev/null and b/src/images/illustration/Light/_017.png differ
diff --git a/src/images/illustration/Light/_018.png b/src/images/illustration/Light/_018.png
new file mode 100644
index 0000000000..ce30689f4c
Binary files /dev/null and b/src/images/illustration/Light/_018.png differ
diff --git a/src/images/illustration/Light/_019.png b/src/images/illustration/Light/_019.png
new file mode 100644
index 0000000000..f753df2623
Binary files /dev/null and b/src/images/illustration/Light/_019.png differ
diff --git a/src/images/illustration/Light/_020.png b/src/images/illustration/Light/_020.png
new file mode 100644
index 0000000000..fc95ba0ff1
Binary files /dev/null and b/src/images/illustration/Light/_020.png differ
diff --git a/src/images/illustration/Light/_021.png b/src/images/illustration/Light/_021.png
new file mode 100644
index 0000000000..079a4db2e9
Binary files /dev/null and b/src/images/illustration/Light/_021.png differ
diff --git a/src/images/illustration/Light/_022.png b/src/images/illustration/Light/_022.png
new file mode 100644
index 0000000000..602397da70
Binary files /dev/null and b/src/images/illustration/Light/_022.png differ
diff --git a/src/images/illustration/Light/_023.png b/src/images/illustration/Light/_023.png
new file mode 100644
index 0000000000..c778a64722
Binary files /dev/null and b/src/images/illustration/Light/_023.png differ
diff --git a/src/images/illustration/Light/_024.png b/src/images/illustration/Light/_024.png
new file mode 100644
index 0000000000..e34e89e94c
Binary files /dev/null and b/src/images/illustration/Light/_024.png differ
diff --git a/src/images/illustration/Light/_025.png b/src/images/illustration/Light/_025.png
new file mode 100644
index 0000000000..5900ca33cc
Binary files /dev/null and b/src/images/illustration/Light/_025.png differ
diff --git a/src/images/illustration/Light/_026.png b/src/images/illustration/Light/_026.png
new file mode 100644
index 0000000000..54d889e382
Binary files /dev/null and b/src/images/illustration/Light/_026.png differ
diff --git a/src/images/illustration/Light/_027.png b/src/images/illustration/Light/_027.png
new file mode 100644
index 0000000000..9838fd1007
Binary files /dev/null and b/src/images/illustration/Light/_027.png differ
diff --git a/src/images/illustration/Light/_028.png b/src/images/illustration/Light/_028.png
new file mode 100644
index 0000000000..04d4c4e5ad
Binary files /dev/null and b/src/images/illustration/Light/_028.png differ
diff --git a/src/images/illustration/Light/_029.png b/src/images/illustration/Light/_029.png
new file mode 100644
index 0000000000..6d17aded07
Binary files /dev/null and b/src/images/illustration/Light/_029.png differ
diff --git a/src/images/illustration/Light/_030.png b/src/images/illustration/Light/_030.png
new file mode 100644
index 0000000000..af61806c0d
Binary files /dev/null and b/src/images/illustration/Light/_030.png differ
diff --git a/src/images/illustration/Light/_031.png b/src/images/illustration/Light/_031.png
new file mode 100644
index 0000000000..3482c4d7d1
Binary files /dev/null and b/src/images/illustration/Light/_031.png differ
diff --git a/src/images/illustration/Light/_032.png b/src/images/illustration/Light/_032.png
new file mode 100644
index 0000000000..f0f84a8b4d
Binary files /dev/null and b/src/images/illustration/Light/_032.png differ
diff --git a/src/images/illustration/Light/_033.png b/src/images/illustration/Light/_033.png
new file mode 100644
index 0000000000..fdf8236ed4
Binary files /dev/null and b/src/images/illustration/Light/_033.png differ
diff --git a/src/images/illustration/Light/_034.png b/src/images/illustration/Light/_034.png
new file mode 100644
index 0000000000..462cd57d40
Binary files /dev/null and b/src/images/illustration/Light/_034.png differ
diff --git a/src/images/illustration/Light/_035.png b/src/images/illustration/Light/_035.png
new file mode 100644
index 0000000000..de3dec635a
Binary files /dev/null and b/src/images/illustration/Light/_035.png differ
diff --git a/src/images/illustration/Light/_036.png b/src/images/illustration/Light/_036.png
new file mode 100644
index 0000000000..fc005d77e5
Binary files /dev/null and b/src/images/illustration/Light/_036.png differ
diff --git a/src/images/illustration/Light/_037.png b/src/images/illustration/Light/_037.png
new file mode 100644
index 0000000000..3d04a971dd
Binary files /dev/null and b/src/images/illustration/Light/_037.png differ
diff --git a/src/images/illustration/Light/_038.png b/src/images/illustration/Light/_038.png
new file mode 100644
index 0000000000..1e095cf88b
Binary files /dev/null and b/src/images/illustration/Light/_038.png differ
diff --git a/src/images/illustration/Light/_039.png b/src/images/illustration/Light/_039.png
new file mode 100644
index 0000000000..0bc34666b6
Binary files /dev/null and b/src/images/illustration/Light/_039.png differ
diff --git a/src/images/illustration/Light/_040.png b/src/images/illustration/Light/_040.png
new file mode 100644
index 0000000000..181d3055a2
Binary files /dev/null and b/src/images/illustration/Light/_040.png differ
diff --git a/src/images/illustration/Light/_041.png b/src/images/illustration/Light/_041.png
new file mode 100644
index 0000000000..4489559701
Binary files /dev/null and b/src/images/illustration/Light/_041.png differ
diff --git a/src/images/illustration/Light/_042.png b/src/images/illustration/Light/_042.png
new file mode 100644
index 0000000000..9ef7bd1e00
Binary files /dev/null and b/src/images/illustration/Light/_042.png differ
diff --git a/src/images/illustration/Light/_043.png b/src/images/illustration/Light/_043.png
new file mode 100644
index 0000000000..9289e744e4
Binary files /dev/null and b/src/images/illustration/Light/_043.png differ
diff --git a/src/images/illustration/Light/_044.png b/src/images/illustration/Light/_044.png
new file mode 100644
index 0000000000..e87e6ee422
Binary files /dev/null and b/src/images/illustration/Light/_044.png differ
diff --git a/src/images/illustration/Light/_045.png b/src/images/illustration/Light/_045.png
new file mode 100644
index 0000000000..feca310b15
Binary files /dev/null and b/src/images/illustration/Light/_045.png differ
diff --git a/src/images/illustration/Light/_047.png b/src/images/illustration/Light/_047.png
new file mode 100644
index 0000000000..788ac2b826
Binary files /dev/null and b/src/images/illustration/Light/_047.png differ
diff --git a/src/images/illustration/Light/_048.png b/src/images/illustration/Light/_048.png
new file mode 100644
index 0000000000..b2ef9f821a
Binary files /dev/null and b/src/images/illustration/Light/_048.png differ
diff --git a/src/images/illustration/Light/_049.png b/src/images/illustration/Light/_049.png
new file mode 100644
index 0000000000..6933629dc6
Binary files /dev/null and b/src/images/illustration/Light/_049.png differ
diff --git a/src/images/illustration/Light/_050.png b/src/images/illustration/Light/_050.png
new file mode 100644
index 0000000000..667b450cda
Binary files /dev/null and b/src/images/illustration/Light/_050.png differ
diff --git a/src/images/illustration/Light/_051.png b/src/images/illustration/Light/_051.png
new file mode 100644
index 0000000000..e7d0cb5a16
Binary files /dev/null and b/src/images/illustration/Light/_051.png differ
diff --git a/src/images/illustration/Light/_052.png b/src/images/illustration/Light/_052.png
new file mode 100644
index 0000000000..6adc62ecb6
Binary files /dev/null and b/src/images/illustration/Light/_052.png differ
diff --git a/src/images/illustration/Light/_053.png b/src/images/illustration/Light/_053.png
new file mode 100644
index 0000000000..1ef7a67144
Binary files /dev/null and b/src/images/illustration/Light/_053.png differ
diff --git a/src/images/illustration/Light/_054.png b/src/images/illustration/Light/_054.png
new file mode 100644
index 0000000000..e13336b094
Binary files /dev/null and b/src/images/illustration/Light/_054.png differ
diff --git a/src/images/illustration/Light/_055.png b/src/images/illustration/Light/_055.png
new file mode 100644
index 0000000000..8a46a6b023
Binary files /dev/null and b/src/images/illustration/Light/_055.png differ
diff --git a/src/images/illustration/Light/_056.png b/src/images/illustration/Light/_056.png
new file mode 100644
index 0000000000..5cdb717ecf
Binary files /dev/null and b/src/images/illustration/Light/_056.png differ
diff --git a/src/images/illustration/Light/_057.png b/src/images/illustration/Light/_057.png
new file mode 100644
index 0000000000..fbbc23a43f
Binary files /dev/null and b/src/images/illustration/Light/_057.png differ
diff --git a/src/images/illustration/Light/_058.png b/src/images/illustration/Light/_058.png
new file mode 100644
index 0000000000..e0d312a062
Binary files /dev/null and b/src/images/illustration/Light/_058.png differ
diff --git a/src/images/illustration/Light/_059.png b/src/images/illustration/Light/_059.png
new file mode 100644
index 0000000000..e740b8394f
Binary files /dev/null and b/src/images/illustration/Light/_059.png differ
diff --git a/src/images/illustration/Light/_060.png b/src/images/illustration/Light/_060.png
new file mode 100644
index 0000000000..2f3fbee534
Binary files /dev/null and b/src/images/illustration/Light/_060.png differ
diff --git a/src/images/illustration/Light/_061.png b/src/images/illustration/Light/_061.png
new file mode 100644
index 0000000000..8fb494d95c
Binary files /dev/null and b/src/images/illustration/Light/_061.png differ
diff --git a/src/images/illustration/Light/_062.png b/src/images/illustration/Light/_062.png
new file mode 100644
index 0000000000..4d66ea72a7
Binary files /dev/null and b/src/images/illustration/Light/_062.png differ
diff --git a/src/images/illustration/Light/_063.png b/src/images/illustration/Light/_063.png
new file mode 100644
index 0000000000..d11d2068e3
Binary files /dev/null and b/src/images/illustration/Light/_063.png differ
diff --git a/src/images/illustration/Light/_064.png b/src/images/illustration/Light/_064.png
new file mode 100644
index 0000000000..a59dcb7cf8
Binary files /dev/null and b/src/images/illustration/Light/_064.png differ
diff --git a/src/images/illustration/Light/_065.png b/src/images/illustration/Light/_065.png
new file mode 100644
index 0000000000..dbd5ba353b
Binary files /dev/null and b/src/images/illustration/Light/_065.png differ
diff --git a/src/images/illustration/Light/_066.png b/src/images/illustration/Light/_066.png
new file mode 100644
index 0000000000..77cc393022
Binary files /dev/null and b/src/images/illustration/Light/_066.png differ
diff --git a/src/images/illustration/Light/_067.png b/src/images/illustration/Light/_067.png
new file mode 100644
index 0000000000..7958ca9790
Binary files /dev/null and b/src/images/illustration/Light/_067.png differ
diff --git a/src/images/illustration/Light/_068.png b/src/images/illustration/Light/_068.png
new file mode 100644
index 0000000000..82921aba6a
Binary files /dev/null and b/src/images/illustration/Light/_068.png differ
diff --git a/src/images/illustration/Light/_069.png b/src/images/illustration/Light/_069.png
new file mode 100644
index 0000000000..65f485f3ea
Binary files /dev/null and b/src/images/illustration/Light/_069.png differ
diff --git a/src/images/illustration/Light/_070.png b/src/images/illustration/Light/_070.png
new file mode 100644
index 0000000000..0ee252b9ad
Binary files /dev/null and b/src/images/illustration/Light/_070.png differ
diff --git a/src/images/illustration/Light/_071.png b/src/images/illustration/Light/_071.png
new file mode 100644
index 0000000000..ca74266a38
Binary files /dev/null and b/src/images/illustration/Light/_071.png differ
diff --git a/src/images/illustration/Light/_072.png b/src/images/illustration/Light/_072.png
new file mode 100644
index 0000000000..5e80109902
Binary files /dev/null and b/src/images/illustration/Light/_072.png differ
diff --git a/src/images/illustration/Light/_073.png b/src/images/illustration/Light/_073.png
new file mode 100644
index 0000000000..a93221d8a2
Binary files /dev/null and b/src/images/illustration/Light/_073.png differ
diff --git a/src/images/illustration/Light/_074.png b/src/images/illustration/Light/_074.png
new file mode 100644
index 0000000000..403598ce1b
Binary files /dev/null and b/src/images/illustration/Light/_074.png differ
diff --git a/src/images/illustration/Light/_075.png b/src/images/illustration/Light/_075.png
new file mode 100644
index 0000000000..30cfcac212
Binary files /dev/null and b/src/images/illustration/Light/_075.png differ
diff --git a/src/images/illustration/Light/_076.png b/src/images/illustration/Light/_076.png
new file mode 100644
index 0000000000..c70620b907
Binary files /dev/null and b/src/images/illustration/Light/_076.png differ
diff --git a/src/images/illustration/Light/_077.png b/src/images/illustration/Light/_077.png
new file mode 100644
index 0000000000..f6e6e474ca
Binary files /dev/null and b/src/images/illustration/Light/_077.png differ
diff --git a/src/images/illustration/Light/_078.png b/src/images/illustration/Light/_078.png
new file mode 100644
index 0000000000..dfd11e6711
Binary files /dev/null and b/src/images/illustration/Light/_078.png differ
diff --git a/src/images/illustration/Light/_079.png b/src/images/illustration/Light/_079.png
new file mode 100644
index 0000000000..0da82dddfc
Binary files /dev/null and b/src/images/illustration/Light/_079.png differ
diff --git a/src/images/illustration/Light/_Apps.png b/src/images/illustration/Light/_Apps.png
new file mode 100644
index 0000000000..9c154d327d
Binary files /dev/null and b/src/images/illustration/Light/_Apps.png differ
diff --git a/src/images/illustration/Light/_Earn.png b/src/images/illustration/Light/_Earn.png
new file mode 100644
index 0000000000..b208314e4e
Binary files /dev/null and b/src/images/illustration/Light/_Earn.png differ
diff --git a/src/images/illustration/Light/_Learn.png b/src/images/illustration/Light/_Learn.png
new file mode 100644
index 0000000000..f625da3f0d
Binary files /dev/null and b/src/images/illustration/Light/_Learn.png differ
diff --git a/src/images/illustration/PileOfBitcoin.dark.png b/src/images/illustration/PileOfBitcoin.dark.png
new file mode 100644
index 0000000000..9b478bd407
Binary files /dev/null and b/src/images/illustration/PileOfBitcoin.dark.png differ
diff --git a/src/images/illustration/PileOfBitcoin.light.png b/src/images/illustration/PileOfBitcoin.light.png
new file mode 100644
index 0000000000..ae3786fee7
Binary files /dev/null and b/src/images/illustration/PileOfBitcoin.light.png differ
diff --git a/src/images/illustration/Swap.dark.png b/src/images/illustration/Swap.dark.png
new file mode 100644
index 0000000000..5d61d3f5c9
Binary files /dev/null and b/src/images/illustration/Swap.dark.png differ
diff --git a/src/images/illustration/Swap.light.png b/src/images/illustration/Swap.light.png
new file mode 100644
index 0000000000..78d870cbba
Binary files /dev/null and b/src/images/illustration/Swap.light.png differ
diff --git a/src/index.js b/src/index.js
index ff031aa92c..b6606c2cf2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -90,6 +90,7 @@ import { FirebaseFeatureFlagsProvider } from "./components/FirebaseFeatureFlags"
import StyleProvider from "./StyleProvider";
// $FlowFixMe
import MarketDataProvider from "./screens/Market/MarketDataProviderWrapper";
+import AdjustProvider from "./components/AdjustProvider";
const themes = {
light: lightTheme,
@@ -249,7 +250,7 @@ const linkingOptions = {
[ScreenName.Accounts]: "account",
},
},
- [NavigatorName.Platform]: {
+ [NavigatorName.Discover]: {
screens: {
/**
* @params ?platform: string
@@ -317,7 +318,8 @@ const linkingOptions = {
[NavigatorName.Exchange]: {
initialRouteName: "buy",
screens: {
- [ScreenName.Coinify]: "coinify",
+ [ScreenName.ExchangeBuy]: "buy",
+ [ScreenName.Coinify]: "buy/coinify",
},
},
/**
@@ -325,12 +327,12 @@ const linkingOptions = {
*/
[NavigatorName.Swap]: "swap",
[NavigatorName.Settings]: {
- initialRouteName: [ScreenName.Settings],
+ initialRouteName: [ScreenName.SettingsScreen],
screens: {
/**
* ie: "ledgerlive://settings/experimental" -> will redirect to the experimental settings panel
*/
- [ScreenName.Settings]: "settings",
+ [ScreenName.SettingsScreen]: "settings",
[ScreenName.GeneralSettings]: "settings/general",
[ScreenName.AccountsSettings]: "settings/accounts",
[ScreenName.AboutSettings]: "settings/about",
@@ -456,10 +458,13 @@ export default class Root extends Component<
<>
+
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 2e290ad7c1..c2e19ec90c 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -124,6 +124,7 @@
},
"android": {
"nanoS": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoSP": "Please make sure your {{productName}} is unlocked and connected by cable.",
"nanoX": "Please make sure your {{productName}} is unlocked and is connected by cable or has Bluetooth enabled."
}
}
@@ -136,6 +137,7 @@
},
"android": {
"nanoS": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoSP": "Please make sure your {{productName}} is unlocked and connected by cable.",
"nanoX": "Please make sure your {{productName}} is unlocked and is connected by cable or has Bluetooth enabled."
}
}
@@ -350,23 +352,24 @@
},
"ManagerNotEnoughSpace": {
"title": "Not enough storage left",
- "info": "Please uninstall some apps to free up space for the {{app}} app.",
- "description": "Uninstalling apps has no impact on your assets."
+ "info": "Please uninstall some apps to free up space for the {{app}} app. Your crypto assets stay safe when uninstalling apps.",
+ "description": "Uninstalling apps has no impact on your assets.",
+ "continue": "Got it!"
},
"ManagerQuitPage": {
"install": {
- "title": "App installation in progress",
- "description": "Quitting the Manager will terminate installation.",
+ "title": "Quit and cancel installations ?",
+ "description": "Quitting will cancel the app installations in progress.",
"stay": "Continue installation"
},
"uninstall": {
- "title": "App uninstall in progress",
- "description": "Quitting the Manager will terminate uninstallation.",
+ "title": "Quit and cancel uninstallations ?",
+ "description": "Quitting will cancel the app uninstallations in progress.",
"stay": "Continue uninstallation"
},
"update": {
- "title": "App update in progress",
- "description": "Quitting the Manager will terminate update.",
+ "title": "Quit and cancel updates ?",
+ "description": "Quitting will cancel the app updates in progress.",
"stay": "Continue update"
},
"quit": "Quit Manager"
@@ -388,7 +391,7 @@
"description": "Please make sure the account has enough funds."
},
"NotEnoughGas": {
- "title": "Parent account balance insufficient for network fees",
+ "title": "Insufficient ETH for network fee",
"description": "Please send some ETH to your account to pay for ERC20 token transactions."
},
"NotEnoughBalanceToDelegate": {
@@ -693,6 +696,7 @@
}
},
"reset": {
+ "title": "Uninstall then reinstall Ledger Live",
"description": "Please uninstall then reinstall the app on your phone to delete Ledger Live data, including accounts and settings.",
"button": "Reset",
"warning": "Resetting Ledger Live will erase your swap transaction history for all your accounts."
@@ -713,9 +717,13 @@
"title": "Valentine’s day",
"description": "Reduced fees on Buy and Sell"
},
+ "tour": {
+ "title": "Take a tour",
+ "description": "Explore the ledger live app and learn how to buy, grow and secure your assets"
+ },
"academy": {
"title": "Ledger Academy",
- "description": "Everything you need to\nknow about crypto"
+ "description": "Everything you need to know about Blockchain"
},
"stakeCosmos": {
"title": "Stake COSMOS",
@@ -727,11 +735,11 @@
},
"buyCrypto": {
"title": "Buy Crypto",
- "description": "Buy crypto on\nLedger Live"
+ "description": "Buy crypto on Ledger Live"
},
"swap": {
"title": "Swap crypto",
- "description": "Securely exchange one\ncrypto for another"
+ "description": "Securely exchange one crypto for another"
},
"algorand": {
"title": "Algorand",
@@ -755,13 +763,89 @@
}
}
},
+ "buyDevice": {
+ "0": {
+ "title": "Gain financial freedom",
+ "desc": "Take charge and unlock the freedom to manage your crypto on your own."
+ },
+ "1": {
+ "title": "Access a world of Defi",
+ "desc": "Securely interact with our trusted DeFi applications directly from the app."
+ },
+ "2": {
+ "title": "Your one stop shop for crypto",
+ "desc": "You can buy, sell and swap major cryptocurrencies from our partners safely with your Ledger."
+ },
+ "3": {
+ "title": "Peace of mind with certified security",
+ "desc": "Our products are the only hardware wallets certified for their security by national cyber security agencies."
+ },
+ "title": "GET YOUR LEDGER",
+ "desc": "Protect your cryptocurrencies with industry-leading security",
+ "cta": "Buy your Ledger now",
+ "footer": "I already have a device, set it up now"
+ },
+ "discover": {
+ "title": "Discover",
+ "desc": "Explore the world of web3 included in Ledger Live",
+ "link": "Tell me more",
+ "sections": {
+ "learn": {
+ "title": "Learn",
+ "desc": "Educate yourself about web3 and stay informed"
+ },
+ "ledgerApps": {
+ "title": "Apps",
+ "desc": "Explore Ledger’s integrated apps all in one place"
+ },
+ "earn": {
+ "title": "Earn",
+ "desc": "Earn passive income and grow your crypto assets"
+ }
+ },
+ "comingSoon": "Coming soon",
+ "mostPopular": "Most popular"
+ },
"onboarding": {
"stepWelcome": {
"title": "Welcome to Ledger",
"subtitle": "Safely manage your cryptos from your smartphone.",
"start": "Get started",
"noDevice": "No device?",
- "buy": "Buy a {{fullDeviceName}}"
+ "buy": "Buy a {{fullDeviceName}}",
+ "terms": "By tapping “Get Started” you consent and agree to our",
+ "termsLink": "Terms of Service",
+ "privacyLink": "Privacy Policy",
+ "and": "and"
+ },
+ "postWelcomeStep": {
+ "title": "It’s your choice",
+ "subtitle": "You can choose to set up your Ledger now or take a look around the ledger live app first.",
+ "noLedgerLink": "I don’t have a Ledger and I would like to buy one",
+ "setupLedger": {
+ "title": "Set up my Ledger",
+ "subtitle": "Secure your crypto now",
+ "label": "30 mins"
+ },
+ "discoverLedger": {
+ "title": "Discover Ledger Live",
+ "subtitle": "Explore the app"
+ }
+ },
+ "discoverLive": {
+ "0": {
+ "title": "One place for all your crypto needs",
+ "desc": "Monitor, buy, sell, swap, grow and manage your assets securely and get the best crypto insights"
+ },
+ "1": {
+ "title": "Not your keys, not your crypto",
+ "desc": "Securing your assets with a Hardware wallet is the best and only way to own and secure your crypto assets"
+ },
+ "2": {
+ "title": "Reclaim power over your money",
+ "desc": "The Ledger Nano X keeps your coins offline and protected. Combine it to Ledger Live app for maximum security and control over your crypto",
+ "cta": "Begin your Journey"
+ }
},
"stepLanguage": {
"title": "Select your language",
@@ -773,18 +857,18 @@
}
},
"stepSelectDevice": {
- "title": "What’s your device?",
+ "title": "Select your device",
"nanoS": "Nano S",
- "nanoSP": "Nano S Plus",
+ "nanoSP": "Nano S+",
"nanoX": "Nano X",
- "blue": "Blue"
+ "blue": "Blue",
+ "chooseDevice": "Choose your device"
},
"stepUseCase": {
"title": "Hello!",
"or": "Or",
"firstUse": {
- "title": "First time using this Nano?",
- "label": "First use",
+ "title": "First time using your Nano?",
"subTitle": "Set up a new Nano",
"desc": "Let’s start and set up your device!"
},
@@ -804,13 +888,15 @@
"label": "Restore device",
"subTitle": "Restore your recovery phrase on a new device",
"desc": "Use an existing recovery phrase to restore your private keys on a new Nano!"
- }
+ },
+ "recovery": "Already have a recovery phrase?"
},
"stepNewDevice": {
"0": {
"label": "Basics",
"title": "Access your crypto",
- "desc": "Your crypto assets are stored on the blockchain. You need a private key to access and manage them."
+ "desc": "Your crypto assets are stored on the blockchain. You need a private key to access and manage them.",
+ "action": "Don’t have a Nano? Discover the app"
},
"1": {
"label": "Basics",
@@ -820,19 +906,20 @@
"2": {
"label": "Basics",
"title": "Stay offline",
- "desc": "Your Nano works as a \"cold storage\" wallet. This means that it never exposes your private key online, even when using the app."
+ "desc": "Ledger Live allows you to buy, sell, manage, exchange and earn crypto while remaining protected. You will validate every crypto transaction with your Nano."
},
"3": {
"label": "Basics",
"title": "Validate transactions",
- "desc": "Ledger Live allows you to buy, sell, manage, exchange and earn crypto while remaining protected. You will validate every crypto transaction with your Nano."
+ "desc": "Your Nano works as a \"cold storage\" wallet. This means that it never exposes your private key online, even when using the app."
},
"4": {
"label": "Basics",
"title": "Let’s set up your Nano!",
"desc": "We'll start by setting up your Nano security."
},
- "cta": "Let’s do this!"
+ "cta": "Let’s do this!",
+ "title": "BASICS"
},
"stepSetupDevice": {
"start": {
@@ -848,7 +935,7 @@
"label": "Stay alone, and choose a safe and quiet environment."
}
},
- "cta": "OK, I’m ready!",
+ "cta": "Continue",
"warning": {
"title": "Please be careful",
"desc": "Make sure you follow the instructions on this app at every step of the process.",
@@ -873,7 +960,7 @@
}
},
"1": {
- "title": "Browse ",
+ "title": "Browse",
"label": "Learn how to interact with your device by reading the on-screen instructions."
},
"2": {
@@ -897,11 +984,13 @@
"bullets": {
"0": {
"title": "Choose PIN code",
- "label": "Press the left or right button to change digits. Press both buttons to validate a digit. Select <1>1> to confirm your PIN code. Select <2>2> to erase a digit."
+ "label": "Press the left or right button to change digits. Press both buttons to validate a digit. Select <1>1> to confirm your PIN code. Select <2>2> to erase a digit.",
+ "desc": "Press the left or right button to change digits. Press both buttons to validate a digit. Select to confirm your PIN code. Select to erase a digit."
},
"1": {
"title": "Confirm PIN code",
- "label": "Enter your PIN code again to confirm it."
+ "label": "Enter your PIN code again to confirm it.",
+ "desc": "Enter your PIN code again to confirm it."
}
},
"infoModal": {
@@ -967,7 +1056,7 @@
"label": "Grab a blank Recovery sheet, included with your Nano. Please reach out to Ledger Support if the Recovery sheet did not come blank."
},
"1": {
- "title": "Write down all words",
+ "title": "Repeat for all words!",
"label": "Write Word #1 displayed on your Nano in position 1 of your Recovery sheet. Then press right on your Nano to display Word #2 and write it down in position 2.",
"label_1": "Repeat the process for all words while carefully respecting the order and spelling. Press left on your Nano to check for any mistakes."
},
@@ -999,13 +1088,13 @@
"title": "Where should I keep my recovery phrase?",
"bullets": {
"0": {
- "label": "<1>NEVER1> enter it on a computer, phone or any other device. Don't take a picture of it."
+ "label": "NEVER enter it on a computer, phone or any other device. Don't take a picture of it."
},
"1": {
- "label": "<1>NEVER1> share your 24 words with anyone."
+ "label": "NEVER share your 24 words with anyone."
},
"2": {
- "label": "<1>ALWAYS1> store it in a secure place, out of sight."
+ "label": "ALWAYS store it in a secure place, out of sight."
},
"3": {
"label": "Ledger will never ask for your recovery phrase."
@@ -1247,7 +1336,7 @@
"wrong": "No problem, Ledger can send me a copy"
},
"modal": {
- "text": "Anyone who knows your recovery phrase can steal your crypto assets. \nIf you lose it, you must quickly transfer your crypto to a secure place.",
+ "text": "Anyone who knows your recovery phrase can steal your crypto assets. \\nIf you lose it, you must quickly transfer your crypto to a secure place.",
"cta": "Next question"
}
},
@@ -1268,7 +1357,9 @@
"failTitle": "You will soon become a pro...",
"failText": "Don’t worry, Ledger is here to guide you through your journey. You will soon feel extra comfortable about your crypto safety. Only one quick step left!",
"cta": "Next step"
- }
+ },
+ "nextQuestion": "Next question",
+ "finish": "Finish quiz"
},
"warning": {
"recoveryPhrase": {
@@ -1286,12 +1377,13 @@
}
},
"tabs": {
- "portfolio": "Portfolio",
+ "portfolio": "Wallet",
"accounts": "Accounts",
"transfer": "Transfer",
- "manager": "Manager",
+ "manager": "My Ledger",
"settings": "Settings",
- "platform": "Discover",
+ "platform": "Live Apps",
+ "discover": "Discover",
"nanoX": "Nano X",
"market": "Market",
"learn": "Learn"
@@ -1320,24 +1412,46 @@
"noAccountsTitle": "You don't have any accounts…",
"noAccountsDesc": "Please add accounts to your Portfolio.",
"buttons": {
- "import": "Add account",
+ "import": "Add asset",
+ "buy": "Buy",
"manager": "Install apps",
"managerSecondary": "Install apps on my device"
+ },
+ "addAccounts": {
+ "title": "Add assets",
+ "description": "You’re one step away from fully exploring Ledger Live and truly owning your money"
}
},
"noOpState": {
"title": "No operations yet?",
"desc": "Simply send crypto assets to your receiving address and wait for the app to sync."
+ },
+ "recommended": {
+ "title": "Recommended"
+ },
+ "topGainers": {
+ "title": "Top gainers (24H)",
+ "seeMarket": "See market"
}
},
"addAccountsModal": {
"ctaAdd": "Add accounts",
- "ctaImport": "Import Desktop accounts"
+ "ctaImport": "Import Desktop accounts",
+ "title": "Add Crypto",
+ "description": "You can choose to add crypto(s) directly with your Ledger, or import them from Ledger Live Desktop.",
+ "add": {
+ "title": "With your Ledger",
+ "description": "Create or import asset(s) with your Ledger"
+ },
+ "import": {
+ "title": "Import from desktop",
+ "description": "Import asset(s) from Ledger Live Desktop"
+ }
},
"byteSize": {
"bytes": "{{size}} bytes",
- "kbUnit": "{{size}} KB",
- "mbUnit": "{{size}} MB"
+ "kbUnit": "{{size}} Kb",
+ "mbUnit": "{{size}} Mb"
},
"numberCompactNotation": {
"d": "",
@@ -1560,12 +1674,12 @@
},
"import": {
"scan": {
- "title": "Scan LiveQR code",
+ "title": "Import from desktop",
"descTop": {
- "line1": "In Ledger Live Desktop, go to",
+ "line1": "In Ledger Live Desktop, go to:",
"line2": "Settings > Accounts > Export accounts > Export"
},
- "descBottom": "Please put the LiveQR code within the square."
+ "descBottom": "Open Ledger Live Desktop & Scan QR Code"
},
"result": {
"title": "Import accounts",
@@ -1644,7 +1758,9 @@
"assets": "asset",
"assets_plural": "assets",
"total": "Total balance:",
- "listAccount": "Account allocation ({{count}})"
+ "listAccount": "Account allocation ({{count}})",
+ "title": "Assets",
+ "moreAssets": "Add more"
},
"help": {
"gettingStarted": {
@@ -1696,7 +1812,8 @@
"themes": {
"system": "System",
"dark": "Dark",
- "light": "Light"
+ "light": "Light",
+ "dusk": "Dusk"
},
"counterValueDesc": "Choose the currency for balances and operations.",
"exchange": "Rate provider",
@@ -1850,6 +1967,9 @@
}
},
"transfer": {
+ "recipient": {
+ "input": "Enter address"
+ },
"send": {
"title": "Send"
},
@@ -1912,8 +2032,8 @@
"subtitle": "Your information is collected by LEDGER on behalf of and transferred to WYRE for KYC purposes. For more information, please check our Privacy Policy",
"pending": {
"cta": "Continue",
- "title": "Your information has been submitted for approval",
- "subtitle": "It usually takes only a few minutes before you can start swapping",
+ "title": "KYC submitted for approval",
+ "subtitle": "Your KYC has been submitted and is pending approval.",
"link": "Learn more about KYC"
},
"approved": {
@@ -1938,7 +2058,6 @@
"country": "Country",
"postalCode": "Zip Code",
"dateOfBirth": "Date of birth",
-
"firstNamePlaceholder": "Enter your first name",
"lastNamePlaceholder": "Enter your last name",
"street1Placeholder": "Eg. 13, Maple street",
@@ -1947,7 +2066,6 @@
"postalCodePlaceholder": "Enter your 5 digit Zip code",
"statePlaceholder": "Select your state",
"dateOfBirthPlaceholder": "YYYY-MM-DD",
-
"firstNameError": "Enter your first name to continue",
"lastNameError": "Enter your last name to continue",
"street1Error": "Enter your address",
@@ -1968,9 +2086,9 @@
"cta": "Close"
},
"pendingOperation": {
- "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your swapped {{targetCurrency}} assets",
+ "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your {{targetCurrency}}.",
"label": "Your Swap ID:",
- "title": "Pending operation",
+ "title": "Swap broadcast successfully ",
"disclaimer": "Take note of your Swap ID number in case you’d need assistance from {{provider}} support.",
"cta": "See details"
},
@@ -2508,7 +2626,8 @@
"voteFor": "Vote for",
"validateVotes": "Validate votes",
"votesRequired": "Votes required ",
- "allVotesUsed": "All votes used"
+ "allVotesUsed": "All votes used",
+ "removeVotes": "Remove votes"
},
"validation": {
"message": "Always verify that your device displays the address exactly as it was originally given to you.",
@@ -2560,17 +2679,17 @@
"createParentCurrencyAccount": "Add {{parrentCurrencyName}} account",
"erc20": {
"title": "Add token",
- "disclaimer": "{{tokenName}} is an ERC20 Token.\nYou can receive tokens directly in an Ethereum account.",
+ "disclaimer": "{{tokenName}} is an ERC20 Token.\\nYou can receive tokens directly in an Ethereum account.",
"learnMore": "Learn more about ERC20"
},
"trc10": {
"title": "Add Token",
- "disclaimer": "{{tokenName}} is a TRC10 Token.\nYou can receive tokens directly in a Tron account.",
+ "disclaimer": "{{tokenName}} is a TRC10 Token.\\nYou can receive tokens directly in a Tron account.",
"learnMore": "Learn more about TRC10"
},
"trc20": {
"title": "Add Token",
- "disclaimer": "{{tokenName}} is a TRC20 Token.\nYou can receive tokens directly in a Tron account.",
+ "disclaimer": "{{tokenName}} is a TRC20 Token.\\nYou can receive tokens directly in a Tron account.",
"learnMore": "Learn more about TRC20"
}
},
@@ -2653,7 +2772,8 @@
}
},
"genuineCheck": {
- "title": "Enable Ledger Manager on your {{productName}}"
+ "title": "Enable Ledger Manager on your {{productName}}",
+ "accept": "Please don't turn off your Nano X. Make sure you allow <1>Ledger Manager1>."
},
"genuineCheckPending": {
"title": "Checking if the device is genuine..."
@@ -2702,11 +2822,14 @@
},
"Pairing": {
"step1": "Validate if the code displayed on your phone exactly matches the one displayed on your {{productName}}.",
- "step2": "Validate on your {{productName}} by pressing both buttons together."
+ "step2": "Validate on your {{productName}} by pressing both buttons together.",
+ "title1": "Check matching codes",
+ "title2": "Validate"
},
"GenuineCheck": {
"title": "Device authentication check",
- "accept": "Please don't turn off your Nano X. Make sure you allow <1>Ledger Manager1>."
+ "accept": "Please don't turn off your Nano X. Make sure you allow <1>Ledger Manager1>.",
+ "info": "This step is meant to ensure that your Nano is authentical."
},
"ScanningHeader": {
"title": "Looking for devices",
@@ -2738,7 +2861,7 @@
"title": "Manager",
"connect": "Select your device",
"appsCatalog": "App catalog",
- "installedApps": "Installed apps",
+ "installedApps": "Apps installed",
"noAppNeededForToken": "Install {{appName}} app for {{tokenName}}",
"tokenAppDisclaimer": "{{tokenName}} is an {{tokenType}} token using the {{appName}} app. To manage {{tokenName}}, <1>install the {{appName}} app1> and send the tokens <3>to your {{appName}} account3>.",
"tokenAppDisclaimerInstalled": "{{tokenName}} is an {{tokenType}} token using the {{appName}} app. To manage {{tokenName}}, <1>open the {{appName}} app1> and send the tokens <3>to your {{appName}} account3>.",
@@ -2762,12 +2885,14 @@
"noAppsDescription": "Go to the App catalog to install apps",
"noResultsFound": "No results found",
"noResultsDesc": "Please verify the spelling and try again",
- "versionNew": "(NEW{{newVersion}})"
+ "versionNew": "(NEW{{newVersion}})",
+ "searchApps": "Search"
},
"uninstall": {
- "title": "Uninstall all",
- "subtitle": "Uninstall all apps?",
- "description": "Uninstalling apps has no impact on your crypto assets. You can reinstall apps in the App catalog."
+ "title": "Uninstall",
+ "subtitle": "Uninstall all apps",
+ "description": "Uninstalling apps has no impact on your crypto assets. You can reinstall apps in the App catalog.",
+ "uninstallAll": "Uninstall all"
},
"remove": {
"title": "Remove device",
@@ -2778,23 +2903,38 @@
"title": "Storage",
"used": "Used",
"genuine": "Your device is genuine",
- "appsInstalled": "<0>{{number}}0> app installed",
- "appsInstalled_plural": "<0>{{number}}0> apps installed",
- "storageAvailable": "available"
+ "appsInstalled": "<0>{{number}}0> app",
+ "appsInstalled_plural": "<0>{{number}}0> apps",
+ "storageAvailable": "available",
+ "appsToUpdate": "<0>{{number}}0> update",
+ "appsToUpdate_plural": "<0>{{number}}0> updates"
},
"installSuccess": {
- "title": "App installed successfully, you can now add your {{app}} accounts",
- "title_plural": "Apps installed successfully, you can now add your accounts",
+ "title": "App successfully installed",
+ "title_plural": "Apps successfully installed",
"notSupported": "App installed successfully, find out more about the installed apps on our website.",
"manageAccount": "Add accounts",
"learnMore": "Learn more",
- "later": "Do it later"
+ "later": "Do it later",
+ "description": "You can now add your {{app}} account"
},
"firmware": {
- "latest": "Firmware version {{version}} is available",
- "outdated": "Device firmware version too old to be updated. Please contact Ledger Support for a replacement.",
- "modalTitle": "Firmware update only available on Ledger Live Desktop",
- "modalDesc": "Please download Ledger Live on your computer to update the device firmware."
+ "latest": "Firmware update is available",
+ "outdated": "Device firmware version is too old to be updated. Please contact Ledger Support for a replacement.",
+ "modalTitle": "Firmware update only available on Desktop",
+ "modalDesc": "Please download Ledger Live on your computer to update the device firmware.",
+ "contactUs": "Contact us"
+ },
+ "myApps": "My apps",
+ "token": {
+ "title": "{{appName}} tokens",
+ "noAppNeeded": "{{tokenName}} is a token using the {{appName}} app. There is no app to install.",
+ "installApp": "{{tokenName}} is a token using the {{appName}} app. Install the {{appName}} app to be able to manage your tokens."
+ },
+ "update": {
+ "title": "Update",
+ "subtitle": "Updates available",
+ "updateAll": "Update"
}
},
"ManagerDevice": {
@@ -2810,12 +2950,13 @@
"button_plural": "Installing... {{progressPercentage}}%"
},
"done": {
- "title": "Successfully installed {{appName}} on your {{productName}}",
- "accounts": "Go to Accounts"
+ "title": "App Successfully installed",
+ "accounts": "Go to Accounts",
+ "description": "You can now add your {{app}} account"
},
"dependency": {
- "title": "{{dependency}} app is needed",
- "description_one": "The {{dependency}} app will also be installed because the {{app}} app uses it.",
+ "title": "{{dependency}} app is required",
+ "description_one": "The {{dependency}} app will also be installed because the {{app}} app needs it.",
"description_two": "Please press Continue to install the {{app}} and {{dependency}} apps."
},
"continueInstall": "Install apps"
@@ -2842,14 +2983,16 @@
"title": "Successfully uninstalled {{appName}} on your {{productName}}"
},
"dependency": {
- "title": "Uninstall {{app}} and related apps?",
+ "title": "Other apps need the {{app}} app",
"showAll": "Show apps to uninstall",
- "description": "Apps on your device that need the {{app}} app will be uninstalled too."
+ "description": "Apps on your device that need the {{app}} app will be uninstalled too.",
+ "description_one": "Some apps are dependent of the {{app}} app.",
+ "description_two": "Some apps are dependent of the {{app}} app.\\nApps on your device that are dependent on the {{app}} app will be uninstalled too."
},
"continueUninstall": "Uninstall {{app}} and other apps"
},
"filter": {
- "title": "Show",
+ "title": "Filter",
"all": "All",
"installed": "Apps",
"not_installed": "Not installed",
@@ -2858,11 +3001,13 @@
"apply": "Apply"
},
"sort": {
- "title": "Sort by",
+ "title": "Sort",
"default": "Default",
"name_asc": "Name A-Z",
"name_desc": "Name Z-A",
- "marketcap_desc": "Market cap"
+ "marketcap_desc": "Market cap",
+ "marketcap": "Marketcap",
+ "name": "Name"
}
},
"AuthenticityRow": {
@@ -2874,7 +3019,7 @@
},
"FirmwareVersionRow": {
"title": "Firmware version",
- "subtitle": "Firmware {{version}}"
+ "subtitle": "V {{version}}"
},
"FirmwareUpdateRow": {
"title": "Firmware version {{version}} available",
@@ -3742,12 +3887,13 @@
"memo": "Memo"
},
"Terms": {
- "title": "Terms of Use",
+ "title": "TERMS OF USE",
"read": "Read the Terms of Use",
"switchLabel": "I have read and agree with the <1>Terms of Service1>",
- "switchLabelFull": "I have read and accept the Terms of Service and Privacy Policy",
+ "switchLabelFull": "I have read and agree with the Privacy Policy.",
"cta": "Enter Ledger App",
- "service": "Terms of service"
+ "service": "Terms of service",
+ "subTitle": "Please take some time to review our Terms of service and Privacy Policy"
},
"exchange": {
"buy": {
@@ -3755,7 +3901,7 @@
"selectCurrency": "Select a currency",
"selectAccount": "Select an account",
"connectDevice": "Connect your device",
- "title": "Choose a provider to buy crypto",
+ "title": "Buy crypto via our partner",
"coinifyTitle": "Buy crypto via our partner",
"description": "Purchase crypto assets via Coinify and receive them directly in your Ledger account.",
"CTAButton": "Buy now",
@@ -3892,7 +4038,8 @@
"disclaimer": {
"title": "External Application",
"description": "You are about to be redirected to an application not operated by Ledger.",
- "legalAdvice": "This application is not operated by Ledger. Ledger is not responsible for any loss of funds or quality of service of such application.\n\nAlways make sure to carefully verify the information displayed on your device.",
+ "legalAdvice": "This application is not operated by Ledger. Ledger is not responsible for any loss of funds or quality of service of such application.\\n\\nAlways make sure to carefully verify the information displayed on your device.",
+ "legalAdviceShort": "Ledger is not responsible for any loss of funds. Always make sure to verify the information displayed on your device.",
"checkbox": "Do not remind me again.",
"CTA": "Continue"
},
@@ -3991,5 +4138,15 @@
"viewInExplorer": "View in explorer",
"txDetails": "Transaction details"
}
+ },
+ "ApplicationVersion": "v{{version}}",
+ "analytics": {
+ "title": "Analytics",
+ "allocation": {
+ "title": "Allocation"
+ },
+ "operations": {
+ "title": "Operations"
+ }
}
}
diff --git a/src/locales/en/v2.json b/src/locales/en/v2.json
new file mode 100644
index 0000000000..674ad58ee4
--- /dev/null
+++ b/src/locales/en/v2.json
@@ -0,0 +1,4053 @@
+{
+ "common": {
+ "cancel": "Cancel",
+ "apply": "Apply",
+ "seeAll": "See all",
+ "back": "Back",
+ "delete": "Delete",
+ "paste": "Paste",
+ "yes": "Yes",
+ "no": "No",
+ "gotit": "Got it",
+ "continue": "Continue",
+ "retry": "Retry",
+ "done": "Done",
+ "sortBy": "Sort by",
+ "signOut": "Sign out",
+ "search": "Search",
+ "contactUs": "Contact Ledger Support",
+ "device": "Device",
+ "cryptoAsset": "Crypto asset",
+ "skip": "Skip",
+ "noCryptoFound": "No crypto assets found",
+ "needHelp": "Do you need help?",
+ "edit": "Edit",
+ "editName": "Edit name",
+ "close": "Close",
+ "confirm": "Confirm",
+ "poweredBy": "Powered by ",
+ "received": "Received",
+ "sent": "Sent",
+ "or": "OR",
+ "rename": "Rename",
+ "learnMore": "Learn more",
+ "checkItOut": "Check it out",
+ "viewDetails": "View details",
+ "today": "Today",
+ "yesterday": "Yesterday",
+ "upToDate": "Up to date",
+ "transactionDate": "Transaction date",
+ "outdated": "Outdated",
+ "satPerByte": "sat/bytes",
+ "notAvailable": "Not available",
+ "import": "Import",
+ "bluetooth": "Bluetooth",
+ "usb": "USB",
+ "add": "Add",
+ "token": "Token",
+ "token_plural": "Tokens",
+ "subaccount": "Subaccount",
+ "subaccount_plural": "Subaccounts",
+ "forgetDevice": "Remove device",
+ "help": "Help",
+ "saveLogs": "Save logs",
+ "sync": {
+ "ago": "Synchronized {{time}}"
+ },
+ "update": "Update available",
+ "install": "Install",
+ "installed": "Installed",
+ "uninstall": "Uninstall",
+ "fromNow": {
+ "seconds": "in one second",
+ "seconds_plural": "in {{time}} seconds",
+ "minutes": "in one minute",
+ "minutes_plural": "in {{time}} minutes",
+ "hours": "in one hour",
+ "hours_plural": "in {{time}} hours",
+ "days": "in one day",
+ "days_plural": "in {{time}} days"
+ },
+ "timeAgo": {
+ "seconds": "one second ago",
+ "seconds_plural": "{{time}} seconds ago",
+ "minutes": "one minute ago",
+ "minutes_plural": "{{time}} minutes ago",
+ "hours": "one hour ago",
+ "hours_plural": "{{time}} hours ago",
+ "days": "yesterday",
+ "days_plural": "{{time}} days ago"
+ },
+ "seeMore": "See more",
+ "moreInfo": "More info",
+ "buyEth": "Buy Ethereum"
+ },
+ "errors": {
+ "countervaluesUnavailable": {
+ "title": "We're not able to provide a countervalue for this asset at the moment"
+ },
+ "AccountAwaitingSendPendingOperations": {
+ "title": "There is a pending operation for this account",
+ "description": "Please wait for the operation to go through."
+ },
+ "AccountNameRequired": {
+ "title": "An account name is needed",
+ "description": "Please enter an account name."
+ },
+ "AccountNeedResync": {
+ "title": "Please try again",
+ "description": "Account is outdated. A synchronisation is needed"
+ },
+ "AlgorandASANotOptInInRecipient": {
+ "title": "Recipient account has not opted in the selected ASA."
+ },
+ "BluetoothRequired": {
+ "title": "Sorry, Bluetooth is disabled",
+ "description": "Please enable Bluetooth in your phone settings. ({{state}} state)"
+ },
+ "BtcUnmatchedApp": {
+ "title": "That's the wrong app",
+ "description": "Please open the {{managerAppName}} app on your device."
+ },
+ "CantOpenDevice": {
+ "title": "Sorry, connection failed",
+ "description": "Your Ledger device must be unlocked and in range to use Bluetooth."
+ },
+ "CantScanQRCode": {
+ "title": "Could not scan this QR code: auto-verification not supported by this address."
+ },
+ "ConnectAppTimeout": {
+ "title": "Sorry, no device found",
+ "description": {
+ "ios": {
+ "nanoX": "Please make sure your {{productName}} is unlocked and has Bluetooth enabled."
+ },
+ "android": {
+ "nanoS": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoSP": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoX": "Please make sure your {{productName}} is unlocked and is connected by cable or has Bluetooth enabled."
+ }
+ }
+ },
+ "ConnectManagerTimeout": {
+ "title": "Sorry, Manager connection failed",
+ "description": {
+ "ios": {
+ "nanoX": "Please make sure your {{productName}} is unlocked and has Bluetooth enabled."
+ },
+ "android": {
+ "nanoS": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoSP": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoX": "Please make sure your {{productName}} is unlocked and is connected by cable or has Bluetooth enabled."
+ }
+ }
+ },
+ "ClaimRewardsFeesWarning": {
+ "title": "The rewards are smaller than the estimated fees to claim them.",
+ "description": ""
+ },
+ "CompoundLowerAllowanceOfActiveAccountError": {
+ "title": "You cannot reduce the amount approved while having an active deposit."
+ },
+ "CosmosBroadcastCodeInternal": {
+ "title": "Something went wrong (Error #1)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeTxDecode": {
+ "title": "Something went wrong (Error #2)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeInvalidSequence": {
+ "title": "Invalid sequence",
+ "description": "Please try again."
+ },
+ "CosmosBroadcastCodeUnauthorized": {
+ "title": "Unauthorized signature",
+ "description": "This account is not authorized to sign this transaction."
+ },
+ "CosmosBroadcastCodeInsufficientFunds": {
+ "title": "Insufficient funds",
+ "description": "Please make sure the account has enough funds."
+ },
+ "CosmosBroadcastCodeUnknownRequest": {
+ "title": "Something went wrong (Error #6)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeInvalidAddress": {
+ "title": "Invalid address",
+ "description": "Please check the address and try again."
+ },
+ "CosmosBroadcastCodeInvalidPubKey": {
+ "title": "Something went wrong (Error #8)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeUnknownAddress": {
+ "title": "Unknown address",
+ "description": "Please check the address and try again."
+ },
+ "CosmosBroadcastCodeInsufficientCoins": {
+ "title": "Insufficient funds",
+ "description": "Please increase the funds on the account."
+ },
+ "CosmosBroadcastCodeInvalidCoins": {
+ "title": "Something went wrong (Error #11)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeOutOfGas": {
+ "title": "Something went wrong (Error #12)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeMemoTooLarge": {
+ "title": "The Memo field is too long",
+ "description": "Please reduce the size of the memo text and try again."
+ },
+ "CosmosBroadcastCodeInsufficientFee": {
+ "title": "Something went wrong (Error #14)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeTooManySignatures": {
+ "title": "Something went wrong (Error #15)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeGasOverflow": {
+ "title": "Something went wrong (Error #16)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeNoSignatures": {
+ "title": "Something went wrong (Error #17)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "DeviceAppVerifyNotSupported": {
+ "title": "Open Manager to update this app",
+ "description": "The app verification is not supported."
+ },
+ "DeviceGenuineSocketEarlyClose": {
+ "title": "Sorry, try again (genuine-close)",
+ "description": null
+ },
+ "DeviceHalted": {
+ "title": "Please restart your Ledger device and retry",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "DeviceNotGenuine": {
+ "title": "Device possibly not genuine",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "DeviceNameInvalid": {
+ "title": "Please name the device without '{{invalidCharacters}}'."
+ },
+ "DeviceOnDashboardExpected": {
+ "title": "Device not on Dashboard",
+ "description": "Please return to the Dashboard on your device."
+ },
+ "DeviceNotOnboarded": {
+ "title": "Your device is not ready to use yet",
+ "description": "Set up your device before using it with Ledger Live."
+ },
+ "DeviceSocketFail": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again."
+ },
+ "DeviceSocketNoBulkStatus": {
+ "title": "The connection failed",
+ "description": "Please try again."
+ },
+ "DeviceSocketNoHandler": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again."
+ },
+ "DisconnectedDevice": {
+ "title": "Sorry, it looks like your device was disconnected",
+ "description": "Please reconnect and try again."
+ },
+ "DisconnectedDeviceDuringOperation": {
+ "title": "Sorry, it looks like your device was disconnected",
+ "description": "Please reconnect and try again."
+ },
+ "ETHAddressNonEIP": {
+ "title": "Auto-verification not available: carefully verify the address.",
+ "description": null
+ },
+ "EthAppNftNotSupported": {
+ "title": "Operation not available for this device",
+ "description": "Send NFT feature is only available for Nano X. Please visit our customer support platform to know how to send a NFT with a Nano S."
+ },
+ "Touch ID Error": {
+ "title": "Biometric authentication failed",
+ "description": "Please use your password or reset the app."
+ },
+ "Error": {
+ "title": "{{message}}",
+ "description": "Something went wrong. Please retry. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "FeeEstimationFailed": {
+ "title": "Sorry, fee estimation failed",
+ "description": "Try setting the fee manually (status: {{status}})."
+ },
+ "FeeNotLoaded": {
+ "title": "Could not load fee rates"
+ },
+ "FeeRequired": {
+ "title": "Fees are required"
+ },
+ "FirmwareOrAppUpdateRequired": {
+ "title": "Firmware or app update needed",
+ "description": "Please use the Manager to uninstall all apps then check if a firmware update is available before reinstalling them."
+ },
+ "LatestFirmwareVersionRequired": {
+ "title": "Device update required on desktop",
+ "description": "Please update your Nano X firmware on Ledger Live Desktop"
+ },
+ "GenuineCheckFailed": {
+ "title": "Device authentication failed",
+ "description": "Something went wrong. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "HardResetFail": {
+ "title": "Sorry, could not reset",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "InvalidAddress": {
+ "title": "This is not a valid {{currencyName}} address"
+ },
+ "InvalidAddressBecauseAlreadyDelegated": {
+ "title": "Your account is already delegated to this validator"
+ },
+ "InvalidAddressBecauseDestinationIsAlsoSource": {
+ "title": "Destination and source accounts must not be the same."
+ },
+ "InvalidRecipient": {
+ "title": "Invalid recipient"
+ },
+ "LatestMCUInstalledError": {
+ "title": "Sorry, there's nothing to update",
+ "description": "Please contact Ledger Support if you cannot use your device."
+ },
+ "LedgerAPIError": {
+ "title": "Sorry, try again (API HTTP {{status}})",
+ "description": "Unsuccessful calls to the Ledger API server. Please try again."
+ },
+ "LedgerAPIErrorWithMessage": {
+ "title": "{{message}}",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "LedgerAPINotAvailable": {
+ "title": "Sorry, {{currencyName}} services unavailable",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "ManagerAPIsFail": {
+ "title": "Sorry, Manager services are unavailable",
+ "description": "Please check the network status."
+ },
+ "ManagerAppAlreadyInstalled": {
+ "title": "Sorry, that's already installed",
+ "description": "Please check which apps are already installed on your device."
+ },
+ "ManagerAppRelyOnBTC": {
+ "title": "Bitcoin and Ethereum apps needed",
+ "description": "Please install the latest Bitcoin and Ethereum apps first."
+ },
+ "ManagerDeviceLocked": {
+ "title": "Your device is locked",
+ "description": "Please unlock it."
+ },
+ "ManagerNotEnoughSpace": {
+ "title": "Not enough storage left",
+ "info": "Please uninstall some apps to free up space for the {{app}} app.",
+ "description": "Uninstalling apps has no impact on your assets."
+ },
+ "ManagerQuitPage": {
+ "install": {
+ "title": "App installation in progress",
+ "description": "Quitting the Manager will terminate installation.",
+ "stay": "Continue installation"
+ },
+ "uninstall": {
+ "title": "App uninstall in progress",
+ "description": "Quitting the Manager will terminate uninstallation.",
+ "stay": "Continue uninstallation"
+ },
+ "update": {
+ "title": "App update in progress",
+ "description": "Quitting the Manager will terminate update.",
+ "stay": "Continue update"
+ },
+ "quit": "Quit Manager"
+ },
+ "ManagerUninstallBTCDep": {
+ "title": "Sorry, this app is needed",
+ "description": "Uninstall the Bitcoin or Ethereum app last."
+ },
+ "NetworkDown": {
+ "title": "Sorry, internet seems to be down",
+ "description": "Please check your internet connection."
+ },
+ "NoAddressesFound": {
+ "title": "Sorry, no account was found",
+ "description": "Something went wrong with the address calculation. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "NotEnoughBalance": {
+ "title": "Sorry, insufficient funds",
+ "description": "Please make sure the account has enough funds."
+ },
+ "NotEnoughGas": {
+ "title": "Parent account balance insufficient for network fees",
+ "description": "Please send some ETH to your account to pay for ERC20 token transactions."
+ },
+ "NotEnoughBalanceToDelegate": {
+ "title": "Insufficient balance to delegate"
+ },
+ "NotEnoughBalanceInParentAccount": {
+ "title": "Insufficient balance in the parent account"
+ },
+ "NotEnoughSpendableBalance": {
+ "title": "Balance cannot be below {{minimumAmount}}"
+ },
+ "NotEnoughBalanceBecauseDestinationNotCreated": {
+ "title": "Minimum of {{minimalAmount}} needed to activate recipient address"
+ },
+ "PairingFailed": {
+ "title": "Pairing unsuccessful",
+ "description": "Please try again or consult our Bluetooth troubleshooting article."
+ },
+ "PasswordIncorrect": {
+ "title": "Incorrect password",
+ "description": "Please try again."
+ },
+ "PasswordsDontMatch": {
+ "title": "Password does not match",
+ "description": "Please try again."
+ },
+ "SelectExchangesLoadError": {
+ "title": "Unable to load",
+ "description": "Cannot load the exchanges."
+ },
+ "SyncError": {
+ "title": "Synchronization error",
+ "description": "Some accounts could not be synchronized."
+ },
+ "TimeoutError": {
+ "title": "Sorry, server took too long to respond",
+ "description": "Please try again."
+ },
+ "TimeoutTagged": {
+ "title": "Sorry, server took too long to respond ({{tag}})",
+ "description": "Timeout occurred."
+ },
+ "TransactionRefusedOnDevice": {
+ "title": "Transaction refused on device",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "TransportError": {
+ "title": "Something went wrong. Please reconnect your device.",
+ "description": "{{message}} Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "TransportStatusError": {
+ "title": "Something went wrong. Please reconnect your device.",
+ "description": "{{message}} Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "TronNoFrozenForBandwidth": {
+ "title": "No assets to unfreeze",
+ "description": "You don't have Bandwidth assets to unfreeze."
+ },
+ "TronNoFrozenForEnergy": {
+ "title": "No assets to unfreeze",
+ "description": "You don't have Energy assets to unfreeze."
+ },
+ "TronUnfreezeNotExpired": {
+ "title": "Unfreeze is not available yet",
+ "description": "Please wait 3 days after your last Freeze operation."
+ },
+ "TronVoteRequired": {
+ "title": "At least 1 vote is needed"
+ },
+ "TronInvalidVoteCount": {
+ "title": "Vote format is incorrect",
+ "description": "You can only vote using round numbers."
+ },
+ "TronRewardNotAvailable": {
+ "title": "Rewards are not claimable yet",
+ "description": "Please wait 24 hours between claims."
+ },
+ "TronNoReward": {
+ "title": "There is no reward to be claimed"
+ },
+ "TronInvalidFreezeAmount": {
+ "title": "Amount to freeze cannot be smaller than 1"
+ },
+ "TronSendTrc20ToNewAccountForbidden": {
+ "title": "Sending TRC20 to the new account will not activate it",
+ "description": "To activate it, first send either TRX or TRC10 to the account. Then it can receive TRC20."
+ },
+ "TronUnexpectedFees": {
+ "title": "Additional fees may apply"
+ },
+ "TronNotEnoughTronPower": {
+ "title": "Not enough votes available"
+ },
+ "TronTransactionExpired": {
+ "title": "Transaction timeout expired",
+ "description": "Transactions must be signed within 30 seconds. Please try again."
+ },
+ "TronNotEnoughEnergy": {
+ "title": "Not enough Energy to send this token"
+ },
+ "UpdateYourApp": {
+ "title": "App update needed",
+ "description": "Please uninstall then reinstall the {{managerAppName}} app in the Manager."
+ },
+ "UserRefusedAllowManager": {
+ "title": "Manager disabled on device",
+ "description": "Please allow the Manager on your device, then try again."
+ },
+ "UserRefusedAddress": {
+ "title": "Receive address rejected",
+ "description": "You rejected the address. If in doubt, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "UserRefusedDeviceNameChange": {
+ "title": "Rename canceled on device",
+ "description": "You canceled the rename. Please try again."
+ },
+ "UserRefusedFirmwareUpdate": {
+ "title": "Firmware update canceled on device",
+ "description": "You canceled the firmware update. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "UserRefusedOnDevice": {
+ "title": "Operation canceled on device",
+ "description": "You rejected the operation on the device."
+ },
+ "WebsocketConnectionError": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again with a better network connection (websocket error)."
+ },
+ "WebsocketConnectionFailed": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again with a better network connection (websocket failed)."
+ },
+ "WrongDeviceForAccount": {
+ "title": "Something went wrong",
+ "description": "Please check that your hardware wallet is set up with the recovery phrase or passphrase associated to the selected account."
+ },
+ "UnexpectedBootloader": {
+ "title": "Sorry, your device must not be in Bootloader mode",
+ "description": "Please restart your device without touching the buttons when the logo appears. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "UnavailableTezosOriginatedAccountReceive": {
+ "title": "Cannot receive in subaccounts. Please select the parent account.",
+ "description": "If you want to receive funds, please use the parent account"
+ },
+ "UnavailableTezosOriginatedAccountSend": {
+ "title": "Cannot send from subaccounts yet",
+ "description": "This feature will be added at a later stage due to changes recently introduced by the Babylon update."
+ },
+ "AccessDeniedError": {
+ "title": "Ledger Live needs an update",
+ "description": "Please update Ledger Live to the latest version, and re verify your identity to swap with wyre"
+ },
+ "RecommendUndelegation": {
+ "title": "Please undelegate the account before emptying it"
+ },
+ "RecommendSubAccountsToEmpty": {
+ "title": "Please empty all subaccounts first"
+ },
+ "NotSupportedLegacyAddress": {
+ "title": "The legacy address format is no longer supported"
+ },
+ "StellarWrongMemoFormat": {
+ "title": "Memo format is wrong"
+ },
+ "SourceHasMultiSign": {
+ "title": "Please disable multisign to send {{currencyName}}"
+ },
+ "StellarMemoRecommended": {
+ "title": "A memo may be needed when sending to this recipient"
+ },
+ "StratisDown2021Warning": {
+ "description": "The Stratis blockchain evolved and may no longer work correctly. Support for the original Stratis blockchain will be maintained until 16 October 2021."
+ },
+ "SwapExchangeRateAmountTooLow": {
+ "title": "Amount must be higher than {{minAmountFromFormatted}}"
+ },
+ "SwapExchangeRateAmountTooHigh": {
+ "title": "Amount must be lower than {{maxAmountFromFormatted}}"
+ },
+ "SwapGenericAPIError": {
+ "title": "Exchange rate expired",
+ "description": "You must confirm the swap before the timer runs out. The exchange rate remains valid for a fixed duration."
+ },
+ "PolkadotElectionClosed": {
+ "title": "Validators election must be closed"
+ },
+ "PolkadotNotValidator": {
+ "title": "Some selected addresses are not validators"
+ },
+ "PolkadotLowBondedBalance": {
+ "title": "All bonded assets will be unbonded if < 1 DOT"
+ },
+ "PolkadotNoUnlockedBalance": {
+ "title": "You have no unbonded assets"
+ },
+ "PolkadotNoNominations": {
+ "title": "You have no nominations"
+ },
+ "PolkadotAllFundsWarning": {
+ "title": "Ensure you have enough balance left for future transaction fees"
+ },
+ "PolkadotDoMaxSendInstead": {
+ "title": "Balance cannot be below {{minimumBalance}}. Send max to empty account."
+ },
+ "PolkadotBondMinimumAmount": {
+ "title": "You must bond at least {{minimumBondAmount}}."
+ },
+ "PolkadotBondMinimumAmountWarning": {
+ "title": "You bonded balance should be at least {{minimumBondBalance}}."
+ },
+ "PolkadotMaxUnbonding": {
+ "title": "You have exceeded the unbond limit"
+ },
+ "PolkadotValidatorsRequired": {
+ "title": "You must select at least one validator"
+ },
+ "TaprootNotActivated": {
+ "title": "Taproot mainnet is not activated yet"
+ },
+ "NotEnoughNftOwned": {
+ "title": "You have exceeded the number of available tokens"
+ },
+ "generic": {
+ "title": "{{message}}",
+ "description": "Something went wrong. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "SolanaAccountNotFunded": {
+ "title": "Account not funded"
+ },
+ "SolanaAddressOfEd25519": {
+ "title": "Address off ed25519 curve"
+ },
+ "SolanaMemoIsTooLong": {
+ "title": "Memo is too long. Max length is {{maxLength}}"
+ }
+ },
+ "bluetooth": {
+ "required": "Sorry, it looks like Bluetooth is disabled",
+ "locationRequiredTitle": "Location is required for Bluetooth LE",
+ "locationRequiredMessage": "On Android location permission is required to list Bluetooth LE devices.",
+ "checkEnabled": "Please enable Bluetooth in your phone settings."
+ },
+ "location": {
+ "required": "Location services required",
+ "open": "Open location settings",
+ "disabled": "Ledger Live requires location services to pair your device through Bluetooth.",
+ "noInfos": "Ledger does not access your location information."
+ },
+ "permissions": {
+ "open": "Open app permissions"
+ },
+ "fees": {
+ "speed": {
+ "high": "High",
+ "standard": "Standard",
+ "low": "Low",
+ "slow": "slow",
+ "medium": "medium",
+ "fast": "fast",
+ "custom": "Custom",
+ "blockCount": "{{blockCount}} blocks"
+ }
+ },
+ "signout": {
+ "confirm": "Are you sure you want to sign out?",
+ "disclaimer": "All account data will be removed from your phone.",
+ "action": "Sign me out"
+ },
+ "auth": {
+ "failed": {
+ "biometrics": {
+ "title": "{{biometricsType}} unlock failed",
+ "description": "Enter your password to continue",
+ "authenticate": "Please authenticate with the Ledger Live app"
+ },
+ "denied": "Auth Security was not enabled because your phone failed to authenticate.",
+ "title": "Authentication failed",
+ "buttons": {
+ "tryAgain": "Try again",
+ "reset": "Reset"
+ }
+ },
+ "unlock": {
+ "biometricsTitle": "Please authenticate with the Ledger Live app",
+ "title": "Welcome back",
+ "desc": "Enter your password to continue",
+ "inputPlaceholder": "Type your password",
+ "login": "Log in",
+ "forgotPassword": "I lost my password"
+ },
+ "addPassword": {
+ "placeholder": "Choose your password",
+ "title": "Password Lock"
+ },
+ "confirmPassword": {
+ "title": "Confirm Password",
+ "placeholder": "Confirm your password"
+ },
+ "enableBiometrics": {
+ "title": "{{biometricsType}}",
+ "desc": "Unlock with {{biometricsType}}"
+ }
+ },
+ "reset": {
+ "title": "Uninstall then reinstall Ledger Live",
+ "description": "Please uninstall then reinstall the app on your phone to delete Ledger Live data, including accounts and settings.",
+ "button": "Reset",
+ "warning": "Resetting Ledger Live will erase your swap transaction history for all your accounts."
+ },
+ "graph": {
+ "week": "1W",
+ "month": "1M",
+ "year": "1Y",
+ "all": "ALL"
+ },
+ "carousel": {
+ "title": "Close banner?",
+ "description": "We'll inform you of any new announcements.",
+ "confirm": "Confirm",
+ "undo": "Undo",
+ "banners": {
+ "valentine": {
+ "title": "Valentine’s day",
+ "description": "Reduced fees on Buy and Sell"
+ },
+ "academy": {
+ "title": "Ledger Academy",
+ "description": "Everything you need to\nknow about crypto"
+ },
+ "stakeCosmos": {
+ "title": "Stake COSMOS",
+ "description": "Delegate your ATOM earn rewards today."
+ },
+ "familyPack": {
+ "title": "Family pack",
+ "description": "Get your family into crypto with 3 Nano S"
+ },
+ "buyCrypto": {
+ "title": "Buy Crypto",
+ "description": "Buy crypto on\nLedger Live"
+ },
+ "swap": {
+ "title": "Swap crypto",
+ "description": "Securely exchange one\ncrypto for another"
+ },
+ "algorand": {
+ "title": "Algorand",
+ "description": "Earn ALGO rewards with each transaction."
+ },
+ "sell": {
+ "title": "Sell crypto",
+ "description": "Sell Bitcoin directly from Ledger Live."
+ },
+ "vote": {
+ "title": "Vote with your ledger",
+ "description": "Vote directly from your Ledger Wallet."
+ },
+ "lending": {
+ "title": "Lend crypto",
+ "description": "Lend asset on the Compound Protocol"
+ },
+ "blackfriday": {
+ "title": "BLACK FRIDAY",
+ "description": "Enjoy 40% off with promo code BLACKFRIDAY20"
+ }
+ }
+ },
+ "buyDevice": {
+ "title": "GET YOUR LEDGER",
+ "desc": "Protect your cryptocurrencies with industry-leading security",
+ "cta": "Buy your Ledger now",
+ "footer": "I already have a device, set it up now",
+ "0": {
+ "title": "Gain financial freedom",
+ "desc": "Take charge and unlock the freedom to manage your crypto on your own."
+ },
+ "1": {
+ "title": "Access a world of Defi",
+ "desc": "Securely interact with our trusted DeFi applications directly from the app."
+ },
+ "2": {
+ "title": "Your one stop shop for crypto",
+ "desc": "You can buy, sell and swap major cryptocurrencies from our partners safely with your Ledger."
+ },
+ "3": {
+ "title": "Peace of mind with certified security",
+ "desc": "Our products are the only hardware wallets certified for their security by national cyber security agencies."
+ }
+ },
+ "onboarding": {
+ "stepWelcome": {
+ "title": "Welcome to Ledger",
+ "subtitle": "Safely manage your cryptos from your smartphone.",
+ "start": "Get started",
+ "noDevice": "No device?",
+ "buy": "Buy a {{fullDeviceName}}",
+ "terms": "By tapping “Get Started” you consent and agree to our",
+ "termsLink": "Terms of Service",
+ "privacyLink": "Privacy Policy",
+ "and": "and"
+ },
+ "postWelcomeStep": {
+ "title": "It’s your choice",
+ "subtitle": "You can choose to set up your Ledger now or take a look around the ledger live app first.",
+ "noLedgerLink": "I don’t have a Ledger and I would like to buy one",
+ "setupLedger": {
+ "title": "Set up my Ledger",
+ "subtitle": "Secure your crypto now",
+ "label": "30 mins"
+ },
+ "discoverLedger": {
+ "title": "Discover Ledger Live",
+ "subtitle": "Explore the app"
+ }
+ },
+ "discoverLive": {
+ "0": {
+ "title": "One place for all your crypto needs",
+ "desc": "Monitor, buy, sell, swap, grow and manage your assets securely and get the best crypto insights"
+ },
+ "1": {
+ "title": "Not your keys, not your crypto",
+ "desc": "Securing your assets with a Hardware wallet is the best and only way to own and secure your crypto assets"
+ },
+ "2": {
+ "title": "Reclaim power over your money",
+ "desc": "The Ledger Nano X keeps your coins offline and protected. Combine it to Ledger Live app for maximum security and control over your crypto",
+ "cta": "Begin your Journey"
+ }
+ },
+ "stepLanguage": {
+ "title": "Select your language",
+ "cta": "Continue",
+ "warning": {
+ "title": "Get started in English",
+ "cta": "Got it!",
+ "desc": "We are introducing additional languages in order to help your onboarding with Ledger. Please be aware that the rest of the Ledger experience is currently only available in English."
+ }
+ },
+ "stepSelectDevice": {
+ "title": "Select your device",
+ "nanoS": "Nano S",
+ "nanoSP": "Nano S+",
+ "nanoX": "Nano X",
+ "blue": "Blue"
+ },
+ "stepUseCase": {
+ "title": "Hello!",
+ "or": "Or",
+ "firstUse": {
+ "title": "First time using this Nano?",
+ "label": "First use",
+ "subTitle": "Set up a new Nano",
+ "desc": "Let’s start and set up your device!"
+ },
+ "devicePairing": {
+ "title": "Already have a recovery phrase?",
+ "label": "Device pairing",
+ "subTitle": "Connect your Nano",
+ "desc": "Your device is already set up? Connect it to the app!"
+ },
+ "desktopSync": {
+ "title": "Already have a recovery phrase?",
+ "label": "Desktop sync",
+ "subTitle": "Sync crypto from your desktop app",
+ "desc": "Already got the desktop app? Sync it to manage your crypto from your smartphone!"
+ },
+ "restoreDevice": {
+ "label": "Restore device",
+ "subTitle": "Restore your recovery phrase on a new device",
+ "desc": "Use an existing recovery phrase to restore your private keys on a new Nano!"
+ }
+ },
+ "stepNewDevice": {
+ "0": {
+ "label": "Basics",
+ "title": "Access your crypto",
+ "desc": "Your crypto assets are stored on the blockchain. You need a private key to access and manage them."
+ },
+ "1": {
+ "label": "Basics",
+ "title": "Own your private key",
+ "desc": "Your private key is stored within your Nano and you must be the only one to own it to be in control of your money."
+ },
+ "2": {
+ "label": "Basics",
+ "title": "Stay offline",
+ "desc": "Your Nano works as a \"cold storage\" wallet. This means that it never exposes your private key online, even when using the app."
+ },
+ "3": {
+ "label": "Basics",
+ "title": "Validate transactions",
+ "desc": "Ledger Live allows you to buy, sell, manage, exchange and earn crypto while remaining protected. You will validate every crypto transaction with your Nano."
+ },
+ "4": {
+ "label": "Basics",
+ "title": "Let’s set up your Nano!",
+ "desc": "We'll start by setting up your Nano security."
+ },
+ "cta": "Let’s do this!"
+ },
+ "stepSetupDevice": {
+ "start": {
+ "title": "The best way to get you started:",
+ "bullets": {
+ "0": {
+ "label": "Plan 30 minutes and take your time."
+ },
+ "1": {
+ "label": "Grab a pen to write with."
+ },
+ "2": {
+ "label": "Stay alone, and choose a safe and quiet environment."
+ }
+ },
+ "cta": "OK, I’m ready!",
+ "warning": {
+ "title": "Please be careful",
+ "desc": "Make sure you follow the instructions on this app at every step of the process.",
+ "ctaText": "Got it!"
+ }
+ },
+ "setup": {
+ "bullets": {
+ "0": {
+ "title": "Turn on Nano",
+ "nanoX": {
+ "label": "Turn on your device by pressing the black button for 1 second."
+ },
+ "nanoS": {
+ "label": "Turn on your device by connecting it to the USB port of your phone."
+ },
+ "nanoSP": {
+ "label": "Turn on your device by connecting it to the USB port of your phone."
+ },
+ "blue": {
+ "label": "Turn on your device by connecting it to the USB port of your phone and pressing the power button."
+ }
+ },
+ "1": {
+ "title": "Browse ",
+ "label": "Learn how to interact with your device by reading the on-screen instructions."
+ },
+ "2": {
+ "title": "Select “Set up as new device”",
+ "label": "Press both buttons simultaneously to validate the selection."
+ },
+ "3": {
+ "title": "Follow instructions",
+ "label": "Come back here to follow instructions on your PIN code."
+ }
+ },
+ "cta": "Next step"
+ },
+ "pinCode": {
+ "title": "PIN code",
+ "desc": "Your PIN code is the first layer of security. It physically secures access to your Nano and your private keys. Your PIN code must be 4 to 8 digits long.",
+ "checkboxDesc": "I understand that I must choose my PIN code by myself and keep it private.",
+ "cta": "Set up PIN code"
+ },
+ "pinCodeSetup": {
+ "bullets": {
+ "0": {
+ "title": "Choose PIN code",
+ "label": "Press the left or right button to change digits. Press both buttons to validate a digit. Select <1>1> to confirm your PIN code. Select <2>2> to erase a digit."
+ },
+ "1": {
+ "title": "Confirm PIN code",
+ "label": "Enter your PIN code again to confirm it."
+ }
+ },
+ "infoModal": {
+ "title": "Secure your PIN code",
+ "bullets": {
+ "0": {
+ "label": "Always choose a PIN code by yourself."
+ },
+ "1": {
+ "label": "Always enter your PIN code out of sight."
+ },
+ "2": {
+ "label": "You can change your PIN code if needed."
+ },
+ "3": {
+ "label": "Three wrong PIN code entries in a row will reset the device."
+ },
+ "4": {
+ "label": "Never use an easy PIN code like 0000, 123456, or 55555555."
+ },
+ "5": {
+ "label": "Never share your PIN code with someone else. Not even with Ledger."
+ },
+ "6": {
+ "label": "Never use a PIN code you did not choose yourself."
+ },
+ "7": {
+ "label": "Never store your PIN code on a computer or phone."
+ }
+ }
+ },
+ "cta": "Next step"
+ },
+ "recoveryPhrase": {
+ "title": "Recovery phrase",
+ "desc": "Your recovery phrase is a secret list of 24 words that backs up your private keys.",
+ "desc_1": "Your Nano generates a unique recovery phrase. Ledger does not keep a copy of it.",
+ "cta": "Recovery phrase",
+ "checkboxDesc": "I understand that if I lose this recovery phrase, I will not be able to access my crypto in case I lose access to my Nano."
+ },
+ "recoveryPhraseSetup": {
+ "infoModal": {
+ "title": "How does a recovery phrase work?",
+ "desc": "Your recovery phrase works like a unique master key. Your Ledger device uses it to calculate private keys for every crypto asset you own.",
+ "desc_1": "To restore access to your crypto, any wallet can calculate the same private keys from your recovery phrase.",
+ "link": "More about the recovery phrase",
+ "title_1": "What if I lose access to my Nano?",
+ "bullets": {
+ "0": {
+ "label": "Get a new hardware wallet."
+ },
+ "1": {
+ "label": "Select “Restore recovery phrase on a new device” in the Ledger app."
+ },
+ "2": {
+ "label": "Enter your recovery phrase on your new device to restore access to your crypto."
+ }
+ }
+ },
+ "bullets": {
+ "0": {
+ "title": "Take your Recovery sheet",
+ "label": "Grab a blank Recovery sheet, included with your Nano. Please reach out to Ledger Support if the Recovery sheet did not come blank."
+ },
+ "1": {
+ "title": "Write down all words",
+ "label": "Write Word #1 displayed on your Nano in position 1 of your Recovery sheet. Then press right on your Nano to display Word #2 and write it down in position 2.",
+ "label_1": "Repeat the process for all words while carefully respecting the order and spelling. Press left on your Nano to check for any mistakes."
+ },
+ "2": {
+ "title": "Confirm your recovery phrase",
+ "label": "Scroll through the words until you find Word #1 by pressing the right button. Validate by pressing both buttons."
+ },
+ "3": {
+ "title": "Repeat for all words!"
+ }
+ },
+ "cta": "Confirm recovery phrase",
+ "nextStep": "Next step"
+ },
+ "hideRecoveryPhrase": {
+ "title": "Hide your recovery phrase",
+ "desc": "Your recovery phrase is your last chance to access your crypto if you cannot use your Nano. You must keep it in a safe place.",
+ "bullets": {
+ "0": {
+ "label": "Enter these words on a hardware wallet only, never on computers or phones."
+ },
+ "1": {
+ "label": "Never share your 24 words with anyone, not even with Ledger."
+ }
+ },
+ "cta": "OK, I’m done!",
+ "infoModal": {
+ "label": "Learn how to hide it",
+ "title": "Where should I keep my recovery phrase?",
+ "bullets": {
+ "0": {
+ "label": "<1>NEVER1> enter it on a computer, phone or any other device. Don't take a picture of it."
+ },
+ "1": {
+ "label": "<1>NEVER1> share your 24 words with anyone."
+ },
+ "2": {
+ "label": "<1>ALWAYS1> store it in a secure place, out of sight."
+ },
+ "3": {
+ "label": "Ledger will never ask for your recovery phrase."
+ },
+ "4": {
+ "label": "If any person or application asks for it, assume it is a scam!"
+ }
+ }
+ },
+ "warning": {
+ "title": "Now game on!",
+ "desc": "Answer 3 simple questions to avoid common misconceptions about your hardware wallet.",
+ "cta": "Let’s take the quiz"
+ }
+ }
+ },
+ "stepRecoveryPhrase": {
+ "importRecoveryPhrase": {
+ "title": "Restore from recovery phrase",
+ "desc": "Restore your Nano from your recovery phrase to restore, replace or back up your Ledger hardware wallet.",
+ "desc_1": "Your Nano will restore your private keys and you will be able to access and manage your crypto.",
+ "cta": "OK, I’m ready!",
+ "warning": {
+ "title": "We recommend Ledger recovery phrases only",
+ "desc": "Ledger cannot guarantee the security of external recovery phrases. We recommend setting up your Nano as a new device if your recovery phrase was not generated by a Ledger.",
+ "cta": "Got it!"
+ },
+ "nextStep": "Next step",
+ "bullets": {
+ "0": {
+ "title": "Turn on Nano",
+ "nanoX": {
+ "label": "Turn on your device by pressing the black button for 1 second."
+ },
+ "nanoS": {
+ "label": "Turn on your device by connecting it to the USB port of your phone."
+ },
+ "nanoSP": {
+ "label": "Turn on your device by connecting it to the USB port of your phone."
+ },
+ "blue": {
+ "label": "Turn on your device by connecting it to the USB port of your phone and pressing the power button."
+ }
+ },
+ "1": {
+ "title": "Browse",
+ "label": "Learn how to interact with your device by reading the on-screen instructions."
+ },
+ "2": {
+ "title": "Select “Restore from recovery phrase”",
+ "label": "Press both buttons simultaneously to validate the selection."
+ },
+ "3": {
+ "title": "Follow instructions",
+ "label": "Come back here to follow instructions on your PIN code."
+ }
+ }
+ },
+ "existingRecoveryPhrase": {
+ "title": "Enter your recovery phrase",
+ "paragraph1": "Your recovery phrase is the secret list of words that you backed up when you first set up your wallet.",
+ "paragraph2": "Ledger does not keep a copy of your recovery phrase.",
+ "checkboxDesc": "I understand that if I lose my recovery phrase, I will not be able to access my crypto in case I lose access to my Nano.",
+ "bullets": {
+ "0": {
+ "title": "Grab your recovery phrase"
+ },
+ "1": {
+ "title": "Select recovery phrase length",
+ "label": "Your recovery phrase can have 12, 18 or 24 words. You must enter all words to access your crypto."
+ },
+ "2": {
+ "title": "Enter Word #1...",
+ "label": "Enter the first letters of Word #1 by selecting them with the right or left button. Press both buttons to validate each letter."
+ },
+ "3": {
+ "title": "Validate Word #1...",
+ "label": "Choose Word #1 from the suggestions. Press both buttons to validate."
+ },
+ "4": {
+ "title": "Repeat for all words!"
+ }
+ },
+ "nextStep": "Next step"
+ }
+ },
+ "stepPairNew": {
+ "nanoX": {
+ "title": "Pair your Nano",
+ "desc": "This is the first time you’re setting up your Nano with this phone. Let’s quickly pair your device.",
+ "cta": "Let’s pair my Nano"
+ },
+ "nanoS": {
+ "title": "Connect your Nano",
+ "desc": "This is the first time you’re setting up your Nano with this phone. Let’s quickly connect your device.",
+ "cta": "Let’s connect my Nano"
+ },
+ "nanoSP": {
+ "title": "Connect your Nano",
+ "desc": "This is the first time you’re setting up your Nano with this phone. Let’s quickly connect your device.",
+ "cta": "Let’s connect my Nano"
+ },
+ "blue": {
+ "title": "Connect your Blue",
+ "desc": "This is the first time you’re setting up your Blue with this phone. Let’s quickly connect your device.",
+ "cta": "Let’s connect my Blue"
+ },
+ "infoModal": {
+ "title": "Where can I find my device name?",
+ "desc": "On your device, select Settings > General > Device name.",
+ "title_1": "How to set up Bluetooth connection?",
+ "title_2": "How do I use my Nano X without Bluetooth?",
+ "desc_1": "Use an <1>OTG-cable1> to connect your Ledger Nano X to your Android smartphone (iOS not supported). Manage your crypto with Ledger Live mobile or any other compatible (web) app.",
+ "bullets": {
+ "0": {
+ "label": "Make sure Bluetooth is enabled on your smartphone and on your Ledger Nano X. Your Ledger Nano X should be on the Dashboard, its main home screen."
+ },
+ "1": {
+ "label": "<1>{{Os}}1>: Make sure location services are enabled in your phone's settings for Ledger Live. Ledger Live never uses your location information, this is a requirement for Bluetooth on {{Os}}."
+ },
+ "2": {
+ "label": "If you have a Bluetooth pairing issue, please refer to the following article",
+ "link": "Fix connection issues."
+ }
+ }
+ },
+ "errorInfoModal": {
+ "title": "Something went wrong?",
+ "title_1": "I have an Android",
+ "title_2": "Update Android version",
+ "desc": "If you're experiencing Bluetooth issues with your Nano X, please remove the pairing and forget Nano X on your phone. Then try the pairing again.",
+ "desc_1": "It may take a while before the Bluetooth pairing request is displayed. Make sure you verify and confirm the pairing code both on your Nano X and on your phone.",
+ "desc_2": "Check in your phone's Bluetooth settings whether the Ledger Nano X is detected. If it not detected, make sure you turn on the Bluetooth on your Ledger Nano X.",
+ "desc_3": "Location services",
+ "desc_4": "If the Ledger Nano X is not detected in your mobile app when trying to pair it, please try the following solution:",
+ "desc_5": "Ledger mobile app asks you to allow location services when it's not enabled yet, but on a few phone models, this is not always properly detected. Please note that Ledger mobile app never uses your location information, this permission is simply required for Bluetooth on Android.",
+ "desc_6": "Some users have reported having fixed their connection issues by updating the Android version running on their phone to a newer one. Please check with your phone manufacturer whether an update is available.",
+ "link": "Learn more",
+ "bullets": {
+ "0": {
+ "label": "Navigate to the system options for the Ledger Live app on your Android phone."
+ },
+ "1": {
+ "label": "Allow the location to be used."
+ },
+ "2": {
+ "label": "Return to the mobile app."
+ },
+ "3": {
+ "label": "Check whether your Nano X is detected."
+ }
+ }
+ }
+ },
+ "stepImportAccounts": {
+ "title": "Sync crypto from desktop",
+ "desc": "If you already have your crypto set up in the Ledger desktop app, you can sync them to manage them from your phone.",
+ "cta": "I’m ready to scan",
+ "bullets": {
+ "0": {
+ "label": "On the Desktop app, select <1>Settings > Accounts > Account export1>."
+ },
+ "1": {
+ "label": "Scan the LiveQR code with your phone."
+ },
+ "2": {
+ "label": "Select the crypto accounts to import."
+ }
+ },
+ "warning": {
+ "title": "Your desktop and mobile apps must be synced manually.",
+ "desc": "Ledger Live respects your privacy and stores your data locally. If you change accounts and settings on your phone, you will need to do the same on your computer, and vice versa. Your transactions stay synchronized with the blockchain.",
+ "cta": "Got it!"
+ }
+ },
+ "stepSetupPin": {
+ "step1": "Turn on your {{fullDeviceName}} and follow the instructions.",
+ "step1-nanoS": "Connect your {{fullDeviceName}} to your phone using an OTG cable.",
+ "step2": "Press both buttons together to select <1><1>Setup as a new device.1>1>",
+ "step2-restore": "Press both buttons together to select <1><1>Restore from recovery phrase.1>1>",
+ "step3": "Press left or right button to select a digit. Press buttons together to confirm.",
+ "step4prefix": "Select ",
+ "step4suffix1": " to confirm your PIN.",
+ "step4suffix2": " to erase last digit.",
+ "modal": {
+ "step1": "Always choose <1><1>your own1>1> PIN",
+ "step2": "Use 8 digits for greater security",
+ "step3": "Never use a device with a PIN or recovery phrase already setup"
+ }
+ },
+ "stepWriteRecovery": {
+ "step1": "Write <1><1>Word #11>1> in entry 1 on a blank Recovery sheet",
+ "step2": "Press the right button and continue to write down all 24 words.",
+ "step3": "Confirm each word of your recovery phrase: select it then confirm it by pressing both buttons together.",
+ "modal": {
+ "step1": "Store your 24-word recovery phrase in a safe place, out of sight.",
+ "step2": "Make sure you are the only person to have the recovery phrase.",
+ "step3": "Ledger does not save your recovery phrase.",
+ "step4": "Never use a device with a recovery phrase or PIN already setup."
+ }
+ },
+ "stepPassword": {
+ "desc": "Set a password to protect Ledger Live data on your phone.",
+ "descConfigured": "Password lock enabled successfully",
+ "setPassword": "Set the password",
+ "modal": {
+ "step1": "Keep your password safe. Do not share it.",
+ "step2": "Keep your password safe. Losing it means resetting Ledger Live and loading the accounts again.",
+ "step3": "Resetting Ledger Live has no impact on your crypto assets."
+ }
+ },
+ "stepFinish": {
+ "title": "Your device is ready!",
+ "readOnlyTitle": "All set!",
+ "desc": "Install apps on your device and manage your portfolio",
+ "cta": "Open Ledger Live"
+ },
+ "quizz": {
+ "label": "Quiz",
+ "modal": {
+ "success": "Congrats!",
+ "fail": "Incorrect!"
+ },
+ "coins": {
+ "title": "As a Ledger user, my crypto is stored:",
+ "answers": {
+ "correct": "On the blockchain",
+ "wrong": "On my Nano"
+ },
+ "modal": {
+ "text": "Your crypto is always stored on the blockchain. Your hardware wallet only holds your private key, which gives you access to your crypto.",
+ "cta": "Next question"
+ }
+ },
+ "recoveryPhrase": {
+ "title": "If my recovery phrase is no longer secret and safe...",
+ "answers": {
+ "correct": "My crypto is no longer safe and I need to transfer them to a secure place",
+ "wrong": "No problem, Ledger can send me a copy"
+ },
+ "modal": {
+ "text": "Anyone who knows your recovery phrase can steal your crypto assets. \nIf you lose it, you must quickly transfer your crypto to a secure place.",
+ "cta": "Next question"
+ }
+ },
+ "privateKey": {
+ "title": "When I connect my Nano to the Ledger app, my private key is...",
+ "answers": {
+ "correct": "Still offline",
+ "wrong": "Briefly connected to the internet"
+ },
+ "modal": {
+ "text": "Your private key always remains offline in your hardware wallet. Even when connected to your Nano, the Ledger app cannot access your private key. You must physically authorize every transaction on your device.",
+ "cta": "Finish quiz"
+ }
+ },
+ "final": {
+ "successTitle": "Already a pro!",
+ "successText": "You are ready to safely manage your crypto. Only one quick step left!",
+ "failTitle": "You will soon become a pro...",
+ "failText": "Don’t worry, Ledger is here to guide you through your journey. You will soon feel extra comfortable about your crypto safety. Only one quick step left!",
+ "cta": "Next step"
+ }
+ },
+ "warning": {
+ "recoveryPhrase": {
+ "title": "Do not use a recovery phrase that you have not generated yourself.",
+ "desc": "Your Ledger hardware wallet’s pin code and recovery phrase should be initialized by you and only you. If you have received a device with a pre-existing seed word or an already initialized pin code do not use the product and contact our customer support.",
+ "supportLink": "Contact customer support"
+ },
+ "seed": {
+ "title": "Please check the box contents",
+ "desc": "If your {{deviceName}} came with a PIN code or recovery phrase, it’s not safe to use and you should contact Ledger Support.",
+ "warning": "Only use a recovery phrase that your device displayed when it was set up",
+ "continueCTA": "Continue",
+ "contactSupportCTA": "Contact support"
+ }
+ }
+ },
+ "tabs": {
+ "portfolio": "Portfolio",
+ "accounts": "Accounts",
+ "transfer": "Transfer",
+ "manager": "Manager",
+ "settings": "Settings",
+ "platform": "Discover",
+ "nanoX": "Nano X",
+ "market": "Market",
+ "learn": "Learn"
+ },
+ "learn": {
+ "pageTitle": "learn",
+ "noConnection": "No connection",
+ "noConnectionDesc": "It seems you don't have access to the Internet. Please check your connection and try again.",
+ "sectionShows": "Shows",
+ "sectionVideo": "Video",
+ "sectionPodcast": "Podcast",
+ "sectionArticles": "Articles"
+ },
+ "portfolio": {
+ "totalBalance": "Total balance",
+ "syncError": "Sync error",
+ "syncFailed": "Synchronization failed",
+ "syncPending": "Synchronizing...",
+ "transactionsPendingConfirmation": {
+ "title": "Unsynchronized balance",
+ "desc": "Some transactions are not confirmed yet. These will be reflected in your balance and useable after being confirmed."
+ },
+ "emptyState": {
+ "noAppsTitle": "Install an app on my device",
+ "noAppsDesc": "Install apps on your device before adding accounts in Ledger Live. Go to the Manager to install apps.",
+ "noAccountsTitle": "You don't have any accounts…",
+ "noAccountsDesc": "Please add accounts to your Portfolio.",
+ "buttons": {
+ "import": "Add account",
+ "manager": "Install apps",
+ "managerSecondary": "Install apps on my device"
+ }
+ },
+ "noOpState": {
+ "title": "No operations yet?",
+ "desc": "Simply send crypto assets to your receiving address and wait for the app to sync."
+ }
+ },
+ "addAccountsModal": {
+ "ctaAdd": "Add accounts",
+ "ctaImport": "Import Desktop accounts"
+ },
+ "byteSize": {
+ "bytes": "{{size}} bytes",
+ "kbUnit": "{{size}} KB",
+ "mbUnit": "{{size}} MB"
+ },
+ "numberCompactNotation": {
+ "d": "",
+ "K": "k",
+ "M": "m",
+ "B": "bn",
+ "T": "tn",
+ "Q": "qa",
+ "Qn": "qi"
+ },
+ "time": {
+ "day": "1D",
+ "week": "1W",
+ "month": "1M",
+ "year": "1Y",
+ "all": "All",
+ "since": {
+ "day": "past day",
+ "week": "past week",
+ "month": "past month",
+ "year": "past year"
+ }
+ },
+ "orderOption": {
+ "choices": {
+ "name|asc": "Name A-Z",
+ "name|desc": "Name Z-A",
+ "balance|asc": "Lowest Balance",
+ "balance|desc": "Highest Balance"
+ }
+ },
+ "operations": {
+ "types": {
+ "IN": "Received",
+ "NFT_IN": "NFT Received",
+ "OUT": "Sent",
+ "NFT_OUT": "NFT Sent",
+ "CREATE": "Created",
+ "REVEAL": "Revealed",
+ "DELEGATE": "Delegated",
+ "UNDELEGATE": "Undelegated",
+ "REDELEGATE": "Redelegated",
+ "VOTE": "Voted",
+ "FREEZE": "Frozen",
+ "UNFREEZE": "Unfrozen",
+ "REWARD": "Claimed reward",
+ "FEES": "Fees",
+ "OPT_IN": "Opt in",
+ "OPT_OUT": "Opt out",
+ "CLOSE_ACCOUNT": "Close account",
+ "SUPPLY": "Deposited",
+ "REDEEM": "Withdrawn",
+ "APPROVE": "Enabled",
+ "BOND": "Bond",
+ "UNBOND": "Unbond",
+ "REWARD_PAYOUT": "Reward",
+ "SLASH": "Slash",
+ "WITHDRAW_UNBONDED": "Withdrawal",
+ "NOMINATE": "Nomination",
+ "CHILL": "Clear nominations",
+ "SET_CONTROLLER": "Set controller"
+ }
+ },
+ "operationDetails": {
+ "title": "Operation details",
+ "account": "Account",
+ "date": "Date",
+ "confirmed": "Confirmed",
+ "notConfirmed": "Not confirmed",
+ "failed": "Failed",
+ "fees": "Network fees",
+ "noFees": "No fee",
+ "from": "From",
+ "to": "To",
+ "identifier": "Transaction ID",
+ "viewOperation": "View in explorer",
+ "whatIsThis": "What is this operation?",
+ "seeAll": "See all",
+ "seeLess": "See less",
+ "viewInExplorer": "View in explorer",
+ "sending": "Sending...",
+ "receiving": "Receiving...",
+ "tokenOperations": "Token operations",
+ "subAccountOperations": "Subaccount operations",
+ "internalOperations": "Internal operations",
+ "tokenModal": {
+ "desc": "This operation is related to the following token operations"
+ },
+ "details": "{{ currency }} details",
+ "extra": {
+ "resource": "Resource",
+ "frozenAmount": "Frozen amount",
+ "unfreezeAmount": "Unfreeze amount",
+ "address": "Address",
+ "votes": "Votes ({{number}})",
+ "votesAddress": "<0>{{votes}}0> to <2>{{name}}2>",
+ "validators": "Validators",
+ "delegated": "Delegated ({{amount}})",
+ "delegatedTo": "Delegated to",
+ "delegatedAmount": "Delegated amount",
+ "redelegated": "Redelegated ({{amount}})",
+ "redelegatedFrom": "Redelegated from",
+ "redelegatedTo": "Redelegated to",
+ "redelegatedAmount": "Redelegated amount",
+ "undelegated": "Undelegated ({{amount}})",
+ "undelegatedFrom": "Undelegated from",
+ "undelegatedAmount": "Undelegated amount",
+ "rewardFrom": "Reward from",
+ "rewardAmount": "Amount collected",
+ "memo": "Memo",
+ "assetId": "Asset ID",
+ "rewards": "Earned rewards",
+ "bondedAmount": "Bonded Amount",
+ "unbondedAmount": "Unbonded Amount",
+ "withdrawUnbondedAmount": "Withdraw Unbonded Amount",
+ "palletMethod": "Method",
+ "transferAmount": "Transfer Amount",
+ "validatorsCount": "Validators ({{number}})",
+ "storageLimit": "Storage Limit",
+ "gasLimit": "Gas Limit",
+ "id": "Id"
+ },
+ "multipleAddresses": "Why multiple addresses?",
+ "tokenName": "Token Name",
+ "collectionContract": "Token Contract",
+ "tokenId": "Token (NFT) ID",
+ "quantity": "Quantity"
+ },
+ "operationList": {
+ "noOperations": "No Operations",
+ "noMoreOperations": "No other Operations"
+ },
+ "selectableAccountsList": {
+ "deselectAll": "Deselect all",
+ "selectAll": "Select all",
+ "tokenCount": "+1 token",
+ "tokenCount_plural": "+{{count}} tokens",
+ "subaccountCount": "+1 subaccount",
+ "subaccountCount_plural": "+{{count}} subaccounts"
+ },
+ "account": {
+ "tokens": {
+ "contractAddress": "Contract address",
+ "viewInExplorer": "View in explorer",
+ "seeMore": "Display more Tokens",
+ "seeLess": "Display fewer Tokens",
+ "addTokens": "Add token",
+ "howTo": "To add token accounts, you need to <0>receive funds0> using your <1>{{currency}} address1>.",
+ "algorand": {
+ "contractAddress": "Contract address",
+ "viewInExplorer": "View in explorer",
+ "seeMore": "See more ASA",
+ "seeLess": "See less ASA",
+ "addTokens": "Add ASA",
+ "howTo": "You can add assets to your Algorand account.",
+ "addAsa": "Add ASA (Asset)",
+ "howAsaWorks": "How ASA (asset) works?"
+ }
+ },
+ "subaccounts": {
+ "seeMore": "See more subaccounts",
+ "seeLess": "See fewer subaccounts"
+ },
+ "send": "Send",
+ "receive": "Receive",
+ "buy": "Buy",
+ "walletconnect": "WalletConnect",
+ "sell": "Sell",
+ "swap": "Swap",
+ "manage": "Manage",
+ "lastOperations": "Last operations",
+ "emptyState": {
+ "title": "No crypto assets yet?",
+ "desc": "Make sure the <1><0>{{managerAppName}}0>1> app is installed and start receiving.",
+ "descWithBuy": "Make sure the <1><0>{{managerAppName}}0>1> app is installed so you can buy or receive <3><0>{{currencyTicker}}0>3>",
+ "descToken": "Make sure the <1><0>{{managerAppName}}0>1> app is installed and start receiving <3><0>{{currencyTicker}}0>3> and <5><0>{{tokenType}}0> tokens5>",
+ "buttons": {
+ "receiveFunds": "Receive",
+ "buyCrypto": "Buy"
+ }
+ },
+ "settings": {
+ "header": "Account settings",
+ "title": "Edit account",
+ "advancedLogs": "Advanced logs",
+ "accountName": {
+ "title": "Account name",
+ "desc": "Description of the account",
+ "placeholder": "Account name"
+ },
+ "accountUnits": {
+ "title": "Edit units"
+ },
+ "unit": {
+ "title": "Unit",
+ "desc": "Choose the unit to be displayed."
+ },
+ "currency": {
+ "title": "Currency"
+ },
+ "endpointConfig": {
+ "title": "Node",
+ "desc": "The API node to use"
+ },
+ "delete": {
+ "title": "Remove account from the Portfolio",
+ "desc": "Stored data will be removed.",
+ "confirmationTitle": "Are you sure?",
+ "confirmationDesc": "This has no impact on your crypto assets. Existing accounts can always be added afterwards.",
+ "confirmationWarn": "Deleting this account will erase your swap transaction history associated to it."
+ },
+ "archive": {
+ "title": "Archive account",
+ "desc": "This account will be archived."
+ },
+ "advanced": {
+ "title": "Advanced logs",
+ "desc": ""
+ }
+ },
+ "import": {
+ "scan": {
+ "title": "Scan LiveQR code",
+ "descTop": {
+ "line1": "In Ledger Live Desktop, go to",
+ "line2": "Settings > Accounts > Export accounts > Export"
+ },
+ "descBottom": "Please put the LiveQR code within the square."
+ },
+ "result": {
+ "title": "Import accounts",
+ "newAccounts": "New accounts",
+ "updatedAccounts": "Updated accounts",
+ "empty": "Add a new account",
+ "descEmpty": "<0><0>No accounts0>0> were found. Please try again or go back to Setup",
+ "alreadyImported": "Already imported",
+ "noAccounts": "Nothing to import",
+ "unsupported": "Unsupported",
+ "settings": "App settings",
+ "includeGeneralSettings": "Import Desktop settings"
+ },
+ "fallback": {
+ "header": "Import account",
+ "title": "Enable camera",
+ "desc": "Please enable Camera in Settings to scan QR codes.",
+ "buttonTitle": "Go to Settings"
+ }
+ },
+ "availableBalance": "Available balance",
+ "totalSupplied": "Amount deposited",
+ "tronFrozen": "Frozen",
+ "bandwidth": "Bandwidth",
+ "energy": "Energy",
+ "delegatedAssets": "Delegated assets",
+ "undelegating": "Undelegating",
+ "delegation": {
+ "sectionLabel": "Delegation(s)",
+ "addDelegation": "Add delegation",
+ "info": {
+ "title": "Earn rewards",
+ "cta": "Earn rewards"
+ }
+ },
+ "claimReward": {
+ "sectionLabel": "Rewards",
+ "cta": "Claim"
+ },
+ "undelegation": {
+ "sectionLabel": "Undelegation(s)"
+ },
+ "nft": {
+ "receiveNft": "Receive NFT"
+ }
+ },
+ "accounts": {
+ "title": "Accounts",
+ "importNotification": {
+ "message": "Your accounts were imported successfully!"
+ },
+ "row": {
+ "syncPending": "Synchronizing...",
+ "upToDate": "Synchronized",
+ "error": "Error",
+ "queued": "Awaiting",
+ "showTokens": "Show {{length}} token",
+ "showTokens_plural": "Show {{length}} tokens",
+ "showSubAccounts": "Show {{length}} subaccount",
+ "showSubAccounts_plural": "Show {{length}} subaccounts",
+ "hideTokens": "Hide token",
+ "hideTokens_plural": "Hide tokens",
+ "hideSubAccounts": "Hide subaccount",
+ "hideSubAccounts_plural": "Hide subaccounts",
+ "algorand": {
+ "showTokens": "Show {{length}} ASA",
+ "showTokens_plural": "Show {{length}} ASA",
+ "hideTokens": "Hide ASA",
+ "hideTokens_plural": "Hide ASA"
+ }
+ }
+ },
+ "distribution": {
+ "header": "Asset allocation",
+ "list": "Asset allocation ({{count}})",
+ "assets": "asset",
+ "assets_plural": "assets",
+ "total": "Total balance:",
+ "listAccount": "Account allocation ({{count}})"
+ },
+ "help": {
+ "gettingStarted": {
+ "title": "Getting started",
+ "desc": "Start here"
+ },
+ "helpCenter": {
+ "title": "Ledger Support",
+ "desc": "Get help"
+ },
+ "ledgerAcademy": {
+ "title": "Ledger Academy",
+ "desc": "Learn about crypto"
+ },
+ "facebook": {
+ "title": "Facebook",
+ "desc": "Like our page"
+ },
+ "twitter": {
+ "title": "Twitter",
+ "desc": "Follow us"
+ },
+ "github": {
+ "title": "Github",
+ "desc": "Review our code"
+ },
+ "status": {
+ "title": "Ledger Status",
+ "desc": "Check our system status"
+ }
+ },
+ "settings": {
+ "header": "Settings",
+ "resources": "Ledger resources",
+ "display": {
+ "title": "General",
+ "desc": "Configure general Ledger Live settings.",
+ "carousel": "Carousel visibility",
+ "carouselDesc": "Enable visibility of the carousel on Portfolio",
+ "language": "Display language",
+ "languageDesc": "Set the language displayed in Ledger Live.",
+ "region": "Region",
+ "regionDesc": "Choose your region to update formats of dates, time and currencies.",
+ "password": "Password lock",
+ "passwordDesc": "Set a password to protect Ledger Live data on your phone.",
+ "counterValue": "Preferred currency",
+ "theme": "Theme",
+ "themeDesc": "Set the app UI theme",
+ "themes": {
+ "system": "System",
+ "dark": "Dark",
+ "light": "Light"
+ },
+ "counterValueDesc": "Choose the currency for balances and operations.",
+ "exchange": "Rate provider",
+ "exchangeDesc": "Set provider of exchange rate from Bitcoin to {{fiat}}",
+ "stock": "Regional market indicator",
+ "stockDesc": "Choose Western to display market increases in green or Eastern to display them in red.",
+ "reportErrors": "Bug reports",
+ "reportErrorsDesc": "Automatically send reports to help Ledger improve its products.",
+ "developerMode": "Developer mode",
+ "developerModeDesc": "Show developer apps in the Manager and enable Testnet apps.",
+ "analytics": "Analytics",
+ "analyticsDesc": "Enable analytics to help Ledger improve user experience.",
+ "analyticsModal": {
+ "title": "Share analytics",
+ "desc": "Enable analytics to help Ledger improve user experience",
+ "bullet0": "Clicks",
+ "bullet1": "In-app page visits",
+ "bullet2": "Redirect to webpage",
+ "bullet3": "Actions: send, receive, lock, etc",
+ "bullet4": "End of page scroll",
+ "bullet5": "App (un)installation and version",
+ "bullet6": "Number of accounts, currencies and operations",
+ "bullet7": "Overall and page session duration",
+ "bullet8": "Ledger device type and firmware"
+ },
+ "technicalData": "Technical data",
+ "technicalDataDesc": "Ledger will automatically collect anonymized technical data to improve user experience.",
+ "technicalDataModal": {
+ "title": "Technical data",
+ "desc": "Ledger will automatically collect anonymized technical data to improve user experience.",
+ "bullet1": "Anonymous unique application ID",
+ "bullet2": "Ledger Live version, OS region, language and region"
+ },
+ "hideEmptyTokenAccounts": "Hide empty token accounts",
+ "hideEmptyTokenAccountsDesc": "Hide empty token accounts on the Accounts page."
+ },
+ "currencies": {
+ "header": "Currencies",
+ "rateProvider": "Rate Provider ({{currencyTicker}}→BTC)",
+ "rateProviderDesc": "Choose the provider of the rate between {{currencyTicker}} and Bitcoin.",
+ "confirmationNb": "Number of confirmations",
+ "confirmationNbDesc": "Set the number of network confirmations for transactions to be approved.",
+ "currencySettingsTitle": "{{currencyName}} Settings",
+ "placeholder": "No settings for this asset"
+ },
+ "accounts": {
+ "header": "Accounts",
+ "title": "Accounts",
+ "desc": "Manage assets display in the app.",
+ "hideTokenCTA": "Hide token",
+ "showContractCTA": "Show contract",
+ "blacklistedTokens": "Hidden token list",
+ "blacklistedTokensDesc": "You can hide tokens by going to the accounts list then long-pressing on the token and selecting 'Hide token'.",
+ "blacklistedTokensModal": {
+ "title": "Hide token",
+ "desc": "This action will hide all <1><0>{tokenName}0>1> accounts, you can show them again using <3>Settings > Accounts3>.",
+ "confirm": "Hide token"
+ },
+ "cryptoAssets": {
+ "header": "Crypto assets",
+ "title": "Crypto assets",
+ "desc": "Select a crypto assets to edit its settings."
+ }
+ },
+ "about": {
+ "title": "About",
+ "desc": "App information, terms and conditions, and privacy policy.",
+ "appDescription": "With Ledger Live, Secure, Buy, Sell, Exchange, Grow and Manage your crypto. All-in-one place.",
+ "appVersion": "Version",
+ "termsConditions": "Terms and conditions",
+ "termsConditionsDesc": "By using Ledger Live you are deemed to have accepted our terms and conditions.",
+ "privacyPolicy": "Privacy policy",
+ "privacyPolicyDesc": "See what personal data are collected, why and how they are used.",
+ "liveReview": {
+ "title": "Rate the app!",
+ "ios": "Review in the App Store",
+ "android": "Review in the Google Play store"
+ }
+ },
+ "help": {
+ "title": "Help",
+ "header": "Help",
+ "desc": "Learn more about Ledger Live or how to get help.",
+ "support": "Ledger Support",
+ "supportDesc": "If you have a problem, get help using Ledger Live with your hardware wallet.",
+ "configureDevice": "Set up a device",
+ "configureDeviceDesc": "Set up as a new device or restore an existing device. Accounts and settings are preserved.",
+ "clearCache": "Clear cache",
+ "clearCacheDesc": "The transactions on the network will be scanned and your accounts will be recalculated.",
+ "clearCacheModal": "Are you sure?",
+ "clearCacheModalDesc": "A fresh and complete scan of transactions on the network will be done. Account histories will be rebuilt and balances will be recalculated.",
+ "clearCacheButton": "Clear",
+ "exportLogs": "Save logs",
+ "exportLogsDesc": "Saving Ledger Live logs may be needed for troubleshooting purposes.",
+ "hardReset": "Reset Ledger Live",
+ "hardResetDesc": "This has no impact on your assets. Erases all data in Ledger Live, including account data, transaction histories and settings. Use your Ledger device to reload and manage your crypto assets on an empty Ledger Live.",
+ "repairDevice": "Repair your Ledger device",
+ "repairDeviceDesc": "If you have an issue updating your device and cannot resume updates, you can try this to repair your device."
+ },
+ "experimental": {
+ "title": "Experimental features",
+ "desc": "Try out the Experimental features and let us know what you think.",
+ "disclaimer": "These are Experimental features provided on an \"as is\" basis for our community of tech enthusiasts. They may change, break or be removed at any time. By enabling them, you agree to use them at your own risk."
+ },
+ "developer": {
+ "title": "Developer",
+ "desc": "Try out the Developer features and let us know what you think.",
+ "customManifest": {
+ "title": "Load Custom Platform Manifest"
+ }
+ }
+ },
+ "migrateAccounts": {
+ "banner": "Ledger Live accounts update",
+ "overview": {
+ "headerTitle": "Account update",
+ "title": "Ledger Live accounts update",
+ "subtitle": "New features in Ledger Live means your accounts need to be updated",
+ "notice": "{{accountCount}} account could not be updated. Please connect the device associated to the account below.",
+ "notice_plural": "{{accountCount}} accounts could not be updated. Please connect the device associated to the accounts below.",
+ "currency": "1 {{currency}} account needs to be updated",
+ "currency_plural": "{{count}} {{currency}} accounts need to be updated",
+ "start": "Start update",
+ "continue": "Continue update"
+ },
+ "progress": {
+ "headerTitle": "Updating accounts",
+ "pending": {
+ "title": "{{currency}} update in progress",
+ "subtitle": "Please wait for your account to be updated."
+ },
+ "notice": {
+ "title": "{{currency}} update incomplete",
+ "subtitle": "No {{currency}} accounts could be updated using this device.",
+ "cta": "Continue",
+ "ctaNextCurrency": "Continue with {{currency}}"
+ },
+ "done": {
+ "title": "{{currency}} update complete",
+ "subtitle": "Congratulations, your {{currency}} accounts were updated successfully.",
+ "cta": "Continue",
+ "ctaNextCurrency": "Continue with {{currency}}",
+ "ctaDone": "Done"
+ },
+ "error": {
+ "cta": "Retry"
+ }
+ },
+ "connectDevice": {
+ "headerTitle": "Connect device"
+ }
+ },
+ "transfer": {
+ "send": {
+ "title": "Send"
+ },
+ "fees": {
+ "title": "Edit fees"
+ },
+ "receive": {
+ "title": "Receive",
+ "titleReadOnly": "Unverified address",
+ "headerTitle": "Crypto asset",
+ "titleDevice": "Connect device",
+ "verifySkipped": "Your receive address was not confirmed on your Ledger device. Please verify your {{accountType}} address to stay secure.",
+ "verifyPending": "Please verify that the {{currencyName}} address displayed on your device exactly matches the one displayed on your phone.",
+ "verified": "Address confirmed. Check again if you copied or scanned it.",
+ "verifyAgain": "Check again",
+ "noAccount": "No account found",
+ "address": "Address for account",
+ "copyAddress": "Copy address",
+ "shareAddress": "Share address",
+ "addressCopied": "Address copied!",
+ "taprootWarning": "Make sure the sender supports taproot",
+ "notSynced": {
+ "text": "Synchronizing",
+ "desc": "This may take a moment if you have many transactions or if you have a slow internet connection."
+ },
+ "readOnly": {
+ "title": "Receive",
+ "text": "Please be careful",
+ "desc": "You are about to view an address which was not verified. Verify addresses on your device to stay secure.",
+ "verify": "The address of {{accountType}} was not verified. Verify addresses on your device to stay secure."
+ }
+ },
+ "exchange": {
+ "title": "Buy / Sell"
+ },
+ "swap": {
+ "title": "Swap",
+ "selectDevice": "Select your device",
+ "broadcasting": "Broadcasting swap",
+ "loadingFees": "Loading network fees...",
+ "landing": {
+ "header": "Swap",
+ "title": "Welcome to Swap",
+ "whatIsSwap": "What is Swap?",
+ "disclaimer": "Exchange crypto directly from your Ledger device. This service is not available in some countries, including the US."
+ },
+ "unauthorizedRates": {
+ "cta": "Reset verification",
+ "banner": "Reset your KYC and update Live to swap with Wyre.",
+ "bannerCTA": "Reset KYC"
+ },
+ "main": {
+ "header": "Swap"
+ },
+ "kyc": {
+ "disclaimer": "I acknowledge that my location data will be shared with third parties for compliance purposes.",
+ "cta": "Continue",
+ "wyre": {
+ "title": "Complete your KYC",
+ "subtitle": "Your information is collected by LEDGER on behalf of and transferred to WYRE for KYC purposes. For more information, please check our Privacy Policy",
+ "pending": {
+ "cta": "Continue",
+ "title": "Your information has been submitted for approval",
+ "subtitle": "It usually takes only a few minutes before you can start swapping",
+ "link": "Learn more about KYC"
+ },
+ "approved": {
+ "cta": "Continue",
+ "title": "KYC approved!",
+ "subtitle": "Your KYC has been submitted and has been approved.",
+ "link": "Learn more about KYC"
+ },
+ "closed": {
+ "cta": "Reset KYC",
+ "title": "KYC application rejected",
+ "subtitle": "Wyre rejected the data you provided for KYC purposes",
+ "link": "Learn more about KYC"
+ },
+ "form": {
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "street1": "Street Address Line 1",
+ "street2": "Street Address Line 2",
+ "city": "City",
+ "state": "State",
+ "country": "Country",
+ "postalCode": "Zip Code",
+ "dateOfBirth": "Date of birth",
+
+ "firstNamePlaceholder": "Enter your first name",
+ "lastNamePlaceholder": "Enter your last name",
+ "street1Placeholder": "Eg. 13, Maple street",
+ "street2Placeholder": "Eg. 13, Maple street",
+ "cityPlaceholder": "Eg. San José",
+ "postalCodePlaceholder": "Enter your 5 digit Zip code",
+ "statePlaceholder": "Select your state",
+ "dateOfBirthPlaceholder": "YYYY-MM-DD",
+
+ "firstNameError": "Enter your first name to continue",
+ "lastNameError": "Enter your last name to continue",
+ "street1Error": "Enter your address",
+ "cityError": "Enter the city you live in the USA",
+ "stateError": "Select your state to continue",
+ "postalCodeError": "Enter a valid US ZIP code",
+ "dateOfBirthError": "Enter your date of birth"
+ }
+ },
+ "states": "Select your state"
+ },
+ "notAvailable": {
+ "title": "Service temporarily unavailable, or not available in your country"
+ },
+ "payoutModal": {
+ "title": "Payout fees",
+ "description": "Payout fees are not shown on the device and substracted from the amount.",
+ "cta": "Close"
+ },
+ "pendingOperation": {
+ "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your swapped {{targetCurrency}} assets",
+ "label": "Your Swap ID:",
+ "title": "Pending operation",
+ "disclaimer": "Take note of your Swap ID number in case you’d need assistance from {{provider}} support.",
+ "cta": "See details"
+ },
+ "tradeMethod": {
+ "float": "Floating rate",
+ "floatUnavailable": "Floating rate not supported for this pair",
+ "fixed": "Fixed rate",
+ "fixedUnavailable": "Fixed rate not supported for this pair",
+ "floatDesc": "Your amount could change depending on the market conditions.",
+ "fixedDesc": "Your amount will remain the same irrespective of the changes on the market. Fixed rate updates every 30 seconds",
+ "by": "By",
+ "modalTitle": "Floating or fixed rate?"
+ },
+ "form": {
+ "validate": "Confirm Swap transaction",
+ "tab": "Exchange",
+ "button": "Continue",
+ "from": "From",
+ "to": "To",
+ "source": "Source",
+ "target": "Target account",
+ "noAccount": "You don’t have {{currency}} account",
+ "balance": "Balance <0>1230>",
+ "fromAccount": "Select account",
+ "toAccount": "Select account",
+ "paraswapCTA": "Looking for Paraswap? It has been moved to the Discover tab!",
+ "quote": "Quote",
+ "receive": "Receive",
+ "amount": {
+ "useMax": "Use max",
+ "available": "Total available"
+ },
+ "noAsset": {
+ "title": "You have no asset to swap",
+ "desc": "Buy some and come back to swap"
+ },
+ "noApp": {
+ "title": "{{appName}} app not installed",
+ "desc": "Please go to Manager to install the {{appName}} app.",
+ "cta": "Go to Manager",
+ "close": "Close"
+ },
+ "noAccounts": {
+ "title": "No {{ticker}} to swap",
+ "desc": "Your {{currencyName}} accounts have no balance to swap.",
+ "addAccountCta": "Add Account",
+ "cta": "Close"
+ },
+ "outdatedApp": {
+ "title": "App update available",
+ "desc": "You need to update the {{appName}} app.",
+ "cta": "Go to Manager",
+ "close": "Close"
+ },
+ "summary": {
+ "from": "From",
+ "to": "To",
+ "send": "Send",
+ "payoutNetworkFees": "Payout fees",
+ "payoutNetworkFeesTooltip": "This amount will not be shown on your device",
+ "receive": "Amount",
+ "receiveFloat": "Amount to receive before service fees",
+ "provider": "Provider",
+ "method": "Rate",
+ "fees": "Max fees",
+ "disclaimer": {
+ "title": "Terms and conditions",
+ "desc": "By clicking “Accept”, I acknowledge and accept that this service is exclusively governed by <0>{{provider}}0>'s Terms & Conditions.",
+ "tos": "Terms & conditions",
+ "accept": "Accept",
+ "reject": "Close"
+ }
+ }
+ },
+ "operationDetails": {
+ "swapId": "Swap ID",
+ "provider": "Provider",
+ "date": "Date",
+ "from": "From",
+ "fromAmount": "Amount sent",
+ "to": "To",
+ "toAmount": "Amount to receive",
+ "statusTooltips": {
+ "expired": "Please contact the swap provider with your swap ID for more information.",
+ "refunded": "Please contact the swap provider with your swap ID for more information.",
+ "pending": "Please wait while the swap provider is processing the transaction.",
+ "onhold": "Please contact the swap provider with your swap ID to solve the situation.",
+ "finished": "Your swap was completed successfully."
+ }
+ },
+ "missingApp": {
+ "title": "Please install the Exchange app on your device",
+ "description": "Go to manager and install Exchange app to swap assets",
+ "button": "Go to Manager"
+ },
+ "outdatedApp": {
+ "title": "Please update the Exchange app on your device",
+ "description": "Go to manager and update Exchange app to swap assets",
+ "button": "Go to Manager"
+ },
+ "emptyState": {
+ "title": "No {{currency}} account",
+ "description": "You need to add an account before swapping {{currency}}",
+ "CTAButton": "Add account"
+ },
+ "history": {
+ "tab": "History",
+ "disclaimer": "Your swap mobile transactions are not synchronized with Ledger Live Desktop",
+ "exportButton": "Export operations",
+ "exportFilename": "Swap operations",
+ "empty": {
+ "title": "Your previous swaps will appear here",
+ "desc": "Either you have not made any swaps yet, or Ledger Live has been reset in the meantime."
+ }
+ }
+ },
+ "swapv2": {
+ "form": {
+ "summary": {
+ "from": "From",
+ "to": "To",
+ "send": "Send",
+ "payoutNetworkFees": "Payout fees",
+ "payoutNetworkFeesTooltip": "This amount will not be shown on your device",
+ "receive": "Amount",
+ "receiveFloat": "Amount to receive before service fees",
+ "provider": "Provider",
+ "method": "Rate",
+ "fees": "Fees"
+ }
+ }
+ },
+ "lending": {
+ "title": "Lend crypto",
+ "titleTransferTab": "Lend",
+ "actionTitle": "Lend",
+ "accountActions": {
+ "approve": "Approve",
+ "supply": "Supply",
+ "withdraw": "Withdraw"
+ },
+ "highFees": {
+ "title": "High fees on Ethereum",
+ "description": "Due to congestion on the Ethereum network, you may experience high fees when issuing transactions."
+ },
+ "account": {
+ "amountSupplied": "Amount deposited",
+ "amountSuppliedTooltip": "Amount lent to the network",
+ "currencyAPY": "Currency APY",
+ "currencyAPYTooltip": "Yearly return rate of a deposit that’s continuously compounded",
+ "accruedInterests": "Interest balance",
+ "accruedInterestsTooltip": "Interest generated on your lent assets",
+ "interestEarned": "Interest earned",
+ "interestEarnedTooltip": "Interest you have earned after withdrawal",
+ "openLoans": "Open loans",
+ "closedLoans": "Closed loans"
+ },
+ "banners": {
+ "needApproval": "You need to approve this account before being able to lend assets.",
+ "fullyApproved": "You have fully approved this account. You can reduce the amount at a fee.",
+ "approvedCanReduce": "You have approved <0>{{value}}0> on this account. You can reduce the amount at a fee.",
+ "approvedButNotEnough": "You have approved <0>{{value}}0> on this account.",
+ "approving": "You can deposit assets once the account approval is confirmed.",
+ "notEnough": "You must increase the limit approved on your account to lend."
+ },
+ "howDoesLendingWork": "How does lending works",
+ "dashboard": {
+ "tabTitle": "Dashboard",
+ "assetsTitle": "Assets to lend",
+ "accountsTitle": "Approved accounts",
+ "emptySateDescription": "You can lend assets directly from your Ethereum accounts and earn interest.",
+ "apy": "{{value}} APY",
+ "activeAccount": {
+ "account": "Account",
+ "amountSupplied": "Amount deposited",
+ "interestEarned": "Interest earned",
+ "status": "Account status",
+ "EARNING": "Earning",
+ "ENABLING": "Approving",
+ "INACTIVE": "Inactive",
+ "SUPPLYING": "Supplying",
+ "approve": "Approve",
+ "supply": "Supply",
+ "withdraw": "Withdraw",
+ "amountRedeemed": "Amount withdrawn",
+ "endDate": "End date"
+ }
+ },
+ "closedLoans": {
+ "tabTitle": "Closed loans",
+ "description": "View all your withdrawn loans and the interest you have earned.",
+ "cta": "Lend asset"
+ },
+ "history": {
+ "tabTitle": "History",
+ "description": "View the history of all your loan transactions.",
+ "cta": "Lend asset"
+ },
+ "terms": {
+ "label": "Lend crypto",
+ "title": "Lend assets on the Compound Protocol",
+ "description": "The Compound protocol allows you to lend and borrow assets on the Ethereum network. You can lend assets and earn interest directly from your Ledger acccount.",
+ "switchLabel": "I have read and agree with the <0><0>Terms of Use0>0>."
+ },
+ "info": {
+ "1": {
+ "label": "Step 1/3",
+ "title": "Approving an account allows the protocol to process future loans.",
+ "description": "You need to authorize the Compound smart contract to transfer up to a certain amount of assets to the protocol. Enabling an account gives permission to the protocol to process future loans."
+ },
+ "2": {
+ "label": "Step 2/3",
+ "title": "Deposit assets to earn interest.",
+ "description": "Once an account is approved, you can select the amount of assets you want to lend and issue a transaction to the protocol. Interest accrue immediately after the transaction is confirmed."
+ },
+ "3": {
+ "label": "Step 3/3",
+ "title": "Withdraw assets at any time.",
+ "description": "You can withdraw your assets and earned interest at any time, partially or entirely, directly from your Ledger account.",
+ "cta": "Lend now"
+ },
+ "title": "Lend crypto"
+ },
+ "noTokenAccount": {
+ "info": {
+ "title": "You don’t have a {{ name }} account.",
+ "description": "In order to deposit funds and lend crypto you need a {{ name }} account. Please Receive funds on your Ethereum address."
+ },
+ "buttons": {
+ "receive": "Receive {{ name }}",
+ "buy": "Buy {{ name }}"
+ }
+ },
+ "enable": {
+ "info": {
+ "title": "This account is not approved",
+ "description": "You need to approve an account before being able to lend this asset.",
+ "cta": "Approve Account"
+ },
+ "stepperHeader": {
+ "selectAccount": "Choose an account",
+ "enable": "Approve",
+ "advanced": "Advanced",
+ "amount": "Enter amount",
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "selectAccount": {
+ "enabledAccountsAmount": "You have {{number}} account that approved {{amount}}",
+ "enabledAccountsAmount_plural": "You have {{number}} accounts that approved {{amount}}",
+ "enabledAccountsNoLimit": "You have {{number}} account that approved an unlimited amount of {{ currency }}",
+ "enabledAccountsNoLimit_plural": "You have {{number}} accounts that approved an unlimited amount of {{ currency }}",
+ "noEnabledAccounts": "You need to approve an account before you can lend."
+ },
+ "enable": {
+ "summary": "<0>I grant the 0><1>{{contractName}}1><0> smart contract access to my 0><1>{{accountName}}1><0> account for a 0><1>{{amount}}1><0> amount0>",
+ "limit": "limited {{amount}}",
+ "noLimit": "no limit {{assetName}}",
+ "contractName": "Compound {{currencyName}}",
+ "advanced": "Advanced"
+ },
+ "advanced": {
+ "amountLabel": "Amount to approve",
+ "amountLabelTooltip": "This limits the amount available to the smart contract.",
+ "limit": "Limit",
+ "limited": "Limited",
+ "noLimit": "No Limit"
+ },
+ "amount": {
+ "totalAvailable": "Total available"
+ },
+ "validation": {
+ "success": "Operation sent successfully",
+ "info": "The approval was sent to the network for confirmation. You’ll be able to issue loans once it is confirmed.",
+ "extraInfo": "Transactions can take some time to be displayed in an explorer and to be confirmed.",
+ "button": {
+ "done": "Close"
+ }
+ }
+ },
+ "supply": {
+ "stepperHeader": {
+ "amount": "Supply",
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "amount": {
+ "totalAvailable": "Available balance",
+ "placeholderMax": "Withdraw Max"
+ },
+ "validation": {
+ "success": "Deposit sent successfully",
+ "info": "You will start earning interest once the network has confirmed the deposit.",
+ "extraInfo": "Transactions can take some time to be displayed in an explorer and to be confirmed.",
+ "button": {
+ "done": "Done",
+ "cta": "View details"
+ }
+ }
+ },
+ "withdraw": {
+ "stepperHeader": {
+ "amount": "Withdraw",
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "validation": {
+ "success": "Withdrawal sent successfully",
+ "info": "Your assets will be available once the network has confirmed the withdrawal.",
+ "button": {
+ "done": "Done",
+ "cta": "View details"
+ }
+ }
+ }
+ }
+ },
+ "sync": {
+ "error": "Sync Error",
+ "loading": "Sync"
+ },
+ "scanning": {
+ "loading": "Searching devices..."
+ },
+ "send": {
+ "tooMuchUTXOBottomModal": {
+ "cta": "Continue",
+ "description": "This transaction may take long to verify and sign because the account has a significant number of coins.",
+ "title": "UTXO message"
+ },
+ "highFeeModal": "Be careful, network fees are greater than <1>10%1> of the amount. Do you want to continue?",
+ "scan": {
+ "title": "Scan QR code",
+ "descBottom": "Please center the QR code inside the square.",
+ "fallback": {
+ "header": "Scan QR code",
+ "title": "Enable camera",
+ "desc": "Please enable Camera in Settings to scan QR codes.",
+ "buttonTitle": "Go to Settings"
+ }
+ },
+ "stepperHeader": {
+ "selectAccount": "Account to debit",
+ "recipientAddress": "Recipient address",
+ "selectAmount": "Amount",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}",
+ "quantity": "Quantity",
+ "selectCollection": "Collection",
+ "selectNft": "NFT"
+ },
+ "recipient": {
+ "scan": "Scan QR code",
+ "enterAddress": "Enter address",
+ "input": "Enter address",
+ "verifyAddress": "Please verify the address matches the one shared by the recipient."
+ },
+ "amount": {
+ "available": "Total available",
+ "useMax": "Use max",
+ "loadingNetwork": "Loading network fees...",
+ "noRateProvider": "Not available",
+ "quantityAvailable": "Quantity available"
+ },
+ "summary": {
+ "subaccountsWarning": "You will need to refill this account with {{ currency }} in order to send the tokens of this account",
+ "tag": "Tag (Optional)",
+ "validateTag": "Validate tag",
+ "total": "Total",
+ "amount": "Amount",
+ "from": "From",
+ "to": "To",
+ "infoTotalTitle": "Total debit",
+ "infoTotalDesc": "Includes transaction amount and the selected network fees",
+ "gasLimit": "Gas limit",
+ "gasPrice": "Gas price",
+ "maxFees": "Max fees",
+ "validateGasLimit": "Validate gas limit",
+ "fees": "Fees",
+ "validateFees": "Validate Fees",
+ "customizeFees": "Customize Fees",
+ "memo": {
+ "title": "Memo",
+ "type": "Memo type",
+ "value": "Memo value"
+ },
+ "validateMemo": "Validate memo",
+ "quantity": "Quantity"
+ },
+ "validation": {
+ "message": "On your device, confirm the transaction to sign it securely.",
+ "sent": "Transaction sent",
+ "amount": "Amount",
+ "fees": "Fees",
+ "confirm": "Your account balance will be updated once the network confirms the transaction.",
+ "button": {
+ "details": "View details",
+ "retry": "Retry"
+ }
+ },
+ "fees": {
+ "title": "Network fees",
+ "validate": "Confirm",
+ "required": "Fees are required",
+ "chooseGas": "Choose a gas price",
+ "higherFaster": "Higher gas price means a faster confirmation.",
+ "edit": {
+ "title": "Choose unit"
+ },
+ "networkInfo": "Choose the amount of network fees to include in the transaction. The amount of fees affects the processing speed of the transaction"
+ },
+ "verification": {
+ "streaming": {
+ "accurate": "Loading... ({{percentage}})",
+ "inaccurate": "Loading..."
+ }
+ },
+ "info": {
+ "maxSpendable": {
+ "title": "Max spendable amount",
+ "description": "The maximum spendable amount is the total balance in an account that is available to send in a transaction."
+ }
+ }
+ },
+ "requestAccount": {
+ "stepperHeader": {
+ "selectCrypto": "Select crypto",
+ "selectAccount": "Select account",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "selectAccount": {
+ "addAccount": "Add {{currency}} account"
+ }
+ },
+ "freeze": {
+ "stepperHeader": {
+ "info": "Earn rewards",
+ "selectAmount": "Freeze",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "info": {
+ "description": "Freeze TRX to earn rewards securely, keeping control of your assets.",
+ "steps": {
+ "0": "Delegated assets remain yours.",
+ "1": "You can unfreeze your assets after 3 days.",
+ "2": "Freeze and vote securely with your Ledger device."
+ },
+ "howVotingWorks": "How voting works",
+ "cta": "Continue"
+ },
+ "amount": {
+ "available": "Total available",
+ "noRateProvider": "Not available",
+ "infoLabel": "Bandwidth or Energy"
+ },
+ "validation": {
+ "message": "Always verify that your device displays the address exactly as it was originally given to you.",
+ "success": "Assets frozen",
+ "amount": "Amount",
+ "info": "You will start earning {{resource}} once the network has confirmed the freeze. You can shortly vote for Super Representatives to also earn rewards.",
+ "button": {
+ "pending": "Transaction is being validated.",
+ "pendingDesc": "Please wait a moment before voting.",
+ "vote": "Vote",
+ "voteTimer": "0:{{time}}",
+ "later": "I will vote later",
+ "retry": "Retry"
+ }
+ }
+ },
+ "unfreeze": {
+ "stepperHeader": {
+ "selectAmount": "Unfreeze",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "amount": {
+ "title": "Select the type of asset to unfreeze",
+ "info": "Unfreezing will decrease your {{resource}} and will cancel all your votes.",
+ "cta": "Continue"
+ },
+ "validation": {
+ "success": "Your assets were unfrozen successfully",
+ "info": "Your {{resource}} points will be decreased and all your votes will be canceled.",
+ "button": {
+ "done": "Done",
+ "cta": "See details"
+ }
+ }
+ },
+ "claimReward": {
+ "stepperHeader": {
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "validation": {
+ "success": "Your rewards were added to the available balance.",
+ "button": {
+ "done": "Done",
+ "cta": "See details"
+ }
+ }
+ },
+ "vote": {
+ "stepperHeader": {
+ "selectValidator": "Cast votes",
+ "castVote": "My votes",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "selectValidator": {},
+ "castVotes": {
+ "ranking": "Ranking: <0>{{rank}}0>",
+ "nbOfVotes": "Number of votes {{amount}}",
+ "percentage": "Percentage",
+ "estYield": "Est. yield",
+ "addMoreVotes": "Add more votes",
+ "votesRemaining": "Votes remaining: <0>{{total}}0>",
+ "maxVotesAvailable": "Max votes available: <0>{{total}}0>",
+ "voteFor": "Vote for",
+ "validateVotes": "Validate votes",
+ "votesRequired": "Votes required ",
+ "allVotesUsed": "All votes used"
+ },
+ "validation": {
+ "message": "Always verify that your device displays the address exactly as it was originally given to you.",
+ "success": "Your votes were cast successfully",
+ "info": "",
+ "button": {}
+ }
+ },
+ "addAccounts": {
+ "supportLinks": {
+ "segwit_or_native_segwit": "Segwit or Native segwit?"
+ },
+ "quitConfirmation": {
+ "title": "Cancel add account",
+ "desc": "Are you sure you want to cancel adding accounts?"
+ },
+ "imported": "Accounts added successfully",
+ "sections": {
+ "importable": {
+ "title": "Add existing account"
+ },
+ "creatable": {
+ "title": "Add new account"
+ },
+ "imported": {
+ "title": "Accounts already in the Portfolio ({{length}})"
+ },
+ "migrate": {
+ "title": "Accounts to update"
+ }
+ },
+ "success": {
+ "desc": "View your accounts or add other accounts",
+ "secondaryCTA": "Add other accounts",
+ "cta": "Go to Accounts"
+ },
+ "stopScanning": "Stop scanning",
+ "retryScanning": "Retry scanning",
+ "retry": "Retry",
+ "done": "Done",
+ "finalCta": "Continue",
+ "finalCtaForSwap": "Go back to Swap",
+ "synchronizing": "Synchronizing",
+ "synchronizingDesc": "Your accounts are being synchronized. This can take a while...",
+ "noAccountToCreate": "Could not create <1><0>{{currencyName}}>> account. Restart the process and sync your accounts.",
+ "cantCreateAccount": "A new account cannot be added before receiving assets on your <1><0>{{accountName}}>> account.",
+ "tokens": {
+ "title": "Add token",
+ "createParentCurrencyAccount": "Add {{parrentCurrencyName}} account",
+ "erc20": {
+ "title": "Add token",
+ "disclaimer": "{{tokenName}} is an ERC20 Token.\nYou can receive tokens directly in an Ethereum account.",
+ "learnMore": "Learn more about ERC20"
+ },
+ "trc10": {
+ "title": "Add Token",
+ "disclaimer": "{{tokenName}} is a TRC10 Token.\nYou can receive tokens directly in a Tron account.",
+ "learnMore": "Learn more about TRC10"
+ },
+ "trc20": {
+ "title": "Add Token",
+ "disclaimer": "{{tokenName}} is a TRC20 Token.\nYou can receive tokens directly in a Tron account.",
+ "learnMore": "Learn more about TRC20"
+ }
+ },
+ "showMoreChainType": "More address types",
+ "addressTypeInfo": {
+ "title": "Which address type to choose?",
+ "subtitle": "Add new account",
+ "native_segwit": {
+ "title": "Native Segwit",
+ "desc": "Best overall. Low fees per transaction, error detection and Lightning Network support. Some wallets/exchanges may need to add support."
+ },
+ "segwit": {
+ "title": "Segwit",
+ "desc": "Higher fees per transaction than Native Segwit. Lightning support. Supported by most wallets."
+ },
+ "legacy": {
+ "title": "Legacy",
+ "desc": "{{currency}}’s original address format. Highest fees per transaction. Some wallets/exchanges still only support this type."
+ },
+ "taproot": {
+ "title": "Taproot",
+ "desc": "Latest {{currency}} network upgrade. Will provide better privacy and cheaper fees once widespread. Still limited support from wallet and exchanges."
+ }
+ }
+ },
+ "DeviceAction": {
+ "stayInTheAppPlz": "Stay in the app and keep your Nano X nearby",
+ "allowAppPermission": "Open the {{wording}} app on your device",
+ "allowAppPermissionSubtitleToken": "to manage your {{token}} tokens",
+ "allowManagerPermission": "Allow {{wording}} on your device",
+ "loading": "Loading...",
+ "turnOnAndUnlockDevice": "Turn on and unlock your device",
+ "connectAndUnlockDevice": "Connect and unlock your device",
+ "unlockDevice": "Unlock your device",
+ "outdated": "App version outdated",
+ "outdatedDesc": "An important update is available for the {{appName}} application on your device. Please go to the Manager to update it.",
+ "quitApp": "Quit the application on your device",
+ "appNotInstalled": "Please install the {{appName}} app",
+ "appNotInstalled_plural": "Please install the {{appName}} apps",
+ "useAnotherDevice": "Use another device",
+ "verifyAddress": {
+ "title": "Verify address on device",
+ "description": "Please verify that the {{currencyName}} address to be shown in Ledger Live matches the one on your Ledger device."
+ },
+ "confirmSwap": {
+ "title": "Confirm swap operation on device",
+ "alert": "Verify the Swap details on your device before sending it. The addresses are exchanged securely so you don’t have to verify them."
+ },
+ "confirmSell": {
+ "title": "Confirm sell operation on device",
+ "alert": "Verify the Sell details on your device before sending it. The addresses are exchanged securely so you don’t have to verify them."
+ },
+ "button": {
+ "openManager": "Open Manager",
+ "openOnboarding": "Setup device"
+ },
+ "installApp": "{{appName}} App installation {{percentage}}",
+ "installAppDescription": "Please wait until the installation is finished",
+ "listApps": "Checking App dependencies",
+ "listAppsDescription": "Please wait while we make sure you have all the required apps"
+ },
+ "SelectDevice": {
+ "title": "Pair new device",
+ "bluetooth": {
+ "title": "Connect via Bluetooth...",
+ "label": "Detect your Nano automatically"
+ },
+ "deviceNotFoundPairNewDevice": "Add new Ledger Nano X",
+ "headerDescription": "Please make sure your {{productName}} is unlocked with Bluetooth enabled",
+ "usb": "... or connect USB cable",
+ "usbLabel": "Connect USB cable and enter your PIN Code on your device",
+ "withoutDeviceHeader": "I don't have my device with me",
+ "withoutDevice": "Continue without my device",
+ "steps": {
+ "connecting": {
+ "title": "Connecting {{deviceName}}",
+ "description": {
+ "ble": "Please make sure your {{productName}} is unlocked and within range",
+ "usb": "Please make sure your {{productName}} is unlocked"
+ }
+ },
+ "genuineCheck": {
+ "title": "Enable Ledger Manager on your {{productName}}"
+ },
+ "genuineCheckPending": {
+ "title": "Checking if the device is genuine..."
+ },
+ "dashboard": {
+ "title": "Return to the Dashboard on your {{productName}}"
+ },
+ "currencyApp": {
+ "title": "Open the {{managerAppName}} app on your {{productName}}",
+ "description": "",
+ "footer": {
+ "appInstalled": "You don't have the app installed",
+ "goManager": "Go to the Manager"
+ }
+ },
+ "accountApp": {
+ "title": "Open the {{managerAppName}} app on your {{productName}}",
+ "description": ""
+ },
+ "receiveVerify": {
+ "title": "Verify address on device",
+ "description": "Please verify that the {{currencyName}} address displayed in Ledger Live exactly matches the one displayed on your Ledger device.",
+ "action": "Continue"
+ },
+ "getDeviceName": {
+ "title": "Press both buttons together to enable reading the device name."
+ },
+ "editDeviceName": {
+ "title": "Press both buttons together to enable setting the device name."
+ },
+ "listApps": {
+ "title": "Loading..."
+ }
+ }
+ },
+ "EditDeviceName": {
+ "title": "Rename device",
+ "charactersRemaining": "{{remainingCount}} characters remaining",
+ "action": "Confirm"
+ },
+ "PairDevices": {
+ "Paired": {
+ "title": "Pairing successful",
+ "desc": "Your {{productName}} is ready to be used with Ledger Live.",
+ "action": "Continue"
+ },
+ "Pairing": {
+ "step1": "Validate if the code displayed on your phone exactly matches the one displayed on your {{productName}}.",
+ "step2": "Validate on your {{productName}} by pressing both buttons together."
+ },
+ "GenuineCheck": {
+ "title": "Device authentication check",
+ "accept": "Please don't turn off your Nano X. Make sure you allow <1>Ledger Manager1>."
+ },
+ "ScanningHeader": {
+ "title": "Looking for devices",
+ "desc": "Please make sure your {{productName}} is unlocked and Bluetooth is enabled."
+ },
+ "ScanningTimeout": {
+ "title": "Sorry, no device was found",
+ "desc": "Please make sure your {{productName}} is unlocked and Bluetooth is enabled."
+ },
+ "bypassGenuine": "Use anyway",
+ "alreadyPaired": "Already paired"
+ },
+ "DeviceItemSummary": {
+ "genuine": "Your device is genuine",
+ "genuineFailed": "Device authentication <1><0>failed0>1>"
+ },
+ "DeviceNameRow": {
+ "title": "Name",
+ "action": "Get device name"
+ },
+ "RemoveDevice": {
+ "button": {
+ "title": "Remove {{nbDevices}} device",
+ "title_plural": "Remove {{nbDevices}} devices"
+ }
+ },
+ "manager": {
+ "tabTitle": "Device Manager",
+ "title": "Manager",
+ "connect": "Select your device",
+ "appsCatalog": "App catalog",
+ "installedApps": "Installed apps",
+ "noAppNeededForToken": "Install {{appName}} app for {{tokenName}}",
+ "tokenAppDisclaimer": "{{tokenName}} is an {{tokenType}} token using the {{appName}} app. To manage {{tokenName}}, <1>install the {{appName}} app1> and send the tokens <3>to your {{appName}} account3>.",
+ "tokenAppDisclaimerInstalled": "{{tokenName}} is an {{tokenType}} token using the {{appName}} app. To manage {{tokenName}}, <1>open the {{appName}} app1> and send the tokens <3>to your {{appName}} account3>.",
+ "goToAccounts": "Go to Accounts",
+ "intallParentApp": "Install {{appName}} app",
+ "readOnly": {
+ "title": "Nano X",
+ "description": "Setup Ledger Live with your Ledger Nano X to install apps, create accounts and make secure transactions wherever you go.",
+ "question": "Do you have your Ledger Nano X?",
+ "button": "Start Nano X setup",
+ "noDevice": "No device?",
+ "buy": "Buy a Ledger Nano X"
+ },
+ "appList": {
+ "title": "App catalog",
+ "loading": "Loading apps...",
+ "noApps": "No apps found",
+ "searchAppsCatalog": "Search app in catalogue...",
+ "searchAppsInstalled": "Search installed apps...",
+ "noAppsInstalled": "No apps installed on your device",
+ "noAppsDescription": "Go to the App catalog to install apps",
+ "noResultsFound": "No results found",
+ "noResultsDesc": "Please verify the spelling and try again",
+ "versionNew": "(NEW{{newVersion}})"
+ },
+ "uninstall": {
+ "title": "Uninstall all",
+ "subtitle": "Uninstall all apps?",
+ "description": "Uninstalling apps has no impact on your crypto assets. You can reinstall apps in the App catalog."
+ },
+ "remove": {
+ "title": "Remove device",
+ "description": "Are you sure? You can add your {{productName}} again at any time.",
+ "button": "Remove"
+ },
+ "storage": {
+ "title": "Storage",
+ "used": "Used",
+ "genuine": "Your device is genuine",
+ "appsInstalled": "App <0>{{number}}0>",
+ "appsInstalled_plural": "Apps <0>{{number}}0>",
+ "storageAvailable": "available"
+ },
+ "installSuccess": {
+ "title": "App installed successfully, you can now add your {{app}} accounts",
+ "title_plural": "Apps installed successfully, you can now add your accounts",
+ "notSupported": "App installed successfully, find out more about the installed apps on our website.",
+ "manageAccount": "Add accounts",
+ "learnMore": "Learn more",
+ "later": "Do it later"
+ },
+ "firmware": {
+ "latest": "Firmware version {{version}} is available",
+ "outdated": "Device firmware version too old to be updated. Please contact Ledger Support for a replacement.",
+ "modalTitle": "Firmware update only available on Ledger Live Desktop",
+ "modalDesc": "Please download Ledger Live on your computer to update the device firmware."
+ }
+ },
+ "ManagerDevice": {
+ "title": "Device"
+ },
+ "AppAction": {
+ "install": {
+ "loading": {
+ "title": "Installing {{appName}}",
+ "desc": "Please wait for the {{appName}} app to be installed",
+ "button": "Installing...",
+ "button_plural": "Installing... {{progressPercentage}}%"
+ },
+ "done": {
+ "title": "Successfully installed {{appName}} on your {{productName}}",
+ "accounts": "Go to Accounts"
+ },
+ "dependency": {
+ "title": "{{dependency}} app is needed",
+ "description_one": "The {{dependency}} app will also be installed because the {{app}} app uses it.",
+ "description_two": "Please press Continue to install the {{app}} and {{dependency}} apps."
+ },
+ "continueInstall": "Install apps"
+ },
+ "update": {
+ "title": "{{number}} app update",
+ "title_plural": "{{number}} app updates",
+ "step": "Update step {{step}}:",
+ "updateWarn": "Do not quit the Manager during the update.",
+ "progress": "Updating all...",
+ "button": "Update all",
+ "version": "New {{version}}",
+ "buttonAction": "Update available",
+ "buttonModal": "Update all apps",
+ "loading": "Updating...",
+ "titleModal": "Updates available"
+ },
+ "uninstall": {
+ "loading": {
+ "title": "Uninstalling {{appName}}",
+ "button": "Uninstalling..."
+ },
+ "done": {
+ "title": "Successfully uninstalled {{appName}} on your {{productName}}"
+ },
+ "dependency": {
+ "title": "Uninstall {{app}} and related apps?",
+ "showAll": "Show apps to uninstall",
+ "description": "Apps on your device that need the {{app}} app will be uninstalled too."
+ },
+ "continueUninstall": "Uninstall {{app}} and other apps"
+ },
+ "filter": {
+ "title": "Show",
+ "all": "All",
+ "installed": "Apps",
+ "not_installed": "Not installed",
+ "supported": "Live supported",
+ "updatable": "Updatable",
+ "apply": "Apply"
+ },
+ "sort": {
+ "title": "Sort by",
+ "default": "Default",
+ "name_asc": "Name A-Z",
+ "name_desc": "Name Z-A",
+ "marketcap_desc": "Market cap"
+ }
+ },
+ "AuthenticityRow": {
+ "title": "Authentication",
+ "subtitle": "Genuine"
+ },
+ "RemoveRow": {
+ "title": "Remove device"
+ },
+ "FirmwareVersionRow": {
+ "title": "Firmware version",
+ "subtitle": "Firmware {{version}}"
+ },
+ "FirmwareUpdateRow": {
+ "title": "Firmware version {{version}} available",
+ "subtitle": "Please use Ledger Live Desktop to update",
+ "action": "Update"
+ },
+ "FirmwareUpdate": {
+ "title": "Update firmware",
+ "Installing": {
+ "title": "{{stepName}}...",
+ "subtitle": "If requested on your device, please enter your PIN to finish the process."
+ },
+ "steps": {
+ "osu": "Installing OSU",
+ "flash-mcu": "MCU updating",
+ "flash-bootloader": "Bootloader updating",
+ "flash": "Flashing your device",
+ "firmware": "Firmware updating"
+ },
+ "newVersion": "Update Firmware to {{version}} is available",
+ "drawerUpdate": {
+ "title": "Firmware Update",
+ "description": "Update your Ledger Nano firmware by connecting it to the Ledger Live application on desktop"
+ }
+ },
+ "FirmwareUpdateReleaseNotes": {
+ "introTitle": "You are about to install <1><0>firmware version {{version}}.0>1>",
+ "introDescription1": "Please note that all the apps on your device will be deleted. You can reinstall your apps after the firmware update.",
+ "introDescription2": "This has no impact on your crypto assets.",
+ "action": "Continue update"
+ },
+ "systemLanguageAvailable": {
+ "title": "Change your app's language?",
+ "description": {
+ "newSupport": "Good news! Our teams have been working hard and Ledger Live now supports {{language}}.",
+ "advice": "You can always change your language back later in the settings."
+ },
+ "switchButton": "Switch to {{language}}",
+ "no": "I prefer not to",
+ "languages": {
+ "en": "English",
+ "fr": "French",
+ "ru": "Russian",
+ "es": "Spanish",
+ "zh": "Chinese",
+ "de": "German"
+ }
+ },
+ "FirmwareUpdateCheckId": {
+ "title": "Identifier",
+ "description": "Please press both buttons together on your {{fullDeviceName}} if it displays the same identifier:"
+ },
+ "FirmwareUpdateMCU": {
+ "title": "Restart device",
+ "desc1": "Disconnect your device from your computer.",
+ "desc2": "Press and hold the left button, connect the USB cable then release the button when the Bootloader screen appears."
+ },
+ "FirmwareUpdateConfirmation": {
+ "title": "Firmware updated",
+ "description": "Go to Manager to reinstall apps on your device."
+ },
+ "RepairDevice": {
+ "title": "Repair",
+ "action": "My device is ready"
+ },
+ "StepLegacyModal": {
+ "description": "Imported accounts synchronize with the network only, not between Mobile and Desktop versions of Ledger Live."
+ },
+ "algorand": {
+ "token": "ASA (Assets)",
+ "claimRewards": {
+ "title": "Rewards",
+ "button": "Claim",
+ "stepperHeader": {
+ "info": "Earn rewards",
+ "starter": "Rewards",
+ "connectDevice": "Connect device",
+ "verification": "Verification",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "info": {
+ "description": "Delegate your Algo to earn rewards while keeping full security and control on your assets.",
+ "steps": {
+ "0": "A minimum balance of 1 Algo is required to receive rewards.",
+ "1": "Increase the account’s balance to increase rewards.",
+ "2": "Perform a transaction to or from the account to claim rewards."
+ },
+ "howItWorks": "How rewards work",
+ "cta": "Receive Algo"
+ },
+ "starter": {
+ "title": "Congratulations! You earned {{amount}}. Continue to claim your rewards.",
+ "howItWorks": "How rewards work",
+ "warning": "You will be prompted to generate an empty transaction to your account. This will add your current rewards to your balance at the minimal cost of the transaction fees.",
+ "cta": "Continue"
+ },
+ "verification": {
+ "success": {
+ "title": "Rewards successfully claimed!",
+ "text": "Your rewards were added to your available balance.",
+ "cta": "Go to account"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please check back in a few minutes to make sure your transaction did not go through before trying again."
+ }
+ }
+ }
+ },
+ "optIn": {
+ "stepperHeader": {
+ "selectToken": "Add ASA (Asset)",
+ "connectDevice": "Connect device",
+ "verification": "Verification",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "selectToken": {
+ "warning": {
+ "title": "Asset already added",
+ "description": "You already have {{token}} asset in your Algorand account."
+ }
+ },
+ "verification": {
+ "success": {
+ "title": "{{token}} asset successfully added!",
+ "text": "You can now receive and send {{token}} assets on your Algorand account.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please check back in a few minutes to make sure your transaction did not go through before trying again."
+ }
+ }
+ }
+ }
+ },
+ "celo": {
+ "info": {
+ "available": {
+ "title": "CELO available",
+ "description": "This amount is disposable."
+ }
+ }
+ },
+ "cosmos": {
+ "info": {
+ "available": {
+ "title": "ATOM available",
+ "description": "This amount is disposable."
+ },
+ "delegated": {
+ "title": "Delegated assets",
+ "description": "Delegated assets are used for Cosmos voting. This is your total number of votes."
+ },
+ "undelegating": {
+ "title": "Undelegating",
+ "description": "Undelegated assets are in a timelock of 21 days, before being available."
+ },
+ "delegationUnavailable": {
+ "title": "Delegation unavailable",
+ "description": "Not enough available balance in your account to start a new delegation."
+ }
+ },
+ "delegation": {
+ "delegationEarn": "You can earn ATOM rewards by delegating your assets.",
+ "info": "How Delegation works",
+ "claimRewards": "Claim rewards",
+ "claimAvailableRewards": "Claim {{amount}}",
+ "header": "Delegation(s)",
+ "Amount": "Amount",
+ "noRewards": "No rewards available",
+ "delegate": "Delegate",
+ "undelegate": "Undelegate",
+ "redelegate": "Redelegate",
+ "reward": "Claim rewards",
+ "estYield": "Est. yield",
+ "stepperHeader": {
+ "starter": "Earn rewards",
+ "validator": "Delegate assets",
+ "amountSubTitle": "Amount to delegate",
+ "summary": "Summary",
+ "verification": "Verification",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "starter": {
+ "description": "Delegate your ATOM to earn rewards securely, keeping control of your assets.",
+ "steps": {
+ "0": "Delegated assets remain yours.",
+ "1": "You will have to wait 21 days for the undelegation to complete.",
+ "2": "Delegate securely with your Ledger device."
+ },
+ "warning": {
+ "description": "Choose your validator wisely: Part of your delegated assets may be irrevocably lost if the validator does not behave appropriately."
+ },
+ "cta": "Continue"
+ },
+ "validator": {
+ "validators": "Validators",
+ "myDelegations": "My delegations",
+ "cta": "Continue",
+ "estYield": "Est. yield: {{amount}}",
+ "totalAvailable": "Total available: <0>{{amount}}0>",
+ "allAssetsUsed": "All assets used",
+ "noResultsFound": "No validator found for <0>{{search}}0>",
+ "currentAmount": "(+ <0>{{amount}}0>)"
+ },
+ "amount": {
+ "assetsRemaining": "Assets remaining: <0>{{amount}}0>",
+ "allAssetsUsed": "All assets used",
+ "minAmount": "Minimum amount: <0>{{min}}0>",
+ "incorrectAmount": "Maximum amount: <0>{{max}}0>",
+ "cta": "Continue"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully delegated your assets",
+ "text": "Your account balance will be updated once the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ },
+ "drawer": {
+ "status": "Status",
+ "rewards": "Rewards",
+ "active": "Active",
+ "inactive": "Inactive",
+ "completionDate": "Completion date",
+ "redelegatedFrom": "Redelegated from"
+ }
+ },
+ "redelegation": {
+ "estYield": "est. yield",
+ "stepperHeader": {
+ "validator": "Choose new validator",
+ "amountSubTitle": "Amount to redelegate",
+ "amountTitle": "{{from}} → {{to}}",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "validator": {
+ "validators": "Validators",
+ "myDelegations": "My delegations",
+ "cta": "Continue",
+ "estYield": "Est. yield: {{amount}}",
+ "totalAvailable": "Total available: <0>{{amount}}0>",
+ "allAssetsUsed": "All assets used",
+ "noResultsFound": "No validator found for <0>{{search}}0>"
+ },
+ "amount": {
+ "newRedelegatedBalance": "New total for <0>{{name}}0> after operation: <0>{{amount}}0>"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully redelegated your assets",
+ "text": "Your account balance will be updated when the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ }
+ },
+ "undelegation": {
+ "stepperHeader": {
+ "amountSubTitle": "Amount to undelegate",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "amount": {
+ "allAssetsUsed": "All assets undelegated"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully undelegated your assets",
+ "text": "Your account balance will be updated when the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ }
+ },
+ "claimRewards": {
+ "stepperHeader": {
+ "validator": "Select reward to collect",
+ "method": "Claim rewards",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "method": {
+ "youEarned": "You earned",
+ "byDelegationAssetsTo": "by delegating assets to",
+ "claimReward": "Cash in",
+ "claimRewardCompound": "Compound",
+ "claimRewardInfo": "They will be claimed now and added to your available balance.",
+ "claimRewardCompoundInfo": "They will be claimed now and automatically delegated to the same validator.",
+ "compoundOrCashIn": "Compound or Cash in?",
+ "claimRewardTooltip": "Rewards will be added to the available balance",
+ "claimRewardCompoundTooltip": "Rewards will be added to the delegated amount",
+ "cta": "Continue"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully claimed your rewards. They were added to your available balance.",
+ "titleCompound": "Your rewards were delegated to the same validator.",
+ "text": "Your account balance will be updated when the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ }
+ }
+ },
+ "cryptoOrg": {
+ "account": {
+ "subHeader": {
+ "cardTitle": "Powered by Crypto.org",
+ "moreInfo": "More info",
+ "drawerTitle": "Crypto.org integration",
+ "title": "The Crypto.org (CRO) token is now available on Ledger Live",
+ "description": "You can now start to manage your Crypto.org (CRO) tokens and secure them through Ledger Live.",
+ "description2": "The Crypto.org (CRO) token is native to the Crypto.org chain and is used for everything from transactions, staking and many upcoming features.",
+ "website": "Cryptocurrency in every wallet"
+ }
+ }
+ },
+ "elrond": {
+ "account": {
+ "subHeader": {
+ "cardTitle": "Powered by Elrond",
+ "drawerTitle": "Elrond integration",
+ "moreInfo": "More info",
+ "title": "Elrond eGold (EGLD) token is now available on Ledger Live",
+ "description": "You can now safely secure your eGold (EGLD) tokens and manage them through Ledger Live.",
+ "description2": "The Elrond eGold (EGLD) Token is native to the Elrond Network and will be used for everything from transactions, staking, smart contracts, governance and validator rewards.",
+ "description3": "We are actively working with the Elrond team to add more functionality, such as staking, native ESDT (Elrond Standard Digital Token) support, and more.",
+ "website": "The Internet Scale Blockchain"
+ }
+ }
+ },
+ "tezos": {
+ "AccountHeader": {
+ "title": "You can earn rewards by delegating your account",
+ "btn": "Earn rewards"
+ }
+ },
+ "tron": {
+ "voting": {
+ "earnRewars": "Earn rewards",
+ "delegationEarn": "You can now earn rewards by freezing and voting.",
+ "howItWorks": "How voting works",
+ "startEarning": "Earn rewards",
+ "title": "Claim rewards",
+ "header": "Votes ({{total}})",
+ "Amount": "Amount",
+ "noRewards": "No rewards available",
+ "votes": {
+ "title": "Votes",
+ "description": "Cast your votes for one or more Representatives to start earning rewards.",
+ "cta": "Vote"
+ },
+ "rewards": {
+ "title": "Voting rewards",
+ "button": "Claim"
+ },
+ "manageVotes": "Manage votes",
+ "remainingVotes": {
+ "title": "You still have votes remaining",
+ "description": "Cast your remaining votes to earn more rewards."
+ },
+ "flow": {
+ "started": {
+ "title": "Vote",
+ "srOrCandidate": "SR or Candidate?",
+ "description": "Vote for one or more Super Representatives to start earning rewards.",
+ "button": {
+ "continue": "Cast votes"
+ }
+ },
+ "selectValidator": {
+ "sections": {
+ "title": {
+ "selected": "Selected",
+ "superRepresentatives": "Super Representatives",
+ "candidates": "Candidates"
+ }
+ }
+ }
+ }
+ },
+ "freeze": {
+ "flow": {
+ "steps": {
+ "starter": {
+ "title": "Earn rewards",
+ "description": "Delegate TRX to a third party candidate to earn rewards. Click on continue to freeze assets and vote.",
+ "bullet": {
+ "delegate": "Delegated assets remain yours.",
+ "access": "You can access your assets 3 days after freezing.",
+ "ledger": "Delegate securely with your Ledger device."
+ },
+ "button": {
+ "cta": "Continue"
+ }
+ }
+ }
+ }
+ },
+ "manage": {
+ "title": "Manage Tron Power",
+ "freeze": {
+ "title": "Freeze",
+ "description": "Freeze TRX to earn Bandwidth or Energy. You can also vote for Super Representatives."
+ },
+ "unfreeze": {
+ "title": "Unfreeze",
+ "description": "Unfreeze TRX to add them back to your available balance. You will no longer earn rewards."
+ },
+ "vote": {
+ "title": "Vote",
+ "description": "Cast votes for Super Representatives to earn rewards."
+ }
+ },
+ "info": {
+ "available": {
+ "title": "TRX available",
+ "description": "This amount is disposable."
+ },
+ "frozen": {
+ "title": "Frozen",
+ "description": "Frozen assets are used for Tron voting. This is your total number of votes."
+ },
+ "bandwidth": {
+ "title": "Bandwidth",
+ "description": "Bandwidth points are used to make transactions instead of paying TRX network fees. Choose Bandwidth to increase your daily free transactions."
+ },
+ "energy": {
+ "title": "Energy",
+ "description": "Energy points are required to execute smart contracts. If you don't run smart contracts, there is no need to have rewards in Energy points."
+ },
+ "claimRewards": {
+ "title": "Voting rewards",
+ "description": "The TRX generated as rewards during block production can be claimed every 24 hours."
+ },
+ "superRepresentative": {
+ "title": "Super Representatives (SR)",
+ "description": "Super Representatives play a key role in governing the TRON community by ensuring basic functions, e.g. block generation and bookkeeping."
+ },
+ "candidates": {
+ "title": "Candidates",
+ "description": "127 individuals elected through voting by the entire token-holder community. Votes are sampled every 6 hours."
+ }
+ }
+ },
+ "stellar": {
+ "memo": {
+ "title": "Memo",
+ "warning": "When using a Memo, carefully verify the type with the recipient."
+ },
+ "memoType": {
+ "MEMO_TEXT": "Memo Text",
+ "NO_MEMO": "No Memo",
+ "MEMO_ID": "Memo ID",
+ "MEMO_HASH": "Memo Hash",
+ "MEMO_RETURN": "Memo Return"
+ }
+ },
+ "polkadot": {
+ "lockedBalance": "Bonded balance",
+ "unlockingBalance": "Unbonding balance",
+ "unlockedBalance": "Unbonded balance",
+ "networkFees": "Network fees are automatically set by the Polkadot consensus, you won't be able to review them on your device",
+ "bondedBalanceBelowMinimum": "Your bonded balance is below the current minimum of {{minimumBondBalance}}. Your nominations are at risk of being removed.",
+ "info": {
+ "available": {
+ "title": "DOT available",
+ "description": "This amount can be sent anytime."
+ },
+ "locked": {
+ "title": "Bonded assets",
+ "description": "Assets must be bonded to nominated validators before earning rewards."
+ },
+ "unlocking": {
+ "title": "Unbonding assets",
+ "description": "Unbonding assets stay locked for 28 days before they can be withdrawn."
+ },
+ "unlocked": {
+ "title": "Unbonded assets",
+ "description": "Unbonded assets can now be moved using the withdraw operation."
+ },
+ "electionOpen": {
+ "title": "Validators election ongoing",
+ "description": "The election of new validators is currently ongoing. As such, staking operations are not available for 15 minutes at most."
+ },
+ "minBondWarning": {
+ "title": "Not enough bonded",
+ "description": "Your bonded balance is below the current minimum allowed to nominate. Your nominations are at risk of being removed."
+ }
+ },
+ "nomination": {
+ "emptyState": {
+ "title": "Earn rewards",
+ "description": "You can earn rewards by bonding assets and then nominating your validator(s).",
+ "info": "How nominations work",
+ "cta": "Earn rewards"
+ },
+ "header": "Nominations",
+ "nominate": "Nominate",
+ "chill": "Clear nominations",
+ "setController": "Change controller",
+ "status": "Status",
+ "totalStake": "Total Stake",
+ "amount": "Bonded Amount",
+ "commission": "Commission",
+ "active": "Active",
+ "activeInfo": "This validator is elected and is collecting rewards on your bonded assets.",
+ "inactive": "Inactive",
+ "inactiveInfo": "This validator is elected but is not collecting rewards on your bonded assets.",
+ "waiting": "Unelected",
+ "waitingInfo": "This validator is not elected, therefore is not collecting rewards.",
+ "notValidator": "Not a validator",
+ "notValidatorInfo": "This address is no longer a validator",
+ "elected": "Elected",
+ "electedInfo": "This validator is elected and is collecting rewards for its nominators.",
+ "nominators": "Nominators",
+ "nominatorsCount": "{{nominatorsCount}} nominators",
+ "nominatorsInfo": "This validator is currently elected by {{count}} nominators.",
+ "oversubscribed": "Oversubscribed ({{nominatorsCount}})",
+ "oversubscribedInfo": "Only the top {{maxNominatorRewardedPerValidator}} nominators with the highest bonded amount earn rewards",
+ "hasPendingBondOperation": "A bond operation is still pending confirmation",
+ "externalControllerUnsupported": "This stash account is controlled by a separate account whose address is <0>{{controllerAddress}}0>. To stake with Ledger Live, you must set this stash account as its own controller.",
+ "externalStashUnsupported": "This account is the controller of a separate stash account whose address is <0>{{stashAddress}}0>. To stake with Ledger Live, you must set your stash account as its own controller.",
+ "showInactiveNominations": "Show all nominations ({{count}})",
+ "hideInactiveNominations": "Show active nominations only",
+ "noActiveNominations": "There are no active nominations.",
+ "showAllUnlockings": "Show all unbonding amounts ({{count}})",
+ "hideAllUnlockings": "Hide unbonding amounts"
+ },
+ "unlockings": {
+ "header": "Unbonding",
+ "withdrawUnbonded": "Withdraw Unbonded",
+ "rebond": "Rebond"
+ },
+ "manage": {
+ "title": "Manage assets",
+ "bond": {
+ "title": "Bond",
+ "description": "To earn rewards, first bond an amount. Then you must nominate your validator(s)."
+ },
+ "unbond": {
+ "title": "Unbond",
+ "description": "To make a bonded amount available again, first you need to unbond it. You can withdraw it after the 28-day unbonding period."
+ },
+ "withdrawUnbonded": {
+ "title": "Withdraw Unbonded",
+ "description": "To retrieve an unbonded amount back to the available balance, you need to withdraw it manually."
+ },
+ "nominate": {
+ "title": "Nominate",
+ "description": "Choose up to 16 validators. Ensure nominations are Active to earn rewards."
+ },
+ "chill": {
+ "title": "Clear nominations",
+ "description": "Remove all nominations. You will stop earning rewards. Your bonded amount remains bonded."
+ }
+ },
+ "nominate": {
+ "stepperHeader": {
+ "validators": "Validators to nominate",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "validators": {
+ "myNominations": "My nominations",
+ "electedValidators": "Elected validators",
+ "waitingValidators": "Unelected validators",
+ "noResultsFound": "No validators found for <0>{{search}}0>",
+ "selected": "{{selected}} out of {{total}} selected",
+ "notValidatorsRemoved": "You have nominated {{count}} addresses who are no longer validators. They will automatically be removed from your nominate transaction.",
+ "maybeChill": "Clear nominations instead"
+ },
+ "validation": {
+ "success": {
+ "title": "You have successfully nominated validators",
+ "description": "You will start earning rewards when your assets are bonded to your elected validator(s)."
+ }
+ }
+ }
+ },
+ "bond": {
+ "rewardDestination": {
+ "label": "Reward Destination",
+ "stash": "Available balance",
+ "stashDescription": "Rewards are credited to your available balance.",
+ "staked": "Bonded balance",
+ "stakedDescription": "Rewards are credited to your bonded balance for compound earning.",
+ "optionTitle": "Note",
+ "optionDescription": "Once set, the option is fixed for the lifetime of this bond. If you change your mind, please read ",
+ "clickableLink": "here."
+ },
+ "stepperHeader": {
+ "starter": "Earn rewards",
+ "amount": "Amount to bond",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "starter": {
+ "description": "You may earn rewards by bonding your assets and then nominate your validators.",
+ "bullet": [
+ "You keep ownership of bonded assets",
+ "Nominate using your Ledger device",
+ "Assets will be available again, 28 days after unbonding"
+ ],
+ "help": "How nominations work",
+ "warning": "Choose your validators wisely: part of your bonded assets may be irrevocably lost if a validator does not behave appropriately."
+ },
+ "amount": {
+ "availableLabel": "Available",
+ "maxLabel": "Max"
+ },
+ "confirm": {
+ "info": "Bonded assets can be unbonded at any time, but unbonding takes 28 days."
+ },
+ "validation": {
+ "success": {
+ "title": "Assets successfully bonded",
+ "description": "You can nominate validators once the network has confirmed the transaction.",
+ "descriptionNominate": "You will be able to nominate validators once the network has confirmed your transaction.",
+ "nominate": "Nominate",
+ "later": "Nominate later"
+ },
+ "pending": {
+ "title": "Transaction awaiting confirmation",
+ "description": "You have to wait a moment before nominating"
+ }
+ }
+ }
+ },
+ "rebond": {
+ "stepperHeader": {
+ "amount": "Amount to rebond",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "amount": {
+ "availableLabel": "Unbonding",
+ "maxLabel": "Max"
+ },
+ "confirm": {
+ "info": "Rebonded assets are immediately added to the bonded amount."
+ },
+ "validation": {
+ "success": {
+ "title": "Assets successfully rebonded",
+ "description": "Your account balance will update once the network has confirmed the transaction."
+ }
+ }
+ }
+ },
+ "unbond": {
+ "stepperHeader": {
+ "amount": "Amount to unbond",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "amount": {
+ "availableLabel": "Bonded",
+ "maxLabel": "Max"
+ },
+ "confirm": {
+ "info": "Unbonded assets can be withdrawn after the 28-day unbonding period."
+ },
+ "validation": {
+ "success": {
+ "title": "Unbond transaction sent successfully",
+ "description": "Unbonded assets can be withdrawn after 28 days."
+ }
+ }
+ }
+ },
+ "simpleOperation": {
+ "modes": {
+ "withdrawUnbonded": {
+ "title": "Withdraw unbonded",
+ "description": "Withdraw unbonded assets to your available balance."
+ },
+ "chill": {
+ "title": "Clear nominations",
+ "description": "Clears all nominations and stops earning rewards.",
+ "info": "Bonded assets will remain bonded. If you unbond them, they will be available after 28 days."
+ },
+ "setController": {
+ "title": "Change Controller",
+ "description": "Set your Ledger account as its own controller",
+ "info": "Ledger Live doesn't support operations on separate stash and controller accounts."
+ }
+ },
+ "stepperHeader": {
+ "info": "Info",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "validation": {
+ "success": {
+ "title": "Transaction sent successfully",
+ "description": "You will see your operation in history once the network has confirmed the transaction."
+ }
+ }
+ }
+ }
+ },
+ "delegation": {
+ "overdelegated": "Overdelegated",
+ "delegationSendWarnDesc": "The amount to be sent will be deducted from your delegated account.",
+ "delegationReceiveWarnDesc": "The amount received in a delegated account will be added to the total staked amount. Choose another account if you want to avoid this.",
+ "iDelegateMy": "I delegate my",
+ "undelegateMy": "Undelegate my",
+ "warnUndelegation": "Your account will be undelegated.",
+ "warnDelegation": "Delegating your voting rights does not guarantee any rewards from your validator.",
+ "to": "to",
+ "from": "from",
+ "forAnEstYield": "for an est. yield of",
+ "yieldPerYear": "{{yield}} / Year",
+ "yieldInfos": "Yield rates are provided by",
+ "termsAndPrivacy": "I have read and I accept the <1>Ledger Live Terms of Use1> and <3>Privacy Policy3>.",
+ "delegation": "Delegation",
+ "viewDetails": "View details",
+ "validator": "Validator",
+ "validatorAddress": "Validator address",
+ "delegatedAccount": "Delegated account",
+ "duration": "Duration",
+ "transactionID": "Transaction ID",
+ "receive": "Receive more",
+ "changeValidator": "Change validator",
+ "endDelegation": "End delegation",
+ "durationForDays0": "Just now",
+ "durationForDays": "For a day",
+ "durationForDays_plural": "For {{count}} days",
+ "durationDays0": "Just now",
+ "durationDays": "1 day",
+ "durationDays_plural": "{{count}} days",
+ "selectValidatorTitle": "Select validator",
+ "started": {
+ "title": "Earn rewards",
+ "description": "Delegate your Tezos account to a third-party validator to earn rewards securely, keeping control of your assets.",
+ "steps": {
+ "0": "Delegated accounts remain yours.",
+ "1": "You can manage your assets at all times.",
+ "2": "Delegate securely with your Ledger device."
+ },
+ "cta": "Delegate to earn rewards"
+ },
+ "broadcastSuccessTitle": {
+ "delegate": "Delegation sent",
+ "undelegate": "Operation sent"
+ },
+ "broadcastSuccessDescription": {
+ "delegate": "Delegation transaction broadcasted successfully. You should earn your first rewards in around 40 days, depending on the validator.",
+ "undelegate": "Your account delegation will end when the operation is confirmed. You can delegate your account again at any time."
+ },
+ "summaryTitle": "Summary",
+ "goToAccount": "Go to Accounts",
+ "howDelegationWorks": "How delegation works",
+ "actions": {
+ "redelegate": "Redelegate",
+ "collectRewards": "Collect rewards",
+ "undelegate": "Undelegate"
+ }
+ },
+ "ValidateOnDevice": {
+ "title": {
+ "send": "Please confirm the operation on your {{productName}} to send it",
+ "freeze": "On your device, please confirm to finalize the operation",
+ "unfreeze": "On your device, please confirm to finalize the operation",
+ "claimReward": "On your device, please confirm to finalize the operation",
+ "vote": "On your device, please confirm to finalize the operation",
+ "delegate": "On your device, please confirm to finalize the operation",
+ "redelegate": "On your device, please confirm to finalize the operation",
+ "undelegate": "On your device, please confirm to finalize the operation"
+ },
+ "warning": "Always verify the address displayed your device exactly matches the one given to you by the {{recipientWording}}",
+ "recipientWording": {
+ "send": "Always verify the address displayed your device exactly matches the one given to you by the recipient",
+ "delegate": "Always verify that your device displays the address exactly as provided by the validator",
+ "undelegate": "Always verify that your device displays the address exactly as provided by the validator",
+ "freeze": "Always verify that your device displays the address exactly as provided",
+ "unfreeze": "Always verify that your device displays the address exactly as provided",
+ "claimReward": "Always verify that your device displays the address exactly as provided",
+ "vote": "Always verify that your device displays the address exactly as provided",
+ "erc20": {
+ "approve": "Verify the operation details on your device before sending it."
+ },
+ "compound.supply": "Verify the deposit details on your device before sending it.",
+ "compound.withdraw": "Verify the withdrawal details on your device before sending it."
+ },
+ "name": "Name",
+ "votes": "Votes",
+ "infoWording": {
+ "freeze": "Frozen tokens are locked for a period of 3 days.",
+ "unfreeze": "Your {{resource}} points will be reduced, and all your votes will be cancelled.",
+ "claimReward": "Rewards can be claimed every 24 hours.",
+ "cosmos": {
+ "claimReward": "If the selected validator has pending rewards, they will automatically be claimed.",
+ "redelegate": "You will have to wait 21 days for undelegated assets to return to the available balance.",
+ "undelegate": "You will have to wait 21 days for undelegated assets to return to the available balance."
+ },
+ "lending": "Verify the operation details on your device before sending it."
+ },
+ "amount": "Amount",
+ "account": "Account",
+ "from": "From",
+ "to": "To",
+ "redelegationAmount": "Redelegated amount",
+ "gas": "Gas",
+ "validatorAddress": "Validator address",
+ "rewardAmount": "Reward amount",
+ "undelegatedAmount": "Undelegated amount",
+ "memo": "Memo"
+ },
+ "Terms": {
+ "title": "Terms of Use",
+ "read": "Read the Terms of Use",
+ "switchLabel": "I have read and agree with the <1>Terms of Service1>",
+ "switchLabelFull": "I have read and accept the Terms of Service and Privacy Policy",
+ "cta": "Enter Ledger App",
+ "service": "Terms of service"
+ },
+ "exchange": {
+ "buy": {
+ "tabTitle": "Buy",
+ "selectCurrency": "Select a currency",
+ "selectAccount": "Select an account",
+ "connectDevice": "Connect your device",
+ "title": "Choose a provider to buy crypto",
+ "coinifyTitle": "Buy crypto via our partner",
+ "description": "Purchase crypto assets via Coinify and receive them directly in your Ledger account.",
+ "CTAButton": "Buy now",
+ "emptyState": {
+ "title": "No {{currency}} account",
+ "description": "Please add an account before buying {{currency}}.",
+ "CTAButton": "Add account"
+ },
+ "skipDeviceVerification": {
+ "confirm": "Confirm",
+ "cancel": "Cancel",
+ "address": "Address for {{currency}} account",
+ "warning": "Your address was not confirmed on your Ledger device. Please verify it for security"
+ },
+ "bullets": {
+ "whereToBuy": "Buy from anywhere",
+ "cryptoSupported": "crypto supported",
+ "payWith": "Pay with card or SEPA"
+ }
+ },
+ "sell": {
+ "tabTitle": "Sell",
+ "selectCurrency": "Select a currency",
+ "selectAccount": "Select an account",
+ "connectDevice": "Connect your device",
+ "title": "Sell crypto via our partner",
+ "description": "Sell crypto assets directly from your Ledger account via Coinify and receive fiat in your bank account.",
+ "CTAButton": "Sell now",
+ "emptyState": {
+ "title": "No {{currency}} account",
+ "description": "You need to add an account before you can sell {{currency}}.",
+ "CTAButton": "Add account"
+ }
+ },
+ "history": {
+ "tabTitle": "History"
+ }
+ },
+ "banner": {
+ "exchangeBuyCrypto": {
+ "title": "BUY CRYPTO",
+ "description": "Purchase crypto assets via Coinify and receive them directly in your Ledger account."
+ },
+ "swap": {
+ "title": "SWAP CRYPTO",
+ "description": "Swap crypto assets directly from your Ledger accounts with our partner."
+ }
+ },
+ "walletconnect": {
+ "disclaimer": "Wants to connect to the following Ethereum account through your wallet :",
+ "reject": "Reject",
+ "connect": "Connect",
+ "connected": "Connected",
+ "disconnected": "Disconnected",
+ "warningdisconnected": "There is a connection problem between the dApp, WalletConnect and Ledger Live. Wait a few moments or relaunch the connection",
+ "info": "You can now access the {{name}} dApp on your web browser.",
+ "warning": "Sharing receive addresses from dApp is not secure. Always use Ledger Live when sharing your address to receive funds.",
+ "isconnecting": "is connecting, please wait...",
+ "disconnect": "Disconnect",
+ "retry": "Retry",
+ "close": "Close",
+ "message": "Message",
+ "messageHash": "Message Hash",
+ "domainHash": "Domain Hash",
+ "stringHash": "Hash",
+ "from": "From",
+ "successTitle": "Message signed",
+ "successDescription": "You have signed the message received from a third party application",
+ "stepperHeader": {
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "stepVerification": {
+ "action": "Please confirm the operation on your device",
+ "accountName": "Account name"
+ },
+ "deeplinkingTitle": "Select an Ethereum account",
+ "addAccount": "Add new account"
+ },
+ "notificationCenter": {
+ "title": "Notification center",
+ "announcement": "Announcement",
+ "liveStatus": "Ledger Live status",
+ "groupedToast": {
+ "text": "You have {{count}} unread notifications",
+ "cta": "See details"
+ },
+ "news": {
+ "title": "News",
+ "titleCount": "News ({{count}})",
+ "emptyState": {
+ "title": "No news for the moment",
+ "desc": "You will find here all the news related to Ledger and Ledger Live"
+ }
+ },
+ "status": {
+ "title": "Status",
+ "ok": {
+ "title": "Ledger Live is up and running",
+ "desc": "<0>Having trouble? Go on our 0><1>help page1>"
+ },
+ "error": {
+ "title": "Ledger Live is experiencing issues"
+ }
+ }
+ },
+ "platform": {
+ "catalog": {
+ "title": "Live Apps Catalog",
+ "branch": {
+ "soon": "coming soon",
+ "experimental": "experimental",
+ "debug": "debug"
+ },
+ "banner": {
+ "title": "Discover our Live Catalog",
+ "description": "Unlock a new world of crypto possibilities. One secure access to all services – DeFi, NFTs and more to come."
+ },
+ "twitterBanner": {
+ "description": "Tell us what's the next service you want to see with the hashtag",
+ "tweetText": "The next Ledger App should be..."
+ },
+ "pollCTA": {
+ "title": "Poll",
+ "description": "Which service do you want to see in Ledger Live?"
+ },
+ "developerCTA": {
+ "title": "For Developers",
+ "description": "All the information you need to integrate your applications on Ledger Live."
+ }
+ },
+ "disclaimer": {
+ "title": "External Application",
+ "description": "You are about to be redirected to an application not operated by Ledger.",
+ "legalAdvice": "This application is not operated by Ledger. Ledger is not responsible for any loss of funds or quality of service of such application.\n\nAlways make sure to carefully verify the information displayed on your device.",
+ "legalAdviceShort": "Ledger is not responsible for any loss of funds. Always make sure to verify the information displayed on your device.",
+ "checkbox": "Do not remind me again.",
+ "CTA": "Continue"
+ },
+ "webPlatformPlayer": {
+ "infoPanel": {
+ "website": "website"
+ }
+ }
+ },
+ "market": {
+ "title": "Market",
+ "filters": {
+ "sort": "Sort",
+ "filter": "Filter",
+ "view": {
+ "label": "View",
+ "all": "All coins",
+ "liveCompatible": "Live Compatible",
+ "all_label": "All cryptocurrencies",
+ "liveCompatible_label": "Only Live Supported"
+ },
+ "order": {
+ "market_cap": "Rank",
+ "market_cap_asc": "Rank (Market cap) asc.",
+ "market_cap_desc": "Rank (Market cap) desc."
+ },
+ "currency": "Currency",
+ "time": "Time",
+ "apply": "Apply"
+ },
+ "marketList": {
+ "crypto": "Crypto",
+ "price": "Price",
+ "change": "Change",
+ "marketCap": "Market cap",
+ "last7d": "Last 7 days"
+ },
+ "detailsPage": {
+ "holding": "Your Holdings",
+ "priceStatistics": "Price Statistics",
+ "price": "Price",
+ "tradingVolume": "Trading volume",
+ "24hLowHight": "24h Low / 24h High",
+ "7dLowHigh": "7d Low / 7d High",
+ "allTimeHigh": "All time high",
+ "allTimeLow": "All time low",
+ "marketCapRank": "Market cap rank",
+ "marketCapDominance": "Market cap dominance",
+ "supply": "Supply",
+ "circulatingSupply": "Circulating supply",
+ "totalSupply": "Total supply",
+ "maxSupply": "Max supply",
+ "assetNotSupportedOnLedgerLive": "This asset is not supported on Ledger Live."
+ },
+ "range": {
+ "1h": "1H",
+ "24h": "24H",
+ "7d": "7D",
+ "30d": "30D",
+ "1y": "1Y"
+ },
+ "warnings": {
+ "connectionError": "Connection Error",
+ "ledgerUnableToRetrieveData": "Ledger Live is unable to retrieve data.",
+ "checkInternetAndReload": "Please check your internet connection and reload this page.",
+ "reload": "Reload",
+ "noCryptosFound": "No coins found",
+ "noCurrencyFound": "No currency found",
+ "noSearchResultsFor": "Sorry, we did not find any coins for <0>{{search}}0>. Please retry the search with another keyword.",
+ "noCurrencySearchResultsFor": "Sorry, we did not find any currencies for <0>{{search}}0>. Please retry the search with another keyword.",
+ "noSearchResults": "Sorry, we did not find any search results.",
+ "retrySearchKeyword": "Please retry the search with another keyword.",
+ "retrySearchParams": "Please retry the search with another parameters.",
+ "trackFavAssets": "Track your favourite",
+ "clickOnStarIcon": "Clicking on the star icon next to an asset will automatically add them to your favorites.",
+ "browseAssets": "Browse assets"
+ }
+ },
+ "nft": {
+ "account": {
+ "seeAllNfts": "See all NFT",
+ "seeFewerNfts": "See fewer NFT"
+ },
+ "gallery": {
+ "allNft": "All NFT"
+ },
+ "viewer": {
+ "properties": "Properties",
+ "description": "Description",
+ "tokenContract": "Token Address",
+ "tokenId": "Token ID",
+ "quantity": "Quantity"
+ },
+ "viewerModal": {
+ "viewOn": "View on",
+ "viewInExplorer": "View in explorer",
+ "txDetails": "Transaction details"
+ }
+ }
+}
diff --git a/src/locales/en/v3.json b/src/locales/en/v3.json
new file mode 100644
index 0000000000..7f72ca80ba
--- /dev/null
+++ b/src/locales/en/v3.json
@@ -0,0 +1,3818 @@
+{
+ "common": {
+ "cancel": "Cancel",
+ "apply": "Apply",
+ "seeAll": "See all",
+ "back": "Back",
+ "delete": "Delete",
+ "paste": "Paste",
+ "yes": "Yes",
+ "no": "No",
+ "gotit": "Got it",
+ "continue": "Continue",
+ "retry": "Retry",
+ "done": "Done",
+ "sortBy": "Sort by",
+ "signOut": "Sign out",
+ "search": "Search",
+ "contactUs": "Contact Ledger Support",
+ "device": "Device",
+ "cryptoAsset": "Crypto asset",
+ "skip": "Skip",
+ "noCryptoFound": "No crypto assets found",
+ "needHelp": "Do you need help?",
+ "edit": "Edit",
+ "editName": "Edit name",
+ "close": "Close",
+ "confirm": "Confirm",
+ "poweredBy": "Powered by ",
+ "received": "Received",
+ "sent": "Sent",
+ "or": "OR",
+ "rename": "Rename",
+ "learnMore": "Learn more",
+ "checkItOut": "Check it out",
+ "viewDetails": "View details",
+ "today": "Today",
+ "yesterday": "Yesterday",
+ "upToDate": "Up to date",
+ "transactionDate": "Transaction date",
+ "outdated": "Outdated",
+ "satPerByte": "sat/bytes",
+ "notAvailable": "Not available",
+ "import": "Import",
+ "bluetooth": "Bluetooth",
+ "usb": "USB",
+ "add": "Add",
+ "token": "Token",
+ "token_plural": "Tokens",
+ "subaccount": "Subaccount",
+ "subaccount_plural": "Subaccounts",
+ "forgetDevice": "Remove device",
+ "help": "Help",
+ "saveLogs": "Save logs",
+ "sync": {
+ "ago": "Synchronized {{time}}"
+ },
+ "update": "Update available",
+ "install": "Install",
+ "installed": "Installed",
+ "uninstall": "Uninstall",
+ "fromNow": {
+ "seconds": "in one second",
+ "seconds_plural": "in {{time}} seconds",
+ "minutes": "in one minute",
+ "minutes_plural": "in {{time}} minutes",
+ "hours": "in one hour",
+ "hours_plural": "in {{time}} hours",
+ "days": "in one day",
+ "days_plural": "in {{time}} days"
+ },
+ "timeAgo": {
+ "seconds": "one second ago",
+ "seconds_plural": "{{time}} seconds ago",
+ "minutes": "one minute ago",
+ "minutes_plural": "{{time}} minutes ago",
+ "hours": "one hour ago",
+ "hours_plural": "{{time}} hours ago",
+ "days": "yesterday",
+ "days_plural": "{{time}} days ago"
+ },
+ "seeMore": "See more",
+ "moreInfo": "More info",
+ "buyEth": "Buy Ethereum"
+ },
+ "errors": {
+ "countervaluesUnavailable": {
+ "title": "We're not able to provide a countervalue for this asset at the moment"
+ },
+ "AccountAwaitingSendPendingOperations": {
+ "title": "There is a pending operation for this account",
+ "description": "Please wait for the operation to go through."
+ },
+ "AccountNameRequired": {
+ "title": "An account name is needed",
+ "description": "Please enter an account name."
+ },
+ "AccountNeedResync": {
+ "title": "Please try again",
+ "description": "Account is outdated. A synchronisation is needed"
+ },
+ "AlgorandASANotOptInInRecipient": {
+ "title": "Recipient account has not opted in the selected ASA."
+ },
+ "BluetoothRequired": {
+ "title": "Sorry, Bluetooth is disabled",
+ "description": "Please enable Bluetooth in your phone settings. ({{state}} state)"
+ },
+ "BtcUnmatchedApp": {
+ "title": "That's the wrong app",
+ "description": "Please open the {{managerAppName}} app on your device."
+ },
+ "CantOpenDevice": {
+ "title": "Sorry, connection failed",
+ "description": "Your Ledger device must be unlocked and in range to use Bluetooth."
+ },
+ "CantScanQRCode": {
+ "title": "Could not scan this QR code: auto-verification not supported by this address."
+ },
+ "ConnectAppTimeout": {
+ "title": "Sorry, no device found",
+ "description": {
+ "ios": {
+ "nanoX": "Please make sure your {{productName}} is unlocked and has Bluetooth enabled."
+ },
+ "android": {
+ "nanoS": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoSP": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoX": "Please make sure your {{productName}} is unlocked and is connected by cable or has Bluetooth enabled."
+ }
+ }
+ },
+ "ConnectManagerTimeout": {
+ "title": "Sorry, Manager connection failed",
+ "description": {
+ "ios": {
+ "nanoX": "Please make sure your {{productName}} is unlocked and has Bluetooth enabled."
+ },
+ "android": {
+ "nanoS": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoSP": "Please make sure your {{productName}} is unlocked and connected by cable.",
+ "nanoX": "Please make sure your {{productName}} is unlocked and is connected by cable or has Bluetooth enabled."
+ }
+ }
+ },
+ "ClaimRewardsFeesWarning": {
+ "title": "The rewards are smaller than the estimated fees to claim them.",
+ "description": ""
+ },
+ "CompoundLowerAllowanceOfActiveAccountError": {
+ "title": "You cannot reduce the amount approved while having an active deposit."
+ },
+ "CosmosBroadcastCodeInternal": {
+ "title": "Something went wrong (Error #1)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeTxDecode": {
+ "title": "Something went wrong (Error #2)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeInvalidSequence": {
+ "title": "Invalid sequence",
+ "description": "Please try again."
+ },
+ "CosmosBroadcastCodeUnauthorized": {
+ "title": "Unauthorized signature",
+ "description": "This account is not authorized to sign this transaction."
+ },
+ "CosmosBroadcastCodeInsufficientFunds": {
+ "title": "Insufficient funds",
+ "description": "Please make sure the account has enough funds."
+ },
+ "CosmosBroadcastCodeUnknownRequest": {
+ "title": "Something went wrong (Error #6)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeInvalidAddress": {
+ "title": "Invalid address",
+ "description": "Please check the address and try again."
+ },
+ "CosmosBroadcastCodeInvalidPubKey": {
+ "title": "Something went wrong (Error #8)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeUnknownAddress": {
+ "title": "Unknown address",
+ "description": "Please check the address and try again."
+ },
+ "CosmosBroadcastCodeInsufficientCoins": {
+ "title": "Insufficient funds",
+ "description": "Please increase the funds on the account."
+ },
+ "CosmosBroadcastCodeInvalidCoins": {
+ "title": "Something went wrong (Error #11)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeOutOfGas": {
+ "title": "Something went wrong (Error #12)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeMemoTooLarge": {
+ "title": "The Memo field is too long",
+ "description": "Please reduce the size of the memo text and try again."
+ },
+ "CosmosBroadcastCodeInsufficientFee": {
+ "title": "Something went wrong (Error #14)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeTooManySignatures": {
+ "title": "Something went wrong (Error #15)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeGasOverflow": {
+ "title": "Something went wrong (Error #16)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "CosmosBroadcastCodeNoSignatures": {
+ "title": "Something went wrong (Error #17)",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "DeviceAppVerifyNotSupported": {
+ "title": "Open Manager to update this app",
+ "description": "The app verification is not supported."
+ },
+ "DeviceGenuineSocketEarlyClose": {
+ "title": "Sorry, try again (genuine-close)",
+ "description": null
+ },
+ "DeviceHalted": {
+ "title": "Please restart your Ledger device and retry",
+ "description": "An unexpected error occurred. Please try again."
+ },
+ "DeviceNotGenuine": {
+ "title": "Device possibly not genuine",
+ "description": "Please save your logs using the button below and provide them to Ledger Support."
+ },
+ "DeviceNameInvalid": {
+ "title": "Please name the device without '{{invalidCharacters}}'."
+ },
+ "DeviceOnDashboardExpected": {
+ "title": "Device not on Dashboard",
+ "description": "Please return to the Dashboard on your device."
+ },
+ "DeviceSocketFail": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again."
+ },
+ "DeviceSocketNoBulkStatus": {
+ "title": "The connection failed",
+ "description": "Please try again."
+ },
+ "DeviceSocketNoHandler": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again."
+ },
+ "DisconnectedDevice": {
+ "title": "Sorry, it looks like your device was disconnected",
+ "description": "Please reconnect and try again."
+ },
+ "DisconnectedDeviceDuringOperation": {
+ "title": "Sorry, it looks like your device was disconnected",
+ "description": "Please reconnect and try again."
+ },
+ "ETHAddressNonEIP": {
+ "title": "Auto-verification not available: carefully verify the address.",
+ "description": null
+ },
+ "Touch ID Error": {
+ "title": "Biometric authentication failed",
+ "description": "Please use your password or reset the app."
+ },
+ "Error": {
+ "title": "{{message}}",
+ "description": "Something went wrong. Please retry. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "FeeEstimationFailed": {
+ "title": "Sorry, fee estimation failed",
+ "description": "Try setting the fee manually (status: {{status}})."
+ },
+ "FeeNotLoaded": {
+ "title": "Could not load fee rates"
+ },
+ "FeeRequired": {
+ "title": "Fees are required"
+ },
+ "FirmwareOrAppUpdateRequired": {
+ "title": "Firmware or app update needed",
+ "description": "Please use the Manager to uninstall all apps then check if a firmware update is available before reinstalling them."
+ },
+ "LatestFirmwareVersionRequired": {
+ "title": "Device update required on desktop",
+ "description": "Please update your Nano X firmware on Ledger Live Desktop"
+ },
+ "GenuineCheckFailed": {
+ "title": "Device authentication failed",
+ "description": "Something went wrong. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "HardResetFail": {
+ "title": "Sorry, could not reset",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "InvalidAddress": {
+ "title": "This is not a valid {{currencyName}} address"
+ },
+ "InvalidAddressBecauseDestinationIsAlsoSource": {
+ "title": "Destination and source accounts must not be the same."
+ },
+ "InvalidRecipient": {
+ "title": "Invalid recipient"
+ },
+ "LatestMCUInstalledError": {
+ "title": "Sorry, there's nothing to update",
+ "description": "Please contact Ledger Support if you cannot use your device."
+ },
+ "LedgerAPIError": {
+ "title": "Sorry, try again (API HTTP {{status}})",
+ "description": "Unsuccessful calls to the Ledger API server. Please try again."
+ },
+ "LedgerAPIErrorWithMessage": {
+ "title": "{{message}}",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "LedgerAPINotAvailable": {
+ "title": "Sorry, {{currencyName}} services unavailable",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "ManagerAPIsFail": {
+ "title": "Sorry, Manager services are unavailable",
+ "description": "Please check the network status."
+ },
+ "ManagerAppAlreadyInstalled": {
+ "title": "Sorry, that's already installed",
+ "description": "Please check which apps are already installed on your device."
+ },
+ "ManagerAppRelyOnBTC": {
+ "title": "Bitcoin and Ethereum apps needed",
+ "description": "Please install the latest Bitcoin and Ethereum apps first."
+ },
+ "ManagerDeviceLocked": {
+ "title": "Your device is locked",
+ "description": "Please unlock it."
+ },
+ "ManagerNotEnoughSpace": {
+ "title": "Not enough storage left",
+ "info": "Please uninstall some apps to free up space for the {{app}} app. Your crypto assets stay safe when uninstalling apps.",
+ "description": "Uninstalling apps has no impact on your assets.",
+ "continue": "Got it!"
+ },
+ "ManagerQuitPage": {
+ "install": {
+ "title": "Quit and cancel installations ?",
+ "description": "Quitting will cancel the app installations in progress."
+ },
+ "uninstall": {
+ "title": "Quit and cancel uninstallations ?",
+ "description": "Quitting will cancel the app uninstallations in progress."
+ },
+ "update": {
+ "title": "Quit and cancel updates ?",
+ "description": "Quitting will cancel the app updates in progress."
+ },
+ "quit": "Quit Manager"
+ },
+ "ManagerUninstallBTCDep": {
+ "title": "Sorry, this app is needed",
+ "description": "Uninstall the Bitcoin or Ethereum app last."
+ },
+ "NetworkDown": {
+ "title": "Sorry, internet seems to be down",
+ "description": "Please check your internet connection."
+ },
+ "NoAddressesFound": {
+ "title": "Sorry, no account was found",
+ "description": "Something went wrong with the address calculation. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "NotEnoughBalance": {
+ "title": "Sorry, insufficient funds",
+ "description": "Please make sure the account has enough funds."
+ },
+ "NotEnoughGas": {
+ "title": "Insufficient ETH for network fee",
+ "description": "Please send some ETH to your account to pay for ERC20 token transactions."
+ },
+ "NotEnoughBalanceToDelegate": {
+ "title": "Insufficient balance to delegate"
+ },
+ "NotEnoughBalanceInParentAccount": {
+ "title": "Insufficient balance in the parent account"
+ },
+ "NotEnoughSpendableBalance": {
+ "title": "Balance cannot be below {{minimumAmount}}"
+ },
+ "NotEnoughBalanceBecauseDestinationNotCreated": {
+ "title": "Minimum of {{minimalAmount}} needed to activate recipient address"
+ },
+ "PairingFailed": {
+ "title": "Pairing unsuccessful",
+ "description": "Please try again or consult our Bluetooth troubleshooting article."
+ },
+ "PasswordIncorrect": {
+ "title": "Incorrect password",
+ "description": "Please try again."
+ },
+ "PasswordsDontMatch": {
+ "title": "Password does not match",
+ "description": "Please try again."
+ },
+ "SelectExchangesLoadError": {
+ "title": "Unable to load",
+ "description": "Cannot load the exchanges."
+ },
+ "SyncError": {
+ "title": "Synchronization error",
+ "description": "Some accounts could not be synchronized."
+ },
+ "TimeoutError": {
+ "title": "Sorry, server took too long to respond",
+ "description": "Please try again."
+ },
+ "TimeoutTagged": {
+ "title": "Sorry, server took too long to respond ({{tag}})",
+ "description": "Timeout occurred."
+ },
+ "TransactionRefusedOnDevice": {
+ "title": "Transaction refused on device",
+ "description": "Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "TransportError": {
+ "title": "Something went wrong. Please reconnect your device.",
+ "description": "{{message}} Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "TransportStatusError": {
+ "title": "Something went wrong. Please reconnect your device.",
+ "description": "{{message}} Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "TronNoFrozenForBandwidth": {
+ "title": "No assets to unfreeze",
+ "description": "You don't have Bandwidth assets to unfreeze."
+ },
+ "TronNoFrozenForEnergy": {
+ "title": "No assets to unfreeze",
+ "description": "You don't have Energy assets to unfreeze."
+ },
+ "TronUnfreezeNotExpired": {
+ "title": "Unfreeze is not available yet",
+ "description": "Please wait 3 days after your last Freeze operation."
+ },
+ "TronVoteRequired": {
+ "title": "At least 1 vote is needed"
+ },
+ "TronInvalidVoteCount": {
+ "title": "Vote format is incorrect",
+ "description": "You can only vote using round numbers."
+ },
+ "TronRewardNotAvailable": {
+ "title": "Rewards are not claimable yet",
+ "description": "Please wait 24 hours between claims."
+ },
+ "TronNoReward": {
+ "title": "There is no reward to be claimed"
+ },
+ "TronInvalidFreezeAmount": {
+ "title": "Amount to freeze cannot be smaller than 1"
+ },
+ "TronSendTrc20ToNewAccountForbidden": {
+ "title": "Sending TRC20 to the new account will not activate it",
+ "description": "To activate it, first send either TRX or TRC10 to the account. Then it can receive TRC20."
+ },
+ "TronUnexpectedFees": {
+ "title": "Additional fees may apply"
+ },
+ "TronNotEnoughTronPower": {
+ "title": "Not enough votes available"
+ },
+ "TronTransactionExpired": {
+ "title": "Transaction timeout expired",
+ "description": "Transactions must be signed within 30 seconds. Please try again."
+ },
+ "TronNotEnoughEnergy": {
+ "title": "Not enough Energy to send this token"
+ },
+ "UpdateYourApp": {
+ "title": "App update needed",
+ "description": "Please uninstall then reinstall the {{managerAppName}} app in the Manager."
+ },
+ "UserRefusedAllowManager": {
+ "title": "Manager disabled on device",
+ "description": "Please allow the Manager on your device, then try again."
+ },
+ "UserRefusedAddress": {
+ "title": "Receive address rejected",
+ "description": "You rejected the address. If in doubt, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "UserRefusedDeviceNameChange": {
+ "title": "Rename canceled on device",
+ "description": "You canceled the rename. Please try again."
+ },
+ "UserRefusedFirmwareUpdate": {
+ "title": "Firmware update canceled on device",
+ "description": "You canceled the firmware update. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "UserRefusedOnDevice": {
+ "title": "Operation canceled on device",
+ "description": "You rejected the operation on the device."
+ },
+ "WebsocketConnectionError": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again with a better network connection (websocket error)."
+ },
+ "WebsocketConnectionFailed": {
+ "title": "Sorry, connection failed",
+ "description": "Please try again with a better network connection (websocket failed)."
+ },
+ "WrongDeviceForAccount": {
+ "title": "Something went wrong",
+ "description": "Please check that your hardware wallet is set up with the recovery phrase or passphrase associated to the selected account."
+ },
+ "UnexpectedBootloader": {
+ "title": "Sorry, your device must not be in Bootloader mode",
+ "description": "Please restart your device without touching the buttons when the logo appears. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ },
+ "UnavailableTezosOriginatedAccountReceive": {
+ "title": "Cannot receive in subaccounts. Please select the parent account.",
+ "description": "If you want to receive funds, please use the parent account"
+ },
+ "UnavailableTezosOriginatedAccountSend": {
+ "title": "Cannot send from subaccounts yet",
+ "description": "This feature will be added at a later stage due to changes recently introduced by the Babylon update."
+ },
+ "AccessDeniedError": {
+ "title": "Ledger Live needs an update",
+ "description": "Please update Ledger Live to the latest version, and re verify your identity to swap with wyre"
+ },
+ "RecommendUndelegation": {
+ "title": "Please undelegate the account before emptying it"
+ },
+ "RecommendSubAccountsToEmpty": {
+ "title": "Please empty all subaccounts first"
+ },
+ "NotSupportedLegacyAddress": {
+ "title": "The legacy address format is no longer supported"
+ },
+ "StellarWrongMemoFormat": {
+ "title": "Memo format is wrong"
+ },
+ "SourceHasMultiSign": {
+ "title": "Please disable multisign to send {{currencyName}}"
+ },
+ "StellarMemoRecommended": {
+ "title": "A memo may be needed when sending to this recipient"
+ },
+ "StratisDown2021Warning": {
+ "description": "The Stratis blockchain evolved and may no longer work correctly. Support for the original Stratis blockchain will be maintained until 16 October 2021."
+ },
+ "SwapExchangeRateAmountTooLow": {
+ "title": "Amount must be higher than {{minAmountFromFormatted}}"
+ },
+ "SwapExchangeRateAmountTooHigh": {
+ "title": "Amount must be lower than {{maxAmountFromFormatted}}"
+ },
+ "SwapGenericAPIError": {
+ "title": "Exchange rate expired",
+ "description": "You must confirm the swap before the timer runs out. The exchange rate remains valid for a fixed duration."
+ },
+ "PolkadotElectionClosed": {
+ "title": "Validators election must be closed"
+ },
+ "PolkadotNotValidator": {
+ "title": "Some selected addresses are not validators"
+ },
+ "PolkadotLowBondedBalance": {
+ "title": "All bonded assets will be unbonded if < 1 DOT"
+ },
+ "PolkadotNoUnlockedBalance": {
+ "title": "You have no unbonded assets"
+ },
+ "PolkadotNoNominations": {
+ "title": "You have no nominations"
+ },
+ "PolkadotAllFundsWarning": {
+ "title": "Ensure you have enough balance left for future transaction fees"
+ },
+ "PolkadotDoMaxSendInstead": {
+ "title": "Balance cannot be below {{minimumBalance}}. Send max to empty account."
+ },
+ "PolkadotBondMinimumAmount": {
+ "title": "You must bond at least {{minimumBondAmount}}."
+ },
+ "PolkadotBondMinimumAmountWarning": {
+ "title": "You bonded balance should be at least {{minimumBondBalance}}."
+ },
+ "PolkadotMaxUnbonding": {
+ "title": "You have exceeded the unbond limit"
+ },
+ "PolkadotValidatorsRequired": {
+ "title": "You must select at least one validator"
+ },
+ "TaprootNotActivated": {
+ "title": "Taproot mainnet is not activated yet"
+ },
+ "generic": {
+ "title": "{{message}}",
+ "description": "Something went wrong. Please try again. If the problem persists, please save your logs using the button below and provide them to Ledger Support."
+ }
+ },
+ "bluetooth": {
+ "required": "Sorry, it looks like Bluetooth is disabled",
+ "locationRequiredTitle": "Location is required for Bluetooth LE",
+ "locationRequiredMessage": "On Android location permission is required to list Bluetooth LE devices.",
+ "checkEnabled": "Please enable Bluetooth in your phone settings."
+ },
+ "location": {
+ "required": "Location services required",
+ "open": "Open location settings",
+ "disabled": "Ledger Live requires location services to pair your device through Bluetooth.",
+ "noInfos": "Ledger does not access your location information."
+ },
+ "permissions": {
+ "open": "Open app permissions"
+ },
+ "fees": {
+ "speed": {
+ "high": "High",
+ "standard": "Standard",
+ "low": "Low",
+ "slow": "slow",
+ "medium": "medium",
+ "fast": "fast",
+ "custom": "Custom",
+ "blockCount": "{{blockCount}} blocks"
+ }
+ },
+ "signout": {
+ "confirm": "Are you sure you want to sign out?",
+ "disclaimer": "All account data will be removed from your phone.",
+ "action": "Sign me out"
+ },
+ "auth": {
+ "failed": {
+ "biometrics": {
+ "title": "{{biometricsType}} unlock failed",
+ "description": "Enter your password to continue",
+ "authenticate": "Please authenticate with the Ledger Live app"
+ },
+ "denied": "Auth Security was not enabled because your phone failed to authenticate.",
+ "title": "Authentication failed",
+ "buttons": {
+ "tryAgain": "Try again",
+ "reset": "Reset"
+ }
+ },
+ "unlock": {
+ "biometricsTitle": "Please authenticate with the Ledger Live app",
+ "title": "Welcome back",
+ "desc": "Enter your password to continue",
+ "inputPlaceholder": "Type your password",
+ "login": "Log in",
+ "forgotPassword": "I lost my password"
+ },
+ "addPassword": {
+ "placeholder": "Choose your password",
+ "title": "Password Lock"
+ },
+ "confirmPassword": {
+ "title": "Confirm Password",
+ "placeholder": "Confirm your password"
+ },
+ "enableBiometrics": {
+ "title": "{{biometricsType}}",
+ "desc": "Unlock with {{biometricsType}}"
+ }
+ },
+ "reset": {
+ "description": "Please uninstall then reinstall the app on your phone to delete Ledger Live data, including accounts and settings.",
+ "button": "Reset"
+ },
+ "graph": {
+ "week": "1W",
+ "month": "1M",
+ "year": "1Y",
+ "all": "ALL"
+ },
+ "carousel": {
+ "title": "Close banner?",
+ "description": "We'll inform you of any new announcements.",
+ "confirm": "Confirm",
+ "undo": "Undo",
+ "banners": {
+ "valentine": {
+ "title": "Valentine’s day",
+ "description": "Reduced fees on Buy and Sell"
+ },
+ "academy": {
+ "title": "Ledger Academy",
+ "description": "Everything you need to know about Blockchain"
+ },
+ "stakeCosmos": {
+ "title": "Stake COSMOS",
+ "description": "Delegate your ATOM earn rewards today."
+ },
+ "familyPack": {
+ "title": "Family pack",
+ "description": "Get your family into crypto with 3 Nano S"
+ },
+ "buyCrypto": {
+ "title": "Buy Crypto",
+ "description": "Buy crypto on Ledger Live"
+ },
+ "swap": {
+ "title": "Swap crypto",
+ "description": "Securely exchange one crypto for another"
+ },
+ "algorand": {
+ "title": "Algorand",
+ "description": "Earn ALGO rewards with each transaction."
+ },
+ "sell": {
+ "title": "Sell crypto",
+ "description": "Sell Bitcoin directly from Ledger Live."
+ },
+ "vote": {
+ "title": "Vote with your ledger",
+ "description": "Vote directly from your Ledger Wallet."
+ },
+ "lending": {
+ "title": "Lend crypto",
+ "description": "Lend asset on the Compound Protocol"
+ },
+ "blackfriday": {
+ "title": "BLACK FRIDAY",
+ "description": "Enjoy 40% off with promo code BLACKFRIDAY20"
+ }
+ }
+ },
+ "onboarding": {
+ "stepWelcome": {
+ "title": "Welcome to Ledger",
+ "subtitle": "Safely manage your cryptos from your smartphone.",
+ "start": "Get started",
+ "noDevice": "No device?",
+ "buy": "Buy a {{fullDeviceName}}"
+ },
+ "stepLanguage": {
+ "title": "Select your language",
+ "cta": "Continue",
+ "warning": {
+ "title": "Get started in English",
+ "cta": "Got it!",
+ "desc": "We are introducing additional languages in order to help your onboarding with Ledger. Please be aware that the rest of the Ledger experience is currently only available in English."
+ }
+ },
+ "stepSelectDevice": {
+ "title": "Select your device",
+ "nanoS": "Nano S",
+ "nanoSP": "Nano S+",
+ "nanoX": "Nano X",
+ "chooseDevice": "Choose your device"
+ },
+ "stepUseCase": {
+ "firstUse": "First time using your Nano?",
+ "recovery": "Already have a recovery phrase?",
+ "deviceActions": {
+ "setup": {
+ "title": "Set up a new Nano",
+ "subTitle": "Let’s start and set up your device!"
+ },
+ "pairing": {
+ "title": "Connect your Nano",
+ "subTitle": "Your device is already set up? Connect it to the app!"
+ },
+ "desktopSync": {
+ "title": "Sync crypto from your desktop app",
+ "subTitle": "Already got the desktop app? Sync it to manage your crypto from your smartphone!"
+ },
+ "restore": {
+ "title": "Restore your recovery phrase on a new device",
+ "subTitle": "Use an existing recovery phrase to restore your private keys on a new Nano!"
+ }
+ }
+ },
+ "stepNewDevice": {
+ "title": "BASICS",
+ "0": {
+ "label": "Basics",
+ "title": "Access your crypto",
+ "desc": "Your crypto assets are stored on the blockchain. You need a private key to access and manage them.",
+ "action": "Don’t have a Nano? Discover the app"
+ },
+ "1": {
+ "label": "Basics",
+ "title": "Own your private key",
+ "desc": "Your private key is stored within your Nano and you must be the only one to own it to be in control of your money."
+ },
+ "2": {
+ "label": "Basics",
+ "title": "Stay offline",
+ "desc": "Ledger Live allows you to buy, sell, manage, exchange and earn crypto while remaining protected. You will validate every crypto transaction with your Nano."
+ },
+ "3": {
+ "label": "Basics",
+ "title": "Validate transactions",
+ "desc": "Your Nano works as a \"cold storage\" wallet. This means that it never exposes your private key online, even when using the app."
+ },
+ "4": {
+ "label": "Basics",
+ "title": "Let’s set up your Nano!",
+ "desc": "We'll start by setting up your Nano security.",
+ "action": "Let’s do this!"
+ },
+ "cta": "Let’s do this!"
+ },
+ "stepSetupDevice": {
+ "start": {
+ "title": "The best way to get you started:",
+ "bullets": {
+ "0": {
+ "label": "Plan 30 minutes and take your time."
+ },
+ "1": {
+ "label": "Grab a pen to write with."
+ },
+ "2": {
+ "label": "Stay alone, and choose a safe and quiet environment."
+ }
+ },
+ "cta": "Continue",
+ "warning": {
+ "title": "Please be careful",
+ "desc": "Make sure you follow the instructions on this app at every step of the process.",
+ "ctaText": "Got it!"
+ }
+ },
+ "setup": {
+ "bullets": {
+ "0": {
+ "title": "Turn on Nano",
+ "label": {
+ "nanoX": "Turn on your device by pressing the black button for 1 second.",
+ "nanoS": "Turn on your device by connecting it to the USB port of your phone.",
+ "nanoSP": "Turn on your device by connecting it to the USB port of your phone."
+ }
+ },
+ "1": {
+ "title": "Browse",
+ "label": "Learn how to interact with your device by reading the on-screen instructions."
+ },
+ "2": {
+ "title": "Select “Set up as new device”",
+ "label": "Press both buttons simultaneously to validate the selection."
+ },
+ "3": {
+ "title": "Follow instructions",
+ "label": "Come back here to follow instructions on your PIN code."
+ }
+ },
+ "cta": "Next step"
+ },
+ "pinCode": {
+ "title": "PIN code",
+ "desc": "Your PIN code is the first layer of security. It physically secures access to your Nano and your private keys. Your PIN code must be 4 to 8 digits long.",
+ "checkboxDesc": "I understand that I must choose my PIN code by myself and keep it private.",
+ "cta": "Set up PIN code"
+ },
+ "recoverySheetInstructions": {
+ "bullets": {
+ "0": {
+ "title": "Take your Recovery sheet",
+ "desc": "Grab a blank Recovery sheet, included with your Nano. Please reach out to Ledger Support if the Recovery sheet did not come blank."
+ },
+ "1": {
+ "title": "Confirm PIN code",
+ "desc": "Write Word #1 displayed on your Nano in position 1 of your Recovery sheet. Then press right on your Nano to display Word #2 and write it down in position 2. Repeat the process for all words while carefully respecting the order and spelling. Press left on your Nano to check for any mistakes."
+ }
+ },
+ "cta": "Confirm recovery phrase"
+ },
+ "pinCodeSetup": {
+ "bullets": {
+ "0": {
+ "title": "Choose PIN code",
+ "desc": "Press the left or right button to change digits. Press both buttons to validate a digit. Select to confirm your PIN code. Select to erase a digit."
+ },
+ "1": {
+ "title": "Confirm PIN code",
+ "desc": "Enter your PIN code again to confirm it."
+ }
+ },
+ "cta": "Next step",
+ "infoModal": {
+ "title": "Secure your PIN code",
+ "bullets": {
+ "0": "Always choose a PIN code by yourself.",
+ "1": "Always enter your PIN code out of sight.",
+ "2": "You can change your PIN code if needed.",
+ "3": "Three wrong PIN code entries in a row will reset the device.",
+ "4": "Never use an easy PIN code like 0000, 123456, or 55555555.",
+ "5": "Never share your PIN code with someone else. Not even with Ledger.",
+ "6": "Never use a PIN code you did not choose yourself.",
+ "7": "Never store your PIN code on a computer or phone."
+ }
+ }
+ },
+ "recoveryPhrase": {
+ "title": "Recovery phrase",
+ "desc": "Your recovery phrase is a secret list of 24 words that backs up your private keys.",
+ "desc_1": "Your Nano generates a unique recovery phrase. Ledger does not keep a copy of it.",
+ "cta": "Recovery phrase",
+ "checkboxDesc": "I understand that if I lose this recovery phrase, I will not be able to access my crypto in case I lose access to my Nano."
+ },
+ "recoveryPhraseSetup": {
+ "infoModal": {
+ "title": "How does a recovery phrase work?",
+ "desc": "Your recovery phrase works like a unique master key. Your Ledger device uses it to calculate private keys for every crypto asset you own.",
+ "desc_1": "To restore access to your crypto, any wallet can calculate the same private keys from your recovery phrase.",
+ "link": "More about the recovery phrase",
+ "title_1": "What if I lose access to my Nano?",
+ "bullets": {
+ "0": {
+ "label": "Get a new hardware wallet."
+ },
+ "1": {
+ "label": "Select “Restore recovery phrase on a new device” in the Ledger app."
+ },
+ "2": {
+ "label": "Enter your recovery phrase on your new device to restore access to your crypto."
+ }
+ }
+ },
+ "bullets": {
+ "0": {
+ "title": "Confirm your recovery phrase",
+ "label": "Scroll through the words until you find Word #1 by pressing the right button. Validate by pressing both buttons."
+ },
+ "1": {
+ "title": "Repeat for all words!"
+ }
+ },
+ "cta": "Confirm recovery phrase",
+ "nextStep": "Next step"
+ },
+ "hideRecoveryPhrase": {
+ "title": "Hide your recovery phrase",
+ "desc": "Your recovery phrase is your last chance to access your crypto if you cannot use your Nano. You must keep it in a safe place.",
+ "bullets": {
+ "0": {
+ "label": "Enter these words on a hardware wallet only, never on computers or phones."
+ },
+ "1": {
+ "label": "Never share your 24 words with anyone, not even with Ledger."
+ }
+ },
+ "cta": "OK, I’m done!",
+ "infoModal": {
+ "label": "Learn how to hide it",
+ "title": "Where should I keep my recovery phrase?",
+ "bullets": {
+ "0": {
+ "label": "NEVER enter it on a computer, phone or any other device. Don't take a picture of it."
+ },
+ "1": {
+ "label": "NEVER share your 24 words with anyone."
+ },
+ "2": {
+ "label": "ALWAYS store it in a secure place, out of sight."
+ },
+ "3": {
+ "label": "Ledger will never ask for your recovery phrase."
+ },
+ "4": {
+ "label": "If any person or application asks for it, assume it is a scam!"
+ }
+ }
+ },
+ "warning": {
+ "title": "Now game on!",
+ "desc": "Answer 3 simple questions to avoid common misconceptions about your hardware wallet.",
+ "cta": "Let’s take the quiz"
+ }
+ }
+ },
+ "stepRecoveryPhrase": {
+ "importRecoveryPhrase": {
+ "title": "Restore from recovery phrase",
+ "desc": "Restore your Nano from your recovery phrase to restore, replace or back up your Ledger hardware wallet.",
+ "desc_1": "Your Nano will restore your private keys and you will be able to access and manage your crypto.",
+ "cta": "OK, I’m ready!",
+ "warning": {
+ "title": "We recommend Ledger recovery phrases only",
+ "desc": "Ledger cannot guarantee the security of external recovery phrases. We recommend setting up your Nano as a new device if your recovery phrase was not generated by a Ledger.",
+ "cta": "Got it!"
+ },
+ "nextStep": "Next step",
+ "bullets": {
+ "0": {
+ "title": "Turn on Nano",
+ "nanoX": {
+ "label": "Turn on your device by pressing the black button for 1 second."
+ },
+ "nanoS": {
+ "label": "Turn on your device by connecting it to the USB port of your phone."
+ },
+ "nanoSP": {
+ "label": "Turn on your device by connecting it to the USB port of your phone."
+ },
+ "blue": {
+ "label": "Turn on your device by connecting it to the USB port of your phone and pressing the power button."
+ }
+ },
+ "1": {
+ "title": "Browse",
+ "label": "Learn how to interact with your device by reading the on-screen instructions."
+ },
+ "2": {
+ "title": "Select “Restore from recovery phrase”",
+ "label": "Press both buttons simultaneously to validate the selection."
+ },
+ "3": {
+ "title": "Follow instructions",
+ "label": "Come back here to follow instructions on your PIN code."
+ }
+ }
+ },
+ "existingRecoveryPhrase": {
+ "title": "Enter your recovery phrase",
+ "paragraph1": "Your recovery phrase is the secret list of words that you backed up when you first set up your wallet.",
+ "paragraph2": "Ledger does not keep a copy of your recovery phrase.",
+ "checkboxDesc": "I understand that if I lose my recovery phrase, I will not be able to access my crypto in case I lose access to my Nano.",
+ "bullets": {
+ "0": {
+ "title": "Grab your recovery phrase"
+ },
+ "1": {
+ "title": "Select recovery phrase length",
+ "label": "Your recovery phrase can have 12, 18 or 24 words. You must enter all words to access your crypto."
+ },
+ "2": {
+ "title": "Enter Word #1...",
+ "label": "Enter the first letters of Word #1 by selecting them with the right or left button. Press both buttons to validate each letter."
+ },
+ "3": {
+ "title": "Validate Word #1...",
+ "label": "Choose Word #1 from the suggestions. Press both buttons to validate."
+ },
+ "4": {
+ "title": "Repeat for all words!"
+ }
+ },
+ "nextStep": "Next step"
+ }
+ },
+ "stepPairNew": {
+ "nanoX": {
+ "title": "Pair your Nano",
+ "desc": "This is the first time you’re setting up your Nano with this phone. Let’s quickly pair your device.",
+ "cta": "Let’s pair my Nano"
+ },
+ "nanoS": {
+ "title": "Connect your Nano",
+ "desc": "This is the first time you’re setting up your Nano with this phone. Let’s quickly connect your device.",
+ "cta": "Let’s connect my Nano"
+ },
+ "nanoSP": {
+ "title": "Connect your Nano",
+ "desc": "This is the first time you’re setting up your Nano with this phone. Let’s quickly connect your device.",
+ "cta": "Let’s connect my Nano"
+ },
+ "blue": {
+ "title": "Connect your Blue",
+ "desc": "This is the first time you’re setting up your Blue with this phone. Let’s quickly connect your device.",
+ "cta": "Let’s connect my Blue"
+ },
+ "infoModal": {
+ "title": "Where can I find my device name?",
+ "desc": "On your device, select Settings > General > Device name.",
+ "title_1": "How to set up Bluetooth connection?",
+ "title_2": "How do I use my Nano X without Bluetooth?",
+ "desc_1": "Use an <1>OTG-cable1> to connect your Ledger Nano X to your Android smartphone (iOS not supported). Manage your crypto with Ledger Live mobile or any other compatible (web) app.",
+ "bullets": {
+ "0": {
+ "label": "Make sure Bluetooth is enabled on your smartphone and on your Ledger Nano X. Your Ledger Nano X should be on the Dashboard, its main home screen."
+ },
+ "1": {
+ "label": "<1>{{Os}}1>: Make sure location services are enabled in your phone's settings for Ledger Live. Ledger Live never uses your location information, this is a requirement for Bluetooth on {{Os}}."
+ },
+ "2": {
+ "label": "If you have a Bluetooth pairing issue, please refer to the following article",
+ "link": "Fix connection issues."
+ }
+ }
+ },
+ "errorInfoModal": {
+ "title": "Something went wrong?",
+ "title_1": "I have an Android",
+ "title_2": "Update Android version",
+ "desc": "If you're experiencing Bluetooth issues with your Nano X, please remove the pairing and forget Nano X on your phone. Then try the pairing again.",
+ "desc_1": "It may take a while before the Bluetooth pairing request is displayed. Make sure you verify and confirm the pairing code both on your Nano X and on your phone.",
+ "desc_2": "Check in your phone's Bluetooth settings whether the Ledger Nano X is detected. If it not detected, make sure you turn on the Bluetooth on your Ledger Nano X.",
+ "desc_3": "Location services",
+ "desc_4": "If the Ledger Nano X is not detected in your mobile app when trying to pair it, please try the following solution:",
+ "desc_5": "Ledger mobile app asks you to allow location services when it's not enabled yet, but on a few phone models, this is not always properly detected. Please note that Ledger mobile app never uses your location information, this permission is simply required for Bluetooth on Android.",
+ "desc_6": "Some users have reported having fixed their connection issues by updating the Android version running on their phone to a newer one. Please check with your phone manufacturer whether an update is available.",
+ "link": "Learn more",
+ "bullets": {
+ "0": {
+ "label": "Navigate to the system options for the Ledger Live app on your Android phone."
+ },
+ "1": {
+ "label": "Allow the location to be used."
+ },
+ "2": {
+ "label": "Return to the mobile app."
+ },
+ "3": {
+ "label": "Check whether your Nano X is detected."
+ }
+ }
+ }
+ },
+ "stepImportAccounts": {
+ "title": "Sync crypto from desktop",
+ "desc": "If you already have your crypto set up in the Ledger desktop app, you can sync them to manage them from your phone.",
+ "cta": "I’m ready to scan",
+ "bullets": {
+ "0": {
+ "label": "On the Desktop app, select <1>Settings > Accounts > Account export1>."
+ },
+ "1": {
+ "label": "Scan the LiveQR code with your phone."
+ },
+ "2": {
+ "label": "Select the crypto accounts to import."
+ }
+ },
+ "warning": {
+ "title": "Your desktop and mobile apps must be synced manually.",
+ "desc": "Ledger Live respects your privacy and stores your data locally. If you change accounts and settings on your phone, you will need to do the same on your computer, and vice versa. Your transactions stay synchronized with the blockchain.",
+ "cta": "Got it!"
+ }
+ },
+ "stepSetupPin": {
+ "step1": "Turn on your {{fullDeviceName}} and follow the instructions.",
+ "step1-nanoS": "Connect your {{fullDeviceName}} to your phone using an OTG cable.",
+ "step2": "Press both buttons together to select <1><1>Setup as a new device.1>1>",
+ "step2-restore": "Press both buttons together to select <1><1>Restore from recovery phrase.1>1>",
+ "step3": "Press left or right button to select a digit. Press buttons together to confirm.",
+ "step4prefix": "Select ",
+ "step4suffix1": " to confirm your PIN.",
+ "step4suffix2": " to erase last digit.",
+ "modal": {
+ "step1": "Always choose <1><1>your own1>1> PIN",
+ "step2": "Use 8 digits for greater security",
+ "step3": "Never use a device with a PIN or recovery phrase already setup"
+ }
+ },
+ "stepWriteRecovery": {
+ "step1": "Write <1><1>Word #11>1> in entry 1 on a blank Recovery sheet",
+ "step2": "Press the right button and continue to write down all 24 words.",
+ "step3": "Confirm each word of your recovery phrase: select it then confirm it by pressing both buttons together.",
+ "modal": {
+ "step1": "Store your 24-word recovery phrase in a safe place, out of sight.",
+ "step2": "Make sure you are the only person to have the recovery phrase.",
+ "step3": "Ledger does not save your recovery phrase.",
+ "step4": "Never use a device with a recovery phrase or PIN already setup."
+ }
+ },
+ "stepPassword": {
+ "desc": "Set a password to protect Ledger Live data on your phone.",
+ "descConfigured": "Password lock enabled successfully",
+ "setPassword": "Set the password",
+ "modal": {
+ "step1": "Keep your password safe. Do not share it.",
+ "step2": "Keep your password safe. Losing it means resetting Ledger Live and loading the accounts again.",
+ "step3": "Resetting Ledger Live has no impact on your crypto assets."
+ }
+ },
+ "stepFinish": {
+ "title": "Your device is ready!",
+ "readOnlyTitle": "All set!",
+ "desc": "Install apps on your device and manage your portfolio",
+ "cta": "Open Ledger Live"
+ },
+ "quizz": {
+ "label": "Quiz",
+ "modal": {
+ "success": "Congrats!",
+ "fail": "Incorrect!"
+ },
+ "nextQuestion": "Next question",
+ "finish": "Finish quiz",
+ "coins": {
+ "title": "As a Ledger user, my crypto is stored:",
+ "answers": {
+ "correct": "On the blockchain",
+ "wrong": "On my Nano"
+ },
+ "modal": {
+ "text": "Your crypto is always stored on the blockchain. Your hardware wallet only holds your private key, which gives you access to your crypto.",
+ "cta": "Next question"
+ }
+ },
+ "recoveryPhrase": {
+ "title": "If my recovery phrase is no longer secret and safe...",
+ "answers": {
+ "correct": "My crypto is no longer safe and I need to transfer them to a secure place",
+ "wrong": "No problem, Ledger can send me a copy"
+ },
+ "modal": {
+ "text": "Anyone who knows your recovery phrase can steal your crypto assets. \nIf you lose it, you must quickly transfer your crypto to a secure place.",
+ "cta": "Next question"
+ }
+ },
+ "privateKey": {
+ "title": "When I connect my Nano to the Ledger app, my private key is...",
+ "answers": {
+ "correct": "Still offline",
+ "wrong": "Briefly connected to the internet"
+ },
+ "modal": {
+ "text": "Your private key always remains offline in your hardware wallet. Even when connected to your Nano, the Ledger app cannot access your private key. You must physically authorize every transaction on your device.",
+ "cta": "Finish quiz"
+ }
+ },
+ "final": {
+ "successTitle": "Already a pro!",
+ "successText": "You are ready to safely manage your crypto. Only one quick step left!",
+ "failTitle": "You will soon become a pro...",
+ "failText": "Don’t worry, Ledger is here to guide you through your journey. You will soon feel extra comfortable about your crypto safety. Only one quick step left!",
+ "cta": "Next step"
+ }
+ },
+ "warning": {
+ "recoveryPhrase": {
+ "title": "Do not use a recovery phrase that you have not generated yourself.",
+ "desc": "Your Ledger hardware wallet’s pin code and recovery phrase should be initialized by you and only you. If you have received a device with a pre-existing seed word or an already initialized pin code do not use the product and contact our customer support.",
+ "supportLink": "Contact customer support"
+ },
+ "seed": {
+ "title": "Please check the box contents",
+ "desc": "If your {{deviceName}} came with a PIN code or recovery phrase, it’s not safe to use and you should contact Ledger Support.",
+ "warning": "Only use a recovery phrase that your device displayed when it was set up",
+ "continueCTA": "Continue",
+ "contactSupportCTA": "Contact support"
+ }
+ }
+ },
+ "tabs": {
+ "portfolio": "Portfolio",
+ "accounts": "Accounts",
+ "transfer": "Transfer",
+ "manager": "Device",
+ "settings": "Settings",
+ "platform": "Discover",
+ "nanoX": "Nano X"
+ },
+ "portfolio": {
+ "totalBalance": "Total balance",
+ "syncError": "Sync error",
+ "syncFailed": "Synchronization failed",
+ "syncPending": "Synchronizing...",
+ "transactionsPendingConfirmation": {
+ "title": "Unsynchronized balance",
+ "desc": "Some transactions are not confirmed yet. These will be reflected in your balance and useable after being confirmed."
+ },
+ "emptyState": {
+ "noAppsTitle": "Install an app on my device",
+ "noAppsDesc": "Install apps on your device before adding accounts in Ledger Live. Go to the Manager to install apps.",
+ "noAccountsTitle": "You don't have any accounts…",
+ "noAccountsDesc": "Please add accounts to your Portfolio.",
+ "buttons": {
+ "import": "Add asset",
+ "manager": "Install apps",
+ "managerSecondary": "Install apps on my device"
+ },
+ "addAccounts": {
+ "title": "Add assets",
+ "description": "You’re one step away from fully exploring Ledger Live and truly owning your money"
+ }
+ },
+ "noOpState": {
+ "title": "No operations yet?",
+ "desc": "Simply send crypto assets to your receiving address and wait for the app to sync."
+ },
+ "recommended": {
+ "title": "Recommended"
+ },
+ "topGainers": {
+ "title": "Top gainers (24H)",
+ "seeMarket": "See market"
+ }
+ },
+ "addAccountsModal": {
+ "ctaAdd": "Add accounts",
+ "ctaImport": "Import Desktop accounts",
+ "title": "Add Crypto",
+ "description": "You can choose to add crypto(s) directly with your Ledger, or import them from Ledger Live Desktop.",
+ "add": {
+ "title": "With your Ledger",
+ "description": "Create or import asset(s) with your Ledger"
+ },
+ "import": {
+ "title": "Import from desktop",
+ "description": "Import asset(s) from Ledger Live Desktop"
+ }
+ },
+ "byteSize": {
+ "bytes": "{{size}} bytes",
+ "kbUnit": "{{size}} Kb",
+ "mbUnit": "{{size}} Mb"
+ },
+ "time": {
+ "day": "1D",
+ "week": "1W",
+ "month": "1M",
+ "year": "1Y",
+ "all": "All",
+ "since": {
+ "day": "past day",
+ "week": "past week",
+ "month": "past month",
+ "year": "past year"
+ }
+ },
+ "orderOption": {
+ "choices": {
+ "name|asc": "Name A-Z",
+ "name|desc": "Name Z-A",
+ "balance|asc": "Lowest Balance",
+ "balance|desc": "Highest Balance"
+ }
+ },
+ "operations": {
+ "types": {
+ "IN": "Received",
+ "OUT": "Sent",
+ "CREATE": "Created",
+ "REVEAL": "Revealed",
+ "DELEGATE": "Delegated",
+ "UNDELEGATE": "Undelegated",
+ "REDELEGATE": "Redelegated",
+ "VOTE": "Voted",
+ "FREEZE": "Frozen",
+ "UNFREEZE": "Unfrozen",
+ "REWARD": "Claimed reward",
+ "FEES": "Fees",
+ "OPT_IN": "Opt in",
+ "OPT_OUT": "Opt out",
+ "CLOSE_ACCOUNT": "Close account",
+ "SUPPLY": "Deposited",
+ "REDEEM": "Withdrawn",
+ "APPROVE": "Enabled",
+ "BOND": "Bond",
+ "UNBOND": "Unbond",
+ "REWARD_PAYOUT": "Reward",
+ "SLASH": "Slash",
+ "WITHDRAW_UNBONDED": "Withdrawal",
+ "NOMINATE": "Nomination",
+ "CHILL": "Clear nominations",
+ "SET_CONTROLLER": "Set controller"
+ }
+ },
+ "operationDetails": {
+ "title": "Operation details",
+ "account": "Account",
+ "date": "Date",
+ "confirmed": "Confirmed",
+ "notConfirmed": "Not confirmed",
+ "failed": "Failed",
+ "fees": "Network fees",
+ "noFees": "No fee",
+ "from": "From",
+ "to": "To",
+ "identifier": "Transaction ID",
+ "viewOperation": "View in explorer",
+ "whatIsThis": "What is this operation?",
+ "seeAll": "See all",
+ "seeLess": "See less",
+ "viewInExplorer": "View in explorer",
+ "sending": "Sending...",
+ "receiving": "Receiving...",
+ "tokenOperations": "Token operations",
+ "subAccountOperations": "Subaccount operations",
+ "internalOperations": "Internal operations",
+ "tokenModal": {
+ "desc": "This operation is related to the following token operations"
+ },
+ "details": "{{ currency }} details",
+ "extra": {
+ "resource": "Resource",
+ "frozenAmount": "Frozen amount",
+ "unfreezeAmount": "Unfreeze amount",
+ "address": "Address",
+ "votes": "Votes ({{number}})",
+ "votesAddress": "<0>{{votes}}0> to <2>{{name}}2>",
+ "validators": "Validators",
+ "delegated": "Delegated ({{amount}})",
+ "delegatedTo": "Delegated to",
+ "delegatedAmount": "Delegated amount",
+ "redelegated": "Redelegated ({{amount}})",
+ "redelegatedFrom": "Redelegated from",
+ "redelegatedTo": "Redelegated to",
+ "redelegatedAmount": "Redelegated amount",
+ "undelegated": "Undelegated ({{amount}})",
+ "undelegatedFrom": "Undelegated from",
+ "undelegatedAmount": "Undelegated amount",
+ "rewardFrom": "Reward from",
+ "rewardAmount": "Amount collected",
+ "memo": "Memo",
+ "assetId": "Asset ID",
+ "rewards": "Earned rewards",
+ "bondedAmount": "Bonded Amount",
+ "unbondedAmount": "Unbonded Amount",
+ "withdrawUnbondedAmount": "Withdraw Unbonded Amount",
+ "palletMethod": "Method",
+ "transferAmount": "Transfer Amount",
+ "validatorsCount": "Validators ({{number}})"
+ },
+ "multipleAddresses": "Why multiple addresses?"
+ },
+ "operationList": {
+ "noOperations": "No Operations",
+ "noMoreOperations": "No other Operations"
+ },
+ "selectableAccountsList": {
+ "deselectAll": "Deselect all",
+ "selectAll": "Select all",
+ "tokenCount": "+1 token",
+ "tokenCount_plural": "+{{count}} tokens",
+ "subaccountCount": "+1 subaccount",
+ "subaccountCount_plural": "+{{count}} subaccounts"
+ },
+ "account": {
+ "tokens": {
+ "contractAddress": "Contract address",
+ "viewInExplorer": "View in explorer",
+ "seeMore": "Display more Tokens",
+ "seeLess": "Display fewer Tokens",
+ "addTokens": "Add token",
+ "howTo": "To add token accounts, you need to <0>receive funds0> using your <1>{{currency}} address1>.",
+ "algorand": {
+ "contractAddress": "Contract address",
+ "viewInExplorer": "View in explorer",
+ "seeMore": "See more ASA",
+ "seeLess": "See less ASA",
+ "addTokens": "Add ASA",
+ "howTo": "You can add assets to your Algorand account.",
+ "addAsa": "Add ASA (Asset)",
+ "howAsaWorks": "How ASA (asset) works?"
+ }
+ },
+ "subaccounts": {
+ "seeMore": "See more subaccounts",
+ "seeLess": "See fewer subaccounts"
+ },
+ "send": "Send",
+ "receive": "Receive",
+ "buy": "Buy",
+ "walletconnect": "WalletConnect",
+ "sell": "Sell",
+ "swap": "Swap",
+ "manage": "Manage",
+ "lastOperations": "Last operations",
+ "emptyState": {
+ "title": "No crypto assets yet?",
+ "desc": "Make sure the <1><0>{{managerAppName}}0>1> app is installed and start receiving.",
+ "descWithBuy": "Make sure the <1><0>{{managerAppName}}0>1> app is installed so you can buy or receive <3><0>{{currencyTicker}}0>3>",
+ "descToken": "Make sure the <1><0>{{managerAppName}}0>1> app is installed and start receiving <3><0>{{currencyTicker}}0>3> and <5><0>{{tokenType}}0> tokens5>",
+ "buttons": {
+ "receiveFunds": "Receive",
+ "buyCrypto": "Buy"
+ }
+ },
+ "settings": {
+ "header": "Account settings",
+ "title": "Edit account",
+ "advancedLogs": "Advanced logs",
+ "accountName": {
+ "title": "Account name",
+ "desc": "Description of the account",
+ "placeholder": "Account name"
+ },
+ "accountUnits": {
+ "title": "Edit units"
+ },
+ "unit": {
+ "title": "Unit",
+ "desc": "Choose the unit to be displayed."
+ },
+ "currency": {
+ "title": "Currency"
+ },
+ "endpointConfig": {
+ "title": "Node",
+ "desc": "The API node to use"
+ },
+ "delete": {
+ "title": "Remove account from the Portfolio",
+ "desc": "Stored data will be removed.",
+ "confirmationTitle": "Are you sure?",
+ "confirmationDesc": "This has no impact on your crypto assets. Existing accounts can always be added afterwards."
+ },
+ "archive": {
+ "title": "Archive account",
+ "desc": "This account will be archived."
+ },
+ "advanced": {
+ "title": "Advanced logs",
+ "desc": ""
+ }
+ },
+ "import": {
+ "scan": {
+ "title": "Import from desktop",
+ "descTop": {
+ "line1": "In Ledger Live Desktop, go to:",
+ "line2": "Settings > Accounts > Export accounts > Export"
+ },
+ "descBottom": "Open Ledger Live Desktop & Scan QR Code"
+ },
+ "result": {
+ "title": "Import accounts",
+ "newAccounts": "New accounts",
+ "updatedAccounts": "Updated accounts",
+ "empty": "Add a new account",
+ "descEmpty": "<0><0>No accounts0>0> were found. Please try again or go back to Setup",
+ "alreadyImported": "Already imported",
+ "noAccounts": "Nothing to import",
+ "unsupported": "Unsupported",
+ "settings": "App settings",
+ "includeGeneralSettings": "Import Desktop settings"
+ },
+ "fallback": {
+ "header": "Import account",
+ "title": "Enable camera",
+ "desc": "Please enable Camera in Settings to scan QR codes.",
+ "buttonTitle": "Go to Settings"
+ }
+ },
+ "availableBalance": "Available balance",
+ "totalSupplied": "Amount deposited",
+ "tronFrozen": "Frozen",
+ "bandwidth": "Bandwidth",
+ "energy": "Energy",
+ "delegatedAssets": "Delegated assets",
+ "undelegating": "Undelegating",
+ "delegation": {
+ "sectionLabel": "Delegation(s)",
+ "addDelegation": "Add delegation",
+ "info": {
+ "title": "Earn rewards",
+ "cta": "Earn rewards"
+ }
+ },
+ "claimReward": {
+ "sectionLabel": "Rewards",
+ "cta": "Claim"
+ },
+ "undelegation": {
+ "sectionLabel": "Undelegation(s)"
+ }
+ },
+ "accounts": {
+ "title": "Accounts",
+ "importNotification": {
+ "message": "Your accounts were imported successfully!"
+ },
+ "row": {
+ "syncPending": "Synchronizing...",
+ "upToDate": "Synchronized",
+ "error": "Error",
+ "queued": "Awaiting",
+ "showTokens": "Show {{length}} token",
+ "showTokens_plural": "Show {{length}} tokens",
+ "showSubAccounts": "Show {{length}} subaccount",
+ "showSubAccounts_plural": "Show {{length}} subaccounts",
+ "hideTokens": "Hide token",
+ "hideTokens_plural": "Hide tokens",
+ "hideSubAccounts": "Hide subaccount",
+ "hideSubAccounts_plural": "Hide subaccounts",
+ "algorand": {
+ "showTokens": "Show {{length}} ASA",
+ "showTokens_plural": "Show {{length}} ASA",
+ "hideTokens": "Hide ASA",
+ "hideTokens_plural": "Hide ASA"
+ }
+ }
+ },
+ "distribution": {
+ "header": "Asset allocation",
+ "title": "Assets",
+ "list": "Asset allocation ({{count}})",
+ "assets": "asset",
+ "assets_plural": "assets",
+ "total": "Total balance:",
+ "listAccount": "Account allocation ({{count}})"
+ },
+ "help": {
+ "gettingStarted": {
+ "title": "Getting started",
+ "desc": "Start here"
+ },
+ "helpCenter": {
+ "title": "Ledger Support",
+ "desc": "Get help"
+ },
+ "ledgerAcademy": {
+ "title": "Ledger Academy",
+ "desc": "Learn about crypto"
+ },
+ "facebook": {
+ "title": "Facebook",
+ "desc": "Like our page"
+ },
+ "twitter": {
+ "title": "Twitter",
+ "desc": "Follow us"
+ },
+ "github": {
+ "title": "Github",
+ "desc": "Review our code"
+ },
+ "status": {
+ "title": "Ledger Status",
+ "desc": "Check our system status"
+ }
+ },
+ "settings": {
+ "header": "Settings",
+ "resources": "Ledger resources",
+ "display": {
+ "title": "General",
+ "desc": "Configure general Ledger Live settings.",
+ "carousel": "Carousel visibility",
+ "carouselDesc": "Enable visibility of the carousel on Portfolio",
+ "language": "Display language",
+ "languageDesc": "Set the language displayed in Ledger Live.",
+ "password": "Password lock",
+ "passwordDesc": "Set a password to protect Ledger Live data on your phone.",
+ "counterValue": "Preferred currency",
+ "theme": "Theme",
+ "themeDesc": "Set the app UI theme",
+ "themes": {
+ "dark": "Dark",
+ "dusk": "Dusk",
+ "light": "Light"
+ },
+ "counterValueDesc": "Choose the currency for balances and operations.",
+ "exchange": "Rate provider",
+ "exchangeDesc": "Set provider of exchange rate from Bitcoin to {{fiat}}",
+ "stock": "Regional market indicator",
+ "stockDesc": "Choose Western to display market increases in green or Eastern to display them in red.",
+ "reportErrors": "Bug reports",
+ "reportErrorsDesc": "Automatically send reports to help Ledger improve its products.",
+ "developerMode": "Developer mode",
+ "developerModeDesc": "Show developer apps in the Manager and enable Testnet apps.",
+ "analytics": "Analytics",
+ "analyticsDesc": "Enable analytics to help Ledger improve user experience.",
+ "analyticsModal": {
+ "title": "Share analytics",
+ "desc": "Enable analytics to help Ledger improve user experience",
+ "bullet0": "Clicks",
+ "bullet1": "In-app page visits",
+ "bullet2": "Redirect to webpage",
+ "bullet3": "Actions: send, receive, lock, etc",
+ "bullet4": "End of page scroll",
+ "bullet5": "App (un)installation and version",
+ "bullet6": "Number of accounts, currencies and operations",
+ "bullet7": "Overall and page session duration",
+ "bullet8": "Ledger device type and firmware"
+ },
+ "technicalData": "Technical data",
+ "technicalDataDesc": "Ledger will automatically collect anonymized technical data to improve user experience.",
+ "technicalDataModal": {
+ "title": "Technical data",
+ "desc": "Ledger will automatically collect anonymized technical data to improve user experience.",
+ "bullet1": "Anonymous unique application ID",
+ "bullet2": "Ledger Live version, OS region, language and region"
+ },
+ "hideEmptyTokenAccounts": "Hide empty token accounts",
+ "hideEmptyTokenAccountsDesc": "Hide empty token accounts on the Accounts page."
+ },
+ "currencies": {
+ "header": "Currencies",
+ "rateProvider": "Rate Provider ({{currencyTicker}}→BTC)",
+ "rateProviderDesc": "Choose the provider of the rate between {{currencyTicker}} and Bitcoin.",
+ "confirmationNb": "Number of confirmations",
+ "confirmationNbDesc": "Set the number of network confirmations for transactions to be approved.",
+ "currencySettingsTitle": "{{currencyName}} Settings",
+ "placeholder": "No settings for this asset"
+ },
+ "accounts": {
+ "header": "Accounts",
+ "title": "Accounts",
+ "desc": "Manage assets display in the app.",
+ "hideTokenCTA": "Hide token",
+ "showContractCTA": "Show contract",
+ "blacklistedTokens": "Hidden token list",
+ "blacklistedTokensDesc": "You can hide tokens by going to the accounts list then long-pressing on the token and selecting 'Hide token'.",
+ "blacklistedTokensModal": {
+ "title": "Hide token",
+ "desc": "This action will hide all <1><0>{tokenName}0>1> accounts, you can show them again using <3>Settings > Accounts3>.",
+ "confirm": "Hide token"
+ },
+ "cryptoAssets": {
+ "header": "Crypto assets",
+ "title": "Crypto assets",
+ "desc": "Select a crypto assets to edit its settings."
+ }
+ },
+ "about": {
+ "title": "About",
+ "desc": "App information, terms and conditions, and privacy policy.",
+ "appDescription": "With Ledger Live, Secure, Buy, Sell, Exchange, Grow and Manage your crypto. All-in-one place.",
+ "appVersion": "Version",
+ "termsConditions": "Terms and conditions",
+ "termsConditionsDesc": "By using Ledger Live you are deemed to have accepted our terms and conditions.",
+ "privacyPolicy": "Privacy policy",
+ "privacyPolicyDesc": "See what personal data are collected, why and how they are used.",
+ "liveReview": {
+ "title": "Rate the app!",
+ "ios": "Review in the App Store",
+ "android": "Review in the Google Play store"
+ }
+ },
+ "help": {
+ "title": "Help",
+ "header": "Help",
+ "desc": "Learn more about Ledger Live or how to get help.",
+ "support": "Ledger Support",
+ "supportDesc": "If you have a problem, get help using Ledger Live with your hardware wallet.",
+ "configureDevice": "Set up a device",
+ "configureDeviceDesc": "Set up as a new device or restore an existing device. Accounts and settings are preserved.",
+ "clearCache": "Clear cache",
+ "clearCacheDesc": "The transactions on the network will be scanned and your accounts will be recalculated.",
+ "clearCacheModal": "Are you sure?",
+ "clearCacheModalDesc": "A fresh and complete scan of transactions on the network will be done. Account histories will be rebuilt and balances will be recalculated.",
+ "clearCacheButton": "Clear",
+ "exportLogs": "Save logs",
+ "exportLogsDesc": "Saving Ledger Live logs may be needed for troubleshooting purposes.",
+ "hardReset": "Reset Ledger Live",
+ "hardResetDesc": "This has no impact on your assets. Erases all data in Ledger Live, including account data, transaction histories and settings. Use your Ledger device to reload and manage your crypto assets on an empty Ledger Live.",
+ "repairDevice": "Repair your Ledger device",
+ "repairDeviceDesc": "If you have an issue updating your device and cannot resume updates, you can try this to repair your device."
+ },
+ "experimental": {
+ "title": "Experimental features",
+ "desc": "Try out the Experimental features and let us know what you think.",
+ "disclaimer": "These are Experimental features provided on an \"as is\" basis for our community of tech enthusiasts. They may change, break or be removed at any time. By enabling them, you agree to use them at your own risk."
+ }
+ },
+ "migrateAccounts": {
+ "banner": "Ledger Live accounts update",
+ "overview": {
+ "headerTitle": "Account update",
+ "title": "Ledger Live accounts update",
+ "subtitle": "New features in Ledger Live means your accounts need to be updated",
+ "notice": "{{accountCount}} account could not be updated. Please connect the device associated to the account below.",
+ "notice_plural": "{{accountCount}} accounts could not be updated. Please connect the device associated to the accounts below.",
+ "currency": "1 {{currency}} account needs to be updated",
+ "currency_plural": "{{count}} {{currency}} accounts need to be updated",
+ "start": "Start update",
+ "continue": "Continue update"
+ },
+ "progress": {
+ "headerTitle": "Updating accounts",
+ "pending": {
+ "title": "{{currency}} update in progress",
+ "subtitle": "Please wait for your account to be updated."
+ },
+ "notice": {
+ "title": "{{currency}} update incomplete",
+ "subtitle": "No {{currency}} accounts could be updated using this device.",
+ "cta": "Continue",
+ "ctaNextCurrency": "Continue with {{currency}}"
+ },
+ "done": {
+ "title": "{{currency}} update complete",
+ "subtitle": "Congratulations, your {{currency}} accounts were updated successfully.",
+ "cta": "Continue",
+ "ctaNextCurrency": "Continue with {{currency}}",
+ "ctaDone": "Done"
+ },
+ "error": {
+ "cta": "Retry"
+ }
+ },
+ "connectDevice": {
+ "headerTitle": "Connect device"
+ }
+ },
+ "transfer": {
+ "send": {
+ "title": "Send"
+ },
+ "fees": {
+ "title": "Edit fees"
+ },
+ "receive": {
+ "title": "Receive",
+ "titleReadOnly": "Unverified address",
+ "headerTitle": "Crypto asset",
+ "titleDevice": "Connect device",
+ "verifySkipped": "Your receive address was not confirmed on your Ledger device. Please verify your {{accountType}} address to stay secure.",
+ "verifyPending": "Please verify that the {{currencyName}} address displayed on your device exactly matches the one displayed on your phone.",
+ "verified": "Address confirmed. Check again if you copied or scanned it.",
+ "verifyAgain": "Check again",
+ "noAccount": "No account found",
+ "address": "Address for account",
+ "copyAddress": "Copy address",
+ "shareAddress": "Share address",
+ "addressCopied": "Address copied!",
+ "notSynced": {
+ "text": "Synchronizing",
+ "desc": "This may take a moment if you have many transactions or if you have a slow internet connection."
+ },
+ "readOnly": {
+ "title": "Receive",
+ "text": "Please be careful",
+ "desc": "You are about to view an address which was not verified. Verify addresses on your device to stay secure.",
+ "verify": "The address of {{accountType}} was not verified. Verify addresses on your device to stay secure."
+ }
+ },
+ "exchange": {
+ "title": "Buy / Sell"
+ },
+ "swap": {
+ "title": "Swap",
+ "selectDevice": "Select your device",
+ "broadcasting": "Broadcasting swap",
+ "loadingFees": "Loading network fees...",
+ "landing": {
+ "header": "Swap",
+ "title": "Welcome to Swap",
+ "whatIsSwap": "What is Swap?",
+ "disclaimer": "Exchange crypto directly from your Ledger device. This service is not available in some countries, including the US."
+ },
+ "unauthorizedRates": {
+ "cta": "Reset verification",
+ "banner": "Reset your KYC and update Live to swap with Wyre.",
+ "bannerCTA": "Reset KYC"
+ },
+ "main": {
+ "header": "Swap"
+ },
+ "kyc": {
+ "disclaimer": "I acknowledge that my location data will be shared with third parties for compliance purposes.",
+ "cta": "Continue",
+ "wyre": {
+ "title": "Complete your KYC",
+ "subtitle": "Your information is collected by LEDGER on behalf of and transferred to WYRE for KYC purposes. For more information, please check our Privacy Policy",
+ "pending": {
+ "cta": "Continue",
+ "title": "KYC submitted for approval",
+ "subtitle": "Your KYC has been submitted and is pending approval.",
+ "link": "Learn more about KYC"
+ },
+ "approved": {
+ "cta": "Continue",
+ "title": "KYC approved!",
+ "subtitle": "Your KYC has been submitted and has been approved.",
+ "link": "Learn more about KYC"
+ },
+ "closed": {
+ "cta": "Reset KYC",
+ "title": "KYC application rejected",
+ "subtitle": "Wyre rejected the data you provided for KYC purposes",
+ "link": "Learn more about KYC"
+ },
+ "form": {
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "street1": "Street Address Line 1",
+ "street2": "Street Address Line 2",
+ "city": "City",
+ "state": "State",
+ "country": "Country",
+ "postalCode": "Zip Code",
+ "dateOfBirth": "Date of birth",
+
+ "firstNamePlaceholder": "Enter your first name",
+ "lastNamePlaceholder": "Enter your last name",
+ "street1Placeholder": "Eg. 13, Maple street",
+ "street2Placeholder": "Eg. 13, Maple street",
+ "cityPlaceholder": "Eg. San José",
+ "postalCodePlaceholder": "Enter your 5 digit Zip code",
+ "statePlaceholder": "Select your state",
+ "dateOfBirthPlaceholder": "YYYY-MM-DD",
+
+ "firstNameError": "Enter your first name to continue",
+ "lastNameError": "Enter your last name to continue",
+ "street1Error": "Enter your address",
+ "cityError": "Enter the city you live in the USA",
+ "stateError": "Select your state to continue",
+ "postalCodeError": "Enter a valid US ZIP code",
+ "dateOfBirthError": "Enter your date of birth"
+ }
+ },
+ "states": "Select your state"
+ },
+ "notAvailable": {
+ "title": "Service temporarily unavailable, or not available in your country"
+ },
+ "payoutModal": {
+ "title": "Payout fees",
+ "description": "Payout fees are not shown on the device and substracted from the amount.",
+ "cta": "Close"
+ },
+ "pendingOperation": {
+ "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your {{targetCurrency}}.",
+ "label": "Your Swap ID:",
+ "title": "Swap broadcast successfully ",
+ "disclaimer": "Take note of your Swap ID number in case you’d need assistance from {{provider}} support.",
+ "cta": "See details"
+ },
+ "tradeMethod": {
+ "float": "Floating rate",
+ "floatUnavailable": "Floating rate not supported for this pair",
+ "fixed": "Fixed rate",
+ "fixedUnavailable": "Fixed rate not supported for this pair",
+ "floatDesc": "Your amount could change depending on the market conditions.",
+ "fixedDesc": "Your amount will remain the same irrespective of the changes on the market. Fixed rate updates every 30 seconds",
+ "by": "By",
+ "modalTitle": "Floating or fixed rate?"
+ },
+ "form": {
+ "validate": "Confirm Swap transaction",
+ "tab": "Exchange",
+ "button": "Continue",
+ "from": "From",
+ "to": "To",
+ "balance": "Balance <0>1230>",
+ "fromAccount": "Select account",
+ "toAccount": "Select account",
+ "paraswapCTA": "Looking for Paraswap? It has been moved to the Discover tab!",
+ "amount": {
+ "useMax": "Use max",
+ "available": "Total available"
+ },
+ "noApp": {
+ "title": "{{appName}} app not installed",
+ "desc": "Please go to Manager to install the {{appName}} app.",
+ "cta": "Go to Manager",
+ "close": "Close"
+ },
+ "noAccounts": {
+ "title": "No {{ticker}} to swap",
+ "desc": "Your {{currencyName}} accounts have no balance to swap.",
+ "addAccountCta": "Add Account",
+ "cta": "Close"
+ },
+ "outdatedApp": {
+ "title": "App update available",
+ "desc": "You need to update the {{appName}} app.",
+ "cta": "Go to Manager",
+ "close": "Close"
+ },
+ "summary": {
+ "from": "From",
+ "to": "To",
+ "send": "Send",
+ "payoutNetworkFees": "Payout fees",
+ "payoutNetworkFeesTooltip": "This amount will not be shown on your device",
+ "receive": "Amount",
+ "receiveFloat": "Amount to receive before service fees",
+ "provider": "Provider",
+ "method": "Rate",
+ "fees": "Max fees",
+ "disclaimer": {
+ "title": "Terms and conditions",
+ "desc": "By clicking “Accept”, I acknowledge and accept that this service is exclusively governed by <0>{{provider}}0>'s Terms & Conditions.",
+ "tos": "Terms & conditions",
+ "accept": "Accept",
+ "reject": "Close"
+ }
+ }
+ },
+ "operationDetails": {
+ "swapId": "Swap ID",
+ "provider": "Provider",
+ "date": "Date",
+ "from": "From",
+ "fromAmount": "Amount sent",
+ "to": "To",
+ "toAmount": "Amount to receive",
+ "statusTooltips": {
+ "expired": "Please contact the swap provider with your swap ID for more information.",
+ "refunded": "Please contact the swap provider with your swap ID for more information.",
+ "pending": "Please wait while the swap provider is processing the transaction.",
+ "onhold": "Please contact the swap provider with your swap ID to solve the situation.",
+ "finished": "Your swap was completed successfully."
+ }
+ },
+ "missingApp": {
+ "title": "Please install the Exchange app on your device",
+ "description": "Go to manager and install Exchange app to swap assets",
+ "button": "Go to Manager"
+ },
+ "outdatedApp": {
+ "title": "Please update the Exchange app on your device",
+ "description": "Go to manager and update Exchange app to swap assets",
+ "button": "Go to Manager"
+ },
+ "emptyState": {
+ "title": "No {{currency}} account",
+ "description": "You need to add an account before swapping {{currency}}",
+ "CTAButton": "Add account"
+ },
+ "history": {
+ "tab": "History",
+ "disclaimer": "Your swap mobile transactions are not synchronized with Ledger Live Desktop",
+ "exportButton": "Export operations",
+ "exportFilename": "Swap operations",
+ "empty": {
+ "title": "Your previous swaps will appear here",
+ "desc": "Either you have not made any swaps yet, or Ledger Live has been reset in the meantime."
+ }
+ }
+ },
+ "lending": {
+ "title": "Lend crypto",
+ "titleTransferTab": "Lend",
+ "actionTitle": "Lend",
+ "accountActions": {
+ "approve": "Approve",
+ "supply": "Supply",
+ "withdraw": "Withdraw"
+ },
+ "highFees": {
+ "title": "High fees on Ethereum",
+ "description": "Due to congestion on the Ethereum network, you may experience high fees when issuing transactions."
+ },
+ "account": {
+ "amountSupplied": "Amount deposited",
+ "amountSuppliedTooltip": "Amount lent to the network",
+ "currencyAPY": "Currency APY",
+ "currencyAPYTooltip": "Yearly return rate of a deposit that’s continuously compounded",
+ "accruedInterests": "Interest balance",
+ "accruedInterestsTooltip": "Interest generated on your lent assets",
+ "interestEarned": "Interest earned",
+ "interestEarnedTooltip": "Interest you have earned after withdrawal",
+ "openLoans": "Open loans",
+ "closedLoans": "Closed loans"
+ },
+ "banners": {
+ "needApproval": "You need to approve this account before being able to lend assets.",
+ "fullyApproved": "You have fully approved this account. You can reduce the amount at a fee.",
+ "approvedCanReduce": "You have approved <0>{{value}}0> on this account. You can reduce the amount at a fee.",
+ "approvedButNotEnough": "You have approved <0>{{value}}0> on this account.",
+ "approving": "You can deposit assets once the account approval is confirmed.",
+ "notEnough": "You must increase the limit approved on your account to lend."
+ },
+ "howDoesLendingWork": "How does lending works",
+ "dashboard": {
+ "tabTitle": "Dashboard",
+ "assetsTitle": "Assets to lend",
+ "accountsTitle": "Approved accounts",
+ "emptySateDescription": "You can lend assets directly from your Ethereum accounts and earn interest.",
+ "apy": "{{value}} APY",
+ "activeAccount": {
+ "account": "Account",
+ "amountSupplied": "Amount deposited",
+ "interestEarned": "Interest earned",
+ "status": "Account status",
+ "EARNING": "Earning",
+ "ENABLING": "Approving",
+ "INACTIVE": "Inactive",
+ "SUPPLYING": "Supplying",
+ "approve": "Approve",
+ "supply": "Supply",
+ "withdraw": "Withdraw",
+ "amountRedeemed": "Amount withdrawn",
+ "endDate": "End date"
+ }
+ },
+ "closedLoans": {
+ "tabTitle": "Closed loans",
+ "description": "View all your withdrawn loans and the interest you have earned.",
+ "cta": "Lend asset"
+ },
+ "history": {
+ "tabTitle": "History",
+ "description": "View the history of all your loan transactions.",
+ "cta": "Lend asset"
+ },
+ "terms": {
+ "label": "Lend crypto",
+ "title": "Lend assets on the Compound Protocol",
+ "description": "The Compound protocol allows you to lend and borrow assets on the Ethereum network. You can lend assets and earn interest directly from your Ledger acccount.",
+ "switchLabel": "I have read and agree with the <0><0>Terms of Use0>0>."
+ },
+ "info": {
+ "1": {
+ "label": "Step 1/3",
+ "title": "Approving an account allows the protocol to process future loans.",
+ "description": "You need to authorize the Compound smart contract to transfer up to a certain amount of assets to the protocol. Enabling an account gives permission to the protocol to process future loans."
+ },
+ "2": {
+ "label": "Step 2/3",
+ "title": "Deposit assets to earn interest.",
+ "description": "Once an account is approved, you can select the amount of assets you want to lend and issue a transaction to the protocol. Interest accrue immediately after the transaction is confirmed."
+ },
+ "3": {
+ "label": "Step 3/3",
+ "title": "Withdraw assets at any time.",
+ "description": "You can withdraw your assets and earned interest at any time, partially or entirely, directly from your Ledger account.",
+ "cta": "Lend now"
+ },
+ "title": "Lend crypto"
+ },
+ "noTokenAccount": {
+ "info": {
+ "title": "You don’t have a {{ name }} account.",
+ "description": "In order to deposit funds and lend crypto you need a {{ name }} account. Please Receive funds on your Ethereum address."
+ },
+ "buttons": {
+ "receive": "Receive {{ name }}",
+ "buy": "Buy {{ name }}"
+ }
+ },
+ "enable": {
+ "info": {
+ "title": "This account is not approved",
+ "description": "You need to approve an account before being able to lend this asset.",
+ "cta": "Approve Account"
+ },
+ "stepperHeader": {
+ "selectAccount": "Choose an account",
+ "enable": "Approve",
+ "advanced": "Advanced",
+ "amount": "Enter amount",
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "selectAccount": {
+ "enabledAccountsAmount": "You have {{number}} account that approved {{amount}}",
+ "enabledAccountsAmount_plural": "You have {{number}} accounts that approved {{amount}}",
+ "enabledAccountsNoLimit": "You have {{number}} account that approved an unlimited amount of {{ currency }}",
+ "enabledAccountsNoLimit_plural": "You have {{number}} accounts that approved an unlimited amount of {{ currency }}",
+ "noEnabledAccounts": "You need to approve an account before you can lend."
+ },
+ "enable": {
+ "summary": "<0>I grant the 0><1>{{contractName}}1><0> smart contract access to my 0><1>{{accountName}}1><0> account for a 0><1>{{amount}}1><0> amount0>",
+ "limit": "limited {{amount}}",
+ "noLimit": "no limit {{assetName}}",
+ "contractName": "Compound {{currencyName}}",
+ "advanced": "Advanced"
+ },
+ "advanced": {
+ "amountLabel": "Amount to approve",
+ "amountLabelTooltip": "This limits the amount available to the smart contract.",
+ "limit": "Limit",
+ "limited": "Limited",
+ "noLimit": "No Limit"
+ },
+ "amount": {
+ "totalAvailable": "Total available"
+ },
+ "validation": {
+ "success": "Operation sent successfully",
+ "info": "The approval was sent to the network for confirmation. You’ll be able to issue loans once it is confirmed.",
+ "extraInfo": "Transactions can take some time to be displayed in an explorer and to be confirmed.",
+ "button": {
+ "done": "Close"
+ }
+ }
+ },
+ "supply": {
+ "stepperHeader": {
+ "amount": "Supply",
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "amount": {
+ "totalAvailable": "Available balance",
+ "placeholderMax": "Withdraw Max"
+ },
+ "validation": {
+ "success": "Deposit sent successfully",
+ "info": "You will start earning interest once the network has confirmed the deposit.",
+ "extraInfo": "Transactions can take some time to be displayed in an explorer and to be confirmed.",
+ "button": {
+ "done": "Done",
+ "cta": "View details"
+ }
+ }
+ },
+ "withdraw": {
+ "stepperHeader": {
+ "amount": "Withdraw",
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "validation": {
+ "success": "Withdrawal sent successfully",
+ "info": "Your assets will be available once the network has confirmed the withdrawal.",
+ "button": {
+ "done": "Done",
+ "cta": "View details"
+ }
+ }
+ }
+ }
+ },
+ "sync": {
+ "error": "Sync Error",
+ "loading": "Sync"
+ },
+ "scanning": {
+ "loading": "Searching devices..."
+ },
+ "send": {
+ "tooMuchUTXOBottomModal": {
+ "cta": "Continue",
+ "description": "This transaction may take long to verify and sign because the account has a significant number of coins.",
+ "title": "UTXO message"
+ },
+ "highFeeModal": "Be careful, network fees are greater than <1>10%1> of the amount. Do you want to continue?",
+ "scan": {
+ "title": "Scan QR code",
+ "descBottom": "Please center the QR code inside the square.",
+ "fallback": {
+ "header": "Scan QR code",
+ "title": "Enable camera",
+ "desc": "Please enable Camera in Settings to scan QR codes.",
+ "buttonTitle": "Go to Settings"
+ }
+ },
+ "stepperHeader": {
+ "selectAccount": "Account to debit",
+ "recipientAddress": "Recipient address",
+ "selectAmount": "Amount",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "recipient": {
+ "scan": "Scan QR code",
+ "enterAddress": "Enter address",
+ "input": "Enter address",
+ "verifyAddress": "Please verify the address matches the one shared by the recipient."
+ },
+ "amount": {
+ "available": "Total available",
+ "useMax": "Use max",
+ "loadingNetwork": "Loading network fees...",
+ "noRateProvider": "Not available"
+ },
+ "summary": {
+ "subaccountsWarning": "You will need to refill this account with {{ currency }} in order to send the tokens of this account",
+ "tag": "Tag (Optional)",
+ "validateTag": "Validate tag",
+ "total": "Total",
+ "amount": "Amount",
+ "from": "From",
+ "to": "To",
+ "infoTotalTitle": "Total debit",
+ "infoTotalDesc": "Includes transaction amount and the selected network fees",
+ "gasLimit": "Gas limit",
+ "gasPrice": "Gas price",
+ "maxFees": "Max fees",
+ "validateGasLimit": "Validate gas limit",
+ "fees": "Fees",
+ "validateFees": "Validate Fees",
+ "customizeFees": "Customize Fees",
+ "memo": {
+ "title": "Memo",
+ "type": "Memo type",
+ "value": "Memo value"
+ },
+ "validateMemo": "Validate memo"
+ },
+ "validation": {
+ "message": "On your device, confirm the transaction to sign it securely.",
+ "sent": "Transaction sent",
+ "amount": "Amount",
+ "fees": "Fees",
+ "confirm": "Your account balance will be updated once the network confirms the transaction.",
+ "button": {
+ "details": "View details",
+ "retry": "Retry"
+ }
+ },
+ "fees": {
+ "title": "Network fees",
+ "validate": "Confirm",
+ "required": "Fees are required",
+ "chooseGas": "Choose a gas price",
+ "higherFaster": "Higher gas price means a faster confirmation.",
+ "edit": {
+ "title": "Choose unit"
+ },
+ "networkInfo": "Choose the amount of network fees to include in the transaction. The amount of fees affects the processing speed of the transaction"
+ },
+ "verification": {
+ "streaming": {
+ "accurate": "Loading... ({{percentage}})",
+ "inaccurate": "Loading..."
+ }
+ },
+ "info": {
+ "maxSpendable": {
+ "title": "Max spendable amount",
+ "description": "The maximum spendable amount is the total balance in an account that is available to send in a transaction."
+ }
+ }
+ },
+ "requestAccount": {
+ "stepperHeader": {
+ "selectCrypto": "Select crypto",
+ "selectAccount": "Select account",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "selectAccount": {
+ "addAccount": "Add {{currency}} account"
+ }
+ },
+ "freeze": {
+ "stepperHeader": {
+ "info": "Earn rewards",
+ "selectAmount": "Freeze",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "info": {
+ "description": "Freeze TRX to earn rewards securely, keeping control of your assets.",
+ "steps": {
+ "0": "Delegated assets remain yours.",
+ "1": "You can unfreeze your assets after 3 days.",
+ "2": "Freeze and vote securely with your Ledger device."
+ },
+ "howVotingWorks": "How voting works",
+ "cta": "Continue"
+ },
+ "amount": {
+ "available": "Total available",
+ "noRateProvider": "Not available",
+ "infoLabel": "Bandwidth or Energy"
+ },
+ "validation": {
+ "message": "Always verify that your device displays the address exactly as it was originally given to you.",
+ "success": "Assets frozen",
+ "amount": "Amount",
+ "info": "You will start earning {{resource}} once the network has confirmed the freeze. You can shortly vote for Super Representatives to also earn rewards.",
+ "button": {
+ "pending": "Transaction is being validated.",
+ "pendingDesc": "Please wait a moment before voting.",
+ "vote": "Vote",
+ "voteTimer": "0:{{time}}",
+ "later": "I will vote later",
+ "retry": "Retry"
+ }
+ }
+ },
+ "unfreeze": {
+ "stepperHeader": {
+ "selectAmount": "Unfreeze",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "amount": {
+ "title": "Select the type of asset to unfreeze",
+ "info": "Unfreezing will decrease your {{resource}} and will cancel all your votes.",
+ "cta": "Continue"
+ },
+ "validation": {
+ "success": "Your assets were unfrozen successfully",
+ "info": "Your {{resource}} points will be decreased and all your votes will be canceled.",
+ "button": {
+ "done": "Done",
+ "cta": "See details"
+ }
+ }
+ },
+ "claimReward": {
+ "stepperHeader": {
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "validation": {
+ "success": "Your rewards were added to the available balance.",
+ "button": {
+ "done": "Done",
+ "cta": "See details"
+ }
+ }
+ },
+ "vote": {
+ "stepperHeader": {
+ "selectValidator": "Cast votes",
+ "castVote": "My votes",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "selectValidator": {},
+ "castVotes": {
+ "ranking": "Ranking: <0>{{rank}}0>",
+ "nbOfVotes": "Number of votes {{amount}}",
+ "percentage": "Percentage",
+ "estYield": "Est. yield",
+ "addMoreVotes": "Add more votes",
+ "votesRemaining": "Votes remaining: <0>{{total}}0>",
+ "maxVotesAvailable": "Max votes available: <0>{{total}}0>",
+ "voteFor": "Vote for",
+ "validateVotes": "Validate votes",
+ "votesRequired": "Votes required ",
+ "allVotesUsed": "All votes used"
+ },
+ "validation": {
+ "message": "Always verify that your device displays the address exactly as it was originally given to you.",
+ "success": "Your votes were cast successfully",
+ "info": "",
+ "button": {}
+ }
+ },
+ "addAccounts": {
+ "supportLinks": {
+ "segwit_or_native_segwit": "Segwit or Native segwit?"
+ },
+ "quitConfirmation": {
+ "title": "Cancel add account",
+ "desc": "Are you sure you want to cancel adding accounts?"
+ },
+ "imported": "Accounts added successfully",
+ "sections": {
+ "importable": {
+ "title": "Add existing account"
+ },
+ "creatable": {
+ "title": "Add new account"
+ },
+ "imported": {
+ "title": "Accounts already in the Portfolio ({{length}})"
+ },
+ "migrate": {
+ "title": "Accounts to update"
+ }
+ },
+ "success": {
+ "desc": "View your accounts or add other accounts",
+ "secondaryCTA": "Add other accounts",
+ "cta": "Go to Accounts"
+ },
+ "stopScanning": "Stop scanning",
+ "retryScanning": "Retry scanning",
+ "retry": "Retry",
+ "done": "Done",
+ "finalCta": "Continue",
+ "finalCtaForSwap": "Go back to Swap",
+ "synchronizing": "Synchronizing",
+ "synchronizingDesc": "Your accounts are being synchronized. This can take a while...",
+ "noAccountToCreate": "Could not create <1><0>{{currencyName}}>> account. Restart the process and sync your accounts.",
+ "cantCreateAccount": "A new account cannot be added before receiving assets on your <1><0>{{accountName}}>> account.",
+ "tokens": {
+ "title": "Add token",
+ "createParentCurrencyAccount": "Add {{parrentCurrencyName}} account",
+ "erc20": {
+ "title": "Add token",
+ "disclaimer": "{{tokenName}} is an ERC20 Token.\nYou can receive tokens directly in an Ethereum account.",
+ "learnMore": "Learn more about ERC20"
+ },
+ "trc10": {
+ "title": "Add Token",
+ "disclaimer": "{{tokenName}} is a TRC10 Token.\nYou can receive tokens directly in a Tron account.",
+ "learnMore": "Learn more about TRC10"
+ },
+ "trc20": {
+ "title": "Add Token",
+ "disclaimer": "{{tokenName}} is a TRC20 Token.\nYou can receive tokens directly in a Tron account.",
+ "learnMore": "Learn more about TRC20"
+ }
+ },
+ "showMoreChainType": "More address types",
+ "addressTypeInfo": {
+ "title": "Which address type to choose?",
+ "subtitle": "Add new account",
+ "native_segwit": {
+ "title": "Native Segwit",
+ "desc": "Best overall. Low fees per transaction, error detection and Lightning Network support. Some wallets/exchanges may need to add support."
+ },
+ "segwit": {
+ "title": "Segwit",
+ "desc": "Higher fees per transaction than Native Segwit. Lightning support. Supported by most wallets."
+ },
+ "legacy": {
+ "title": "Legacy",
+ "desc": "{{currency}}’s original address format. Highest fees per transaction. Some wallets/exchanges still only support this type."
+ }
+ }
+ },
+ "DeviceAction": {
+ "stayInTheAppPlz": "Stay in the app and keep your Nano X nearby",
+ "allowAppPermission": "Open the {{wording}} app on your device",
+ "allowAppPermissionSubtitleToken": "to manage your {{token}} tokens",
+ "allowManagerPermission": "Allow {{wording}} on your device",
+ "loading": "Loading...",
+ "turnOnAndUnlockDevice": "Turn on and unlock your device",
+ "connectAndUnlockDevice": "Connect and unlock your device",
+ "unlockDevice": "Unlock your device",
+ "outdated": "App version outdated",
+ "outdatedDesc": "An important update is available for the {{appName}} application on your device. Please go to the Manager to update it.",
+ "quitApp": "Quit the application on your device",
+ "appNotInstalled": "Please install the {{appName}} app",
+ "appNotInstalled_plural": "Please install the {{appName}} apps",
+ "verifyAddress": {
+ "title": "Verify address on device",
+ "description": "Please verify that the {{currencyName}} address to be shown in Ledger Live matches the one on your Ledger device."
+ },
+ "confirmSwap": {
+ "title": "Confirm swap operation on device",
+ "alert": "Verify the Swap details on your device before sending it. The addresses are exchanged securely so you don’t have to verify them."
+ },
+ "confirmSell": {
+ "title": "Confirm sell operation on device",
+ "alert": "Verify the Sell details on your device before sending it. The addresses are exchanged securely so you don’t have to verify them."
+ },
+ "button": {
+ "openManager": "Open Manager"
+ },
+ "installApp": "{{appName}} App installation {{percentage}}",
+ "installAppDescription": "Please wait until the installation is finished",
+ "listApps": "Checking App dependencies",
+ "listAppsDescription": "Please wait while we make sure you have all the required apps"
+ },
+ "SelectDevice": {
+ "title": "Pair new device",
+ "bluetooth": {
+ "title": "Connect via Bluetooth...",
+ "label": "Detect your Nano automatically"
+ },
+ "deviceNotFoundPairNewDevice": "Add new Ledger Nano X",
+ "headerDescription": "Please make sure your {{productName}} is unlocked with Bluetooth enabled",
+ "usb": "... or connect USB cable",
+ "usbLabel": "Connect USB cable and enter your PIN Code on your device",
+ "withoutDeviceHeader": "I don't have my device with me",
+ "withoutDevice": "Continue without my device",
+ "steps": {
+ "connecting": {
+ "title": "Connecting {{deviceName}}",
+ "description": {
+ "ble": "Please make sure your {{productName}} is unlocked and within range",
+ "usb": "Please make sure your {{productName}} is unlocked"
+ }
+ },
+ "genuineCheck": {
+ "title": "Enable Ledger Manager on your {{productName}}",
+ "accept": "Please don't turn off your Nano X. Make sure you allow <1>Ledger Manager1>."
+ },
+ "genuineCheckPending": {
+ "title": "Checking if the device is genuine..."
+ },
+ "dashboard": {
+ "title": "Return to the Dashboard on your {{productName}}"
+ },
+ "currencyApp": {
+ "title": "Open the {{managerAppName}} app on your {{productName}}",
+ "description": "",
+ "footer": {
+ "appInstalled": "You don't have the app installed",
+ "goManager": "Go to the Manager"
+ }
+ },
+ "accountApp": {
+ "title": "Open the {{managerAppName}} app on your {{productName}}",
+ "description": ""
+ },
+ "receiveVerify": {
+ "title": "Verify address on device",
+ "description": "Please verify that the {{currencyName}} address displayed in Ledger Live exactly matches the one displayed on your Ledger device.",
+ "action": "Continue"
+ },
+ "getDeviceName": {
+ "title": "Press both buttons together to enable reading the device name."
+ },
+ "editDeviceName": {
+ "title": "Press both buttons together to enable setting the device name."
+ },
+ "listApps": {
+ "title": "Loading..."
+ }
+ }
+ },
+ "EditDeviceName": {
+ "title": "Rename device",
+ "charactersRemaining": "{{remainingCount}} characters remaining",
+ "action": "Confirm"
+ },
+ "PairDevices": {
+ "Paired": {
+ "title": "Pairing successful",
+ "desc": "Your {{productName}} is ready to be used with Ledger Live.",
+ "action": "Continue"
+ },
+ "Pairing": {
+ "title1": "Check matching codes",
+ "title2": "Validate",
+ "step1": "Validate if the code displayed on your phone exactly matches the one displayed on your {{productName}}.",
+ "step2": "Validate on your {{productName}} by pressing both buttons together."
+ },
+ "GenuineCheck": {
+ "title": "Device authentication check",
+ "accept": "Please don't turn off your Nano X. Make sure you allow <1>Ledger Manager1>.",
+ "info": "This step is meant to ensure that your Nano is authentical."
+ },
+ "ScanningHeader": {
+ "title": "Looking for devices",
+ "desc": "Please make sure your {{productName}} is unlocked and Bluetooth is enabled."
+ },
+ "ScanningTimeout": {
+ "title": "Sorry, no device was found",
+ "desc": "Please make sure your {{productName}} is unlocked and Bluetooth is enabled."
+ },
+ "bypassGenuine": "Use anyway",
+ "alreadyPaired": "Already paired"
+ },
+ "DeviceItemSummary": {
+ "genuine": "Your device is genuine",
+ "genuineFailed": "Device authentication <1><0>failed0>1>"
+ },
+ "DeviceNameRow": {
+ "title": "Name",
+ "action": "Get device name"
+ },
+ "RemoveDevice": {
+ "button": {
+ "title": "Remove {{nbDevices}} device",
+ "title_plural": "Remove {{nbDevices}} devices"
+ }
+ },
+ "manager": {
+ "tabTitle": "Device Manager",
+ "title": "Manager",
+ "connect": "Select your device",
+ "appsCatalog": "App catalog",
+ "installedApps": "Apps installed",
+ "myApps": "My apps",
+ "token": {
+ "title": "{{appName}} tokens",
+ "noAppNeeded": "{{tokenName}} is a token using the {{appName}} app. There is no app to install.",
+ "installApp": "{{tokenName}} is a token using the {{appName}} app. Install the {{appName}} app to be able to manage your tokens."
+ },
+ "tokenAppDisclaimer": "{{tokenName}} is an {{tokenType}} token using the {{appName}} app. To manage {{tokenName}}, <1>install the {{appName}} app1> and send the tokens <3>to your {{appName}} account3>.",
+ "tokenAppDisclaimerInstalled": "{{tokenName}} is an {{tokenType}} token using the {{appName}} app. To manage {{tokenName}}, <1>open the {{appName}} app1> and send the tokens <3>to your {{appName}} account3>.",
+ "goToAccounts": "Go to Accounts",
+ "intallParentApp": "Install {{appName}} app",
+ "readOnly": {
+ "title": "Nano X",
+ "description": "Setup Ledger Live with your Ledger Nano X to install apps, create accounts and make secure transactions wherever you go.",
+ "question": "Do you have your Ledger Nano X?",
+ "button": "Start Nano X setup",
+ "noDevice": "No device?",
+ "buy": "Buy a Ledger Nano X"
+ },
+ "appList": {
+ "title": "App catalog",
+ "loading": "Loading apps...",
+ "noApps": "No apps found",
+ "searchApps": "Search",
+ "searchAppsCatalog": "Search app in catalogue...",
+ "searchAppsInstalled": "Search installed apps...",
+ "noAppsInstalled": "No apps installed on your device",
+ "noAppsDescription": "Go to the App catalog to install apps",
+ "noResultsFound": "No results found",
+ "noResultsDesc": "Please verify the spelling and try again",
+ "versionNew": "(NEW{{newVersion}})"
+ },
+ "update": {
+ "title": "Update",
+ "subtitle": "Updates available",
+ "updateAll": "Update"
+ },
+ "uninstall": {
+ "title": "Uninstall",
+ "subtitle": "Uninstall all apps",
+ "description": "Uninstalling apps has no impact on your crypto assets. You can reinstall apps in the App catalog.",
+ "uninstallAll": "Uninstall all"
+ },
+ "remove": {
+ "title": "Remove device",
+ "description": "Are you sure? You can add your {{productName}} again at any time.",
+ "button": "Remove"
+ },
+ "storage": {
+ "title": "Storage",
+ "used": "Used",
+ "genuine": "Your device is genuine",
+ "appsToUpdate": "<0>{{number}}0> update",
+ "appsToUpdate_plural": "<0>{{number}}0> updates",
+ "appsInstalled": "<0>{{number}}0> app",
+ "appsInstalled_plural": "<0>{{number}}0> apps",
+ "storageAvailable": "available"
+ },
+ "installSuccess": {
+ "title": "App successfully installed",
+ "title_plural": "Apps successfully installed",
+ "description": "You can now add your {{app}} account",
+ "notSupported": "App installed successfully, find out more about the installed apps on our website.",
+ "manageAccount": "Add accounts",
+ "learnMore": "Learn more",
+ "later": "Do it later"
+ },
+ "firmware": {
+ "latest": "Firmware update is available",
+ "outdated": "Device firmware version is too old to be updated. Please contact Ledger Support for a replacement.",
+ "modalTitle": "Firmware update only available on Desktop",
+ "modalDesc": "Please download Ledger Live on your computer to update the device firmware.",
+ "contactUs": "Contact us"
+ }
+ },
+ "ManagerDevice": {
+ "title": "Device"
+ },
+ "AppAction": {
+ "install": {
+ "loading": {
+ "title": "Installing {{appName}}",
+ "desc": "Please wait for the {{appName}} app to be installed",
+ "button": "Installing...",
+ "button_plural": "Installing... {{progressPercentage}}%"
+ },
+ "done": {
+ "title": "App Successfully installed",
+ "description": "You can now add your {{app}} account",
+ "accounts": "Go to Accounts"
+ },
+ "dependency": {
+ "title": "{{dependency}} app is required",
+ "description_one": "The {{dependency}} app will also be installed because the {{app}} app needs it.",
+ "description_two": "Please press Continue to install the {{app}} and {{dependency}} apps."
+ },
+ "continueInstall": "Install apps"
+ },
+ "update": {
+ "title": "{{number}} app update",
+ "title_plural": "{{number}} app updates",
+ "step": "Update step {{step}}:",
+ "updateWarn": "Do not quit the Manager during the update.",
+ "progress": "Updating all...",
+ "button": "Update all",
+ "version": "New {{version}}",
+ "buttonAction": "Update available",
+ "buttonModal": "Update all apps",
+ "loading": "Updating...",
+ "titleModal": "Updates available"
+ },
+ "uninstall": {
+ "loading": {
+ "title": "Uninstalling {{appName}}",
+ "button": "Uninstalling..."
+ },
+ "done": {
+ "title": "Successfully uninstalled {{appName}} on your {{productName}}"
+ },
+ "dependency": {
+ "title": "Other apps need the {{app}} app",
+ "description_one": "Some apps are dependent of the {{app}} app.",
+ "description_two": "Some apps are dependent of the {{app}} app.\nApps on your device that are dependent on the {{app}} app will be uninstalled too."
+ },
+ "continueUninstall": "Uninstall {{app}} and other apps"
+ },
+ "filter": {
+ "title": "Filter",
+ "all": "All",
+ "installed": "Apps",
+ "not_installed": "Not installed",
+ "supported": "Live supported",
+ "updatable": "Updatable",
+ "apply": "Apply"
+ },
+ "sort": {
+ "title": "Sort",
+ "default": "Default",
+ "marketcap": "Marketcap",
+ "name": "Name",
+ "name_asc": "Name A-Z",
+ "name_desc": "Name Z-A",
+ "marketcap_desc": "Market cap"
+ }
+ },
+ "AuthenticityRow": {
+ "title": "Authentication",
+ "subtitle": "Genuine"
+ },
+ "RemoveRow": {
+ "title": "Remove device"
+ },
+ "FirmwareVersionRow": {
+ "title": "Firmware version",
+ "subtitle": "V {{version}}"
+ },
+ "FirmwareUpdateRow": {
+ "title": "Firmware version {{version}} available",
+ "subtitle": "Please use Ledger Live Desktop to update",
+ "action": "Update"
+ },
+ "FirmwareUpdate": {
+ "title": "Update firmware",
+ "Installing": {
+ "title": "{{stepName}}...",
+ "subtitle": "If requested on your device, please enter your PIN to finish the process."
+ },
+ "steps": {
+ "osu": "Installing OSU",
+ "flash-mcu": "MCU updating",
+ "flash-bootloader": "Bootloader updating",
+ "flash": "Flashing your device",
+ "firmware": "Firmware updating"
+ },
+ "newVersion": "Update Firmware to {{version}} is available",
+ "drawerUpdate": {
+ "title": "Firmware Update",
+ "description": "Update your Ledger Nano firmware by connecting it to the Ledger Live application on desktop"
+ }
+ },
+ "FirmwareUpdateReleaseNotes": {
+ "introTitle": "You are about to install <1><0>firmware version {{version}}.0>1>",
+ "introDescription1": "Please note that all the apps on your device will be deleted. You can reinstall your apps after the firmware update.",
+ "introDescription2": "This has no impact on your crypto assets.",
+ "action": "Continue update"
+ },
+ "ApplicationVersion": "v{{version}}",
+ "systemLanguageAvailable": {
+ "title": "Change your app's language?",
+ "description": {
+ "newSupport": "Good news! Our teams have been working hard and Ledger Live now supports {{language}}.",
+ "advice": "You can always change your language back later in the settings."
+ },
+ "switchButton": "Switch to {{language}}",
+ "no": "I prefer not to",
+ "languages": {
+ "en": "English",
+ "fr": "French",
+ "ru": "Russian",
+ "es": "Spanish",
+ "zh": "Chinese",
+ "de": "German"
+ }
+ },
+ "FirmwareUpdateCheckId": {
+ "title": "Identifier",
+ "description": "Please press both buttons together on your {{fullDeviceName}} if it displays the same identifier:"
+ },
+ "FirmwareUpdateMCU": {
+ "title": "Restart device",
+ "desc1": "Disconnect your device from your computer.",
+ "desc2": "Press and hold the left button, connect the USB cable then release the button when the Bootloader screen appears."
+ },
+ "FirmwareUpdateConfirmation": {
+ "title": "Firmware updated",
+ "description": "Go to Manager to reinstall apps on your device."
+ },
+ "RepairDevice": {
+ "title": "Repair",
+ "action": "My device is ready"
+ },
+ "StepLegacyModal": {
+ "description": "Imported accounts synchronize with the network only, not between Mobile and Desktop versions of Ledger Live."
+ },
+ "algorand": {
+ "token": "ASA (Assets)",
+ "claimRewards": {
+ "title": "Rewards",
+ "button": "Claim",
+ "stepperHeader": {
+ "info": "Earn rewards",
+ "starter": "Rewards",
+ "connectDevice": "Connect device",
+ "verification": "Verification",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "info": {
+ "description": "Delegate your Algo to earn rewards while keeping full security and control on your assets.",
+ "steps": {
+ "0": "A minimum balance of 1 Algo is required to receive rewards.",
+ "1": "Increase the account’s balance to increase rewards.",
+ "2": "Perform a transaction to or from the account to claim rewards."
+ },
+ "howItWorks": "How rewards work",
+ "cta": "Receive Algo"
+ },
+ "starter": {
+ "title": "Congratulations! You earned {{amount}}. Continue to claim your rewards.",
+ "howItWorks": "How rewards work",
+ "warning": "You will be prompted to generate an empty transaction to your account. This will add your current rewards to your balance at the minimal cost of the transaction fees.",
+ "cta": "Continue"
+ },
+ "verification": {
+ "success": {
+ "title": "Rewards successfully claimed!",
+ "text": "Your rewards were added to your available balance.",
+ "cta": "Go to account"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please check back in a few minutes to make sure your transaction did not go through before trying again."
+ }
+ }
+ }
+ },
+ "optIn": {
+ "stepperHeader": {
+ "selectToken": "Add ASA (Asset)",
+ "connectDevice": "Connect device",
+ "verification": "Verification",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "selectToken": {
+ "warning": {
+ "title": "Asset already added",
+ "description": "You already have {{token}} asset in your Algorand account."
+ }
+ },
+ "verification": {
+ "success": {
+ "title": "{{token}} asset successfully added!",
+ "text": "You can now receive and send {{token}} assets on your Algorand account.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please check back in a few minutes to make sure your transaction did not go through before trying again."
+ }
+ }
+ }
+ }
+ },
+ "cosmos": {
+ "info": {
+ "available": {
+ "title": "ATOM available",
+ "description": "This amount is disposable."
+ },
+ "delegated": {
+ "title": "Delegated assets",
+ "description": "Delegated assets are used for Cosmos voting. This is your total number of votes."
+ },
+ "undelegating": {
+ "title": "Undelegating",
+ "description": "Undelegated assets are in a timelock of 21 days, before being available."
+ },
+ "delegationUnavailable": {
+ "title": "Delegation unavailable",
+ "description": "Not enough available balance in your account to start a new delegation."
+ }
+ },
+ "delegation": {
+ "delegationEarn": "You can earn ATOM rewards by delegating your assets.",
+ "info": "How Delegation works",
+ "claimRewards": "Claim rewards",
+ "claimAvailableRewards": "Claim {{amount}}",
+ "header": "Delegation(s)",
+ "Amount": "Amount",
+ "noRewards": "No rewards available",
+ "delegate": "Delegate",
+ "undelegate": "Undelegate",
+ "redelegate": "Redelegate",
+ "reward": "Claim rewards",
+ "estYield": "Est. yield",
+ "stepperHeader": {
+ "starter": "Earn rewards",
+ "validator": "Delegate assets",
+ "amountSubTitle": "Amount to delegate",
+ "summary": "Summary",
+ "verification": "Verification",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "starter": {
+ "description": "Delegate your ATOM to earn rewards securely, keeping control of your assets.",
+ "steps": {
+ "0": "Delegated assets remain yours.",
+ "1": "You will have to wait 21 days for the undelegation to complete.",
+ "2": "Delegate securely with your Ledger device."
+ },
+ "warning": {
+ "description": "Choose your validator wisely: Part of your delegated assets may be irrevocably lost if the validator does not behave appropriately."
+ },
+ "cta": "Continue"
+ },
+ "validator": {
+ "validators": "Validators",
+ "myDelegations": "My delegations",
+ "cta": "Continue",
+ "estYield": "Est. yield: {{amount}}",
+ "totalAvailable": "Total available: <0>{{amount}}0>",
+ "allAssetsUsed": "All assets used",
+ "noResultsFound": "No validator found for <0>{{search}}0>",
+ "currentAmount": "(+ <0>{{amount}}0>)"
+ },
+ "amount": {
+ "assetsRemaining": "Assets remaining: <0>{{amount}}0>",
+ "allAssetsUsed": "All assets used",
+ "minAmount": "Minimum amount: <0>{{min}}0>",
+ "incorrectAmount": "Maximum amount: <0>{{max}}0>",
+ "cta": "Continue"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully delegated your assets",
+ "text": "Your account balance will be updated once the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ },
+ "drawer": {
+ "status": "Status",
+ "rewards": "Rewards",
+ "active": "Active",
+ "inactive": "Inactive",
+ "completionDate": "Completion date",
+ "redelegatedFrom": "Redelegated from"
+ }
+ },
+ "redelegation": {
+ "estYield": "est. yield",
+ "stepperHeader": {
+ "validator": "Choose new validator",
+ "amountSubTitle": "Amount to redelegate",
+ "amountTitle": "{{from}} → {{to}}",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "validator": {
+ "validators": "Validators",
+ "myDelegations": "My delegations",
+ "cta": "Continue",
+ "estYield": "Est. yield: {{amount}}",
+ "totalAvailable": "Total available: <0>{{amount}}0>",
+ "allAssetsUsed": "All assets used",
+ "noResultsFound": "No validator found for <0>{{search}}0>"
+ },
+ "amount": {
+ "newRedelegatedBalance": "New total for <0>{{name}}0> after operation: <0>{{amount}}0>"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully redelegated your assets",
+ "text": "Your account balance will be updated when the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ }
+ },
+ "undelegation": {
+ "stepperHeader": {
+ "amountSubTitle": "Amount to undelegate",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "amount": {
+ "allAssetsUsed": "All assets undelegated"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully undelegated your assets",
+ "text": "Your account balance will be updated when the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ }
+ },
+ "claimRewards": {
+ "stepperHeader": {
+ "validator": "Select reward to collect",
+ "method": "Claim rewards",
+ "summary": "Summary",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "flow": {
+ "steps": {
+ "method": {
+ "youEarned": "You earned",
+ "byDelegationAssetsTo": "by delegating assets to",
+ "claimReward": "Cash in",
+ "claimRewardCompound": "Compound",
+ "claimRewardInfo": "They will be claimed now and added to your available balance.",
+ "claimRewardCompoundInfo": "They will be claimed now and automatically delegated to the same validator.",
+ "compoundOrCashIn": "Compound or Cash in?",
+ "claimRewardTooltip": "Rewards will be added to the available balance",
+ "claimRewardCompoundTooltip": "Rewards will be added to the delegated amount",
+ "cta": "Continue"
+ },
+ "verification": {
+ "success": {
+ "title": "You have successfully claimed your rewards. They were added to your available balance.",
+ "titleCompound": "Your rewards were delegated to the same validator.",
+ "text": "Your account balance will be updated when the network confirms the transaction.",
+ "cta": "View details"
+ },
+ "pending": {
+ "title": "Broadcasting transaction..."
+ },
+ "broadcastError": "Your transaction may have failed. Please wait a moment then check the transaction history before trying again."
+ }
+ }
+ }
+ }
+ },
+ "tezos": {
+ "AccountHeader": {
+ "title": "You can earn rewards by delegating your account",
+ "btn": "Earn rewards"
+ }
+ },
+ "tron": {
+ "voting": {
+ "earnRewars": "Earn rewards",
+ "delegationEarn": "You can now earn rewards by freezing and voting.",
+ "howItWorks": "How voting works",
+ "startEarning": "Earn rewards",
+ "title": "Claim rewards",
+ "header": "Votes ({{total}})",
+ "Amount": "Amount",
+ "noRewards": "No rewards available",
+ "votes": {
+ "title": "Votes",
+ "description": "Cast your votes for one or more Representatives to start earning rewards.",
+ "cta": "Vote"
+ },
+ "rewards": {
+ "title": "Voting rewards",
+ "button": "Claim"
+ },
+ "manageVotes": "Manage votes",
+ "remainingVotes": {
+ "title": "You still have votes remaining",
+ "description": "Cast your remaining votes to earn more rewards."
+ },
+ "flow": {
+ "started": {
+ "title": "Vote",
+ "srOrCandidate": "SR or Candidate?",
+ "description": "Vote for one or more Super Representatives to start earning rewards.",
+ "button": {
+ "continue": "Cast votes"
+ }
+ },
+ "selectValidator": {
+ "sections": {
+ "title": {
+ "selected": "Selected",
+ "superRepresentatives": "Super Representatives",
+ "candidates": "Candidates"
+ }
+ }
+ }
+ }
+ },
+ "freeze": {
+ "flow": {
+ "steps": {
+ "starter": {
+ "title": "Earn rewards",
+ "description": "Delegate TRX to a third party candidate to earn rewards. Click on continue to freeze assets and vote.",
+ "bullet": {
+ "delegate": "Delegated assets remain yours.",
+ "access": "You can access your assets 3 days after freezing.",
+ "ledger": "Delegate securely with your Ledger device."
+ },
+ "button": {
+ "cta": "Continue"
+ }
+ }
+ }
+ }
+ },
+ "manage": {
+ "title": "Manage Tron Power",
+ "freeze": {
+ "title": "Freeze",
+ "description": "Freeze TRX to earn Bandwidth or Energy. You can also vote for Super Representatives."
+ },
+ "unfreeze": {
+ "title": "Unfreeze",
+ "description": "Unfreeze TRX to add them back to your available balance. You will no longer earn rewards."
+ },
+ "vote": {
+ "title": "Vote",
+ "description": "Cast votes for Super Representatives to earn rewards."
+ }
+ },
+ "info": {
+ "available": {
+ "title": "TRX available",
+ "description": "This amount is disposable."
+ },
+ "frozen": {
+ "title": "Frozen",
+ "description": "Frozen assets are used for Tron voting. This is your total number of votes."
+ },
+ "bandwidth": {
+ "title": "Bandwidth",
+ "description": "Bandwidth points are used to make transactions instead of paying TRX network fees. Choose Bandwidth to increase your daily free transactions."
+ },
+ "energy": {
+ "title": "Energy",
+ "description": "Energy points are required to execute smart contracts. If you don't run smart contracts, there is no need to have rewards in Energy points."
+ },
+ "claimRewards": {
+ "title": "Voting rewards",
+ "description": "The TRX generated as rewards during block production can be claimed every 24 hours."
+ },
+ "superRepresentative": {
+ "title": "Super Representatives (SR)",
+ "description": "Super Representatives play a key role in governing the TRON community by ensuring basic functions, e.g. block generation and bookkeeping."
+ },
+ "candidates": {
+ "title": "Candidates",
+ "description": "127 individuals elected through voting by the entire token-holder community. Votes are sampled every 6 hours."
+ }
+ }
+ },
+ "stellar": {
+ "memo": {
+ "title": "Memo",
+ "warning": "When using a Memo, carefully verify the type with the recipient."
+ },
+ "memoType": {
+ "MEMO_TEXT": "Memo Text",
+ "NO_MEMO": "No Memo",
+ "MEMO_ID": "Memo ID",
+ "MEMO_HASH": "Memo Hash",
+ "MEMO_RETURN": "Memo Return"
+ }
+ },
+ "polkadot": {
+ "lockedBalance": "Bonded balance",
+ "unlockingBalance": "Unbonding balance",
+ "unlockedBalance": "Unbonded balance",
+ "networkFees": "Network fees are automatically set by the Polkadot consensus, you won't be able to review them on your device",
+ "bondedBalanceBelowMinimum": "Your bonded balance is below the current minimum of {{minimumBondBalance}}. Your nominations are at risk of being removed.",
+ "info": {
+ "available": {
+ "title": "DOT available",
+ "description": "This amount can be sent anytime."
+ },
+ "locked": {
+ "title": "Bonded assets",
+ "description": "Assets must be bonded to nominated validators before earning rewards."
+ },
+ "unlocking": {
+ "title": "Unbonding assets",
+ "description": "Unbonding assets stay locked for 28 days before they can be withdrawn."
+ },
+ "unlocked": {
+ "title": "Unbonded assets",
+ "description": "Unbonded assets can now be moved using the withdraw operation."
+ },
+ "electionOpen": {
+ "title": "Validators election ongoing",
+ "description": "The election of new validators is currently ongoing. As such, staking operations are not available for 15 minutes at most."
+ },
+ "minBondWarning": {
+ "title": "Not enough bonded",
+ "description": "Your bonded balance is below the current minimum allowed to nominate. Your nominations are at risk of being removed."
+ }
+ },
+ "nomination": {
+ "emptyState": {
+ "title": "Earn rewards",
+ "description": "You can earn rewards by bonding assets and then nominating your validator(s).",
+ "info": "How nominations work",
+ "cta": "Earn rewards"
+ },
+ "header": "Nominations",
+ "nominate": "Nominate",
+ "chill": "Clear nominations",
+ "setController": "Change controller",
+ "status": "Status",
+ "totalStake": "Total Stake",
+ "amount": "Bonded Amount",
+ "commission": "Commission",
+ "active": "Active",
+ "activeInfo": "This validator is elected and is collecting rewards on your bonded assets.",
+ "inactive": "Inactive",
+ "inactiveInfo": "This validator is elected but is not collecting rewards on your bonded assets.",
+ "waiting": "Unelected",
+ "waitingInfo": "This validator is not elected, therefore is not collecting rewards.",
+ "notValidator": "Not a validator",
+ "notValidatorInfo": "This address is no longer a validator",
+ "elected": "Elected",
+ "electedInfo": "This validator is elected and is collecting rewards for its nominators.",
+ "nominators": "Nominators",
+ "nominatorsCount": "{{nominatorsCount}} nominators",
+ "nominatorsInfo": "This validator is currently elected by {{count}} nominators.",
+ "oversubscribed": "Oversubscribed ({{nominatorsCount}})",
+ "oversubscribedInfo": "Only the top {{maxNominatorRewardedPerValidator}} nominators with the highest bonded amount earn rewards",
+ "hasPendingBondOperation": "A bond operation is still pending confirmation",
+ "externalControllerUnsupported": "This stash account is controlled by a separate account whose address is <0>{{controllerAddress}}0>. To stake with Ledger Live, you must set this stash account as its own controller.",
+ "externalStashUnsupported": "This account is the controller of a separate stash account whose address is <0>{{stashAddress}}0>. To stake with Ledger Live, you must set your stash account as its own controller.",
+ "showInactiveNominations": "Show all nominations ({{count}})",
+ "hideInactiveNominations": "Show active nominations only",
+ "noActiveNominations": "There are no active nominations.",
+ "showAllUnlockings": "Show all unbonding amounts ({{count}})",
+ "hideAllUnlockings": "Hide unbonding amounts"
+ },
+ "unlockings": {
+ "header": "Unbonding",
+ "withdrawUnbonded": "Withdraw Unbonded",
+ "rebond": "Rebond"
+ },
+ "manage": {
+ "title": "Manage assets",
+ "bond": {
+ "title": "Bond",
+ "description": "To earn rewards, first bond an amount. Then you must nominate your validator(s)."
+ },
+ "unbond": {
+ "title": "Unbond",
+ "description": "To make a bonded amount available again, first you need to unbond it. You can withdraw it after the 28-day unbonding period."
+ },
+ "withdrawUnbonded": {
+ "title": "Withdraw Unbonded",
+ "description": "To retrieve an unbonded amount back to the available balance, you need to withdraw it manually."
+ },
+ "nominate": {
+ "title": "Nominate",
+ "description": "Choose up to 16 validators. Ensure nominations are Active to earn rewards."
+ },
+ "chill": {
+ "title": "Clear nominations",
+ "description": "Remove all nominations. You will stop earning rewards. Your bonded amount remains bonded."
+ }
+ },
+ "nominate": {
+ "stepperHeader": {
+ "validators": "Validators to nominate",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "validators": {
+ "myNominations": "My nominations",
+ "electedValidators": "Elected validators",
+ "waitingValidators": "Unelected validators",
+ "noResultsFound": "No validators found for <0>{{search}}0>",
+ "selected": "{{selected}} out of {{total}} selected",
+ "notValidatorsRemoved": "You have nominated {{count}} addresses who are no longer validators. They will automatically be removed from your nominate transaction.",
+ "maybeChill": "Clear nominations instead"
+ },
+ "validation": {
+ "success": {
+ "title": "You have successfully nominated validators",
+ "description": "You will start earning rewards when your assets are bonded to your elected validator(s)."
+ }
+ }
+ }
+ },
+ "bond": {
+ "rewardDestination": {
+ "label": "Reward Destination",
+ "stash": "Available balance",
+ "stashDescription": "Rewards are credited to your available balance.",
+ "staked": "Bonded balance",
+ "stakedDescription": "Rewards are credited to your bonded balance for compound earning.",
+ "optionTitle": "Note",
+ "optionDescription": "Once set, the option is fixed for the lifetime of this bond. If you change your mind, please read ",
+ "clickableLink": "here."
+ },
+ "stepperHeader": {
+ "starter": "Earn rewards",
+ "amount": "Amount to bond",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "starter": {
+ "description": "You may earn rewards by bonding your assets and then nominate your validators.",
+ "bullet": [
+ "You keep ownership of bonded assets",
+ "Nominate using your Ledger device",
+ "Assets will be available again, 28 days after unbonding"
+ ],
+ "help": "How nominations work",
+ "warning": "Choose your validators wisely: part of your bonded assets may be irrevocably lost if a validator does not behave appropriately."
+ },
+ "amount": {
+ "availableLabel": "Available",
+ "maxLabel": "Max"
+ },
+ "confirm": {
+ "info": "Bonded assets can be unbonded at any time, but unbonding takes 28 days."
+ },
+ "validation": {
+ "success": {
+ "title": "Assets successfully bonded",
+ "description": "You can nominate validators once the network has confirmed the transaction.",
+ "descriptionNominate": "You will be able to nominate validators once the network has confirmed your transaction.",
+ "nominate": "Nominate",
+ "later": "Nominate later"
+ },
+ "pending": {
+ "title": "Transaction awaiting confirmation",
+ "description": "You have to wait a moment before nominating"
+ }
+ }
+ }
+ },
+ "rebond": {
+ "stepperHeader": {
+ "amount": "Amount to rebond",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "amount": {
+ "availableLabel": "Unbonding",
+ "maxLabel": "Max"
+ },
+ "confirm": {
+ "info": "Rebonded assets are immediately added to the bonded amount."
+ },
+ "validation": {
+ "success": {
+ "title": "Assets successfully rebonded",
+ "description": "Your account balance will update once the network has confirmed the transaction."
+ }
+ }
+ }
+ },
+ "unbond": {
+ "stepperHeader": {
+ "amount": "Amount to unbond",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "amount": {
+ "availableLabel": "Bonded",
+ "maxLabel": "Max"
+ },
+ "confirm": {
+ "info": "Unbonded assets can be withdrawn after the 28-day unbonding period."
+ },
+ "validation": {
+ "success": {
+ "title": "Unbond transaction sent successfully",
+ "description": "Unbonded assets can be withdrawn after 28 days."
+ }
+ }
+ }
+ },
+ "simpleOperation": {
+ "modes": {
+ "withdrawUnbonded": {
+ "title": "Withdraw unbonded",
+ "description": "Withdraw unbonded assets to your available balance."
+ },
+ "chill": {
+ "title": "Clear nominations",
+ "description": "Clears all nominations and stops earning rewards.",
+ "info": "Bonded assets will remain bonded. If you unbond them, they will be available after 28 days."
+ },
+ "setController": {
+ "title": "Change Controller",
+ "description": "Set your Ledger account as its own controller",
+ "info": "Ledger Live doesn't support operations on separate stash and controller accounts."
+ }
+ },
+ "stepperHeader": {
+ "info": "Info",
+ "selectDevice": "Select device",
+ "connectDevice": "Connect device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "steps": {
+ "validation": {
+ "success": {
+ "title": "Transaction sent successfully",
+ "description": "You will see your operation in history once the network has confirmed the transaction."
+ }
+ }
+ }
+ }
+ },
+ "delegation": {
+ "overdelegated": "Overdelegated",
+ "delegationSendWarnDesc": "The amount to be sent will be deducted from your delegated account.",
+ "delegationReceiveWarnDesc": "The amount received in a delegated account will be added to the total staked amount. Choose another account if you want to avoid this.",
+ "iDelegateMy": "I delegate my",
+ "undelegateMy": "Undelegate my",
+ "warnUndelegation": "Your account will be undelegated.",
+ "warnDelegation": "Delegating your voting rights does not guarantee any rewards from your validator.",
+ "to": "to",
+ "from": "from",
+ "forAnEstYield": "for an est. yield of",
+ "yieldPerYear": "{{yield}} / Year",
+ "yieldInfos": "Yield rates are provided by",
+ "termsAndPrivacy": "I have read and I accept the <1>Ledger Live Terms of Use1> and <3>Privacy Policy3>.",
+ "delegation": "Delegation",
+ "viewDetails": "View details",
+ "validator": "Validator",
+ "validatorAddress": "Validator address",
+ "delegatedAccount": "Delegated account",
+ "duration": "Duration",
+ "transactionID": "Transaction ID",
+ "receive": "Receive more",
+ "changeValidator": "Change validator",
+ "endDelegation": "End delegation",
+ "durationForDays0": "Just now",
+ "durationForDays": "For a day",
+ "durationForDays_plural": "For {{count}} days",
+ "durationDays0": "Just now",
+ "durationDays": "1 day",
+ "durationDays_plural": "{{count}} days",
+ "selectValidatorTitle": "Select validator",
+ "started": {
+ "title": "Earn rewards",
+ "description": "Delegate your Tezos account to a third-party validator to earn rewards securely, keeping control of your assets.",
+ "steps": {
+ "0": "Delegated accounts remain yours.",
+ "1": "You can manage your assets at all times.",
+ "2": "Delegate securely with your Ledger device."
+ },
+ "cta": "Delegate to earn rewards"
+ },
+ "broadcastSuccessTitle": {
+ "delegate": "Delegation sent",
+ "undelegate": "Operation sent"
+ },
+ "broadcastSuccessDescription": {
+ "delegate": "Delegation transaction broadcasted successfully. You should earn your first rewards in around 40 days, depending on the validator.",
+ "undelegate": "Your account delegation will end when the operation is confirmed. You can delegate your account again at any time."
+ },
+ "summaryTitle": "Summary",
+ "goToAccount": "Go to Accounts",
+ "howDelegationWorks": "How delegation works",
+ "actions": {
+ "redelegate": "Redelegate",
+ "collectRewards": "Collect rewards",
+ "undelegate": "Undelegate"
+ }
+ },
+ "ValidateOnDevice": {
+ "title": {
+ "send": "Please confirm the operation on your {{productName}} to send it",
+ "freeze": "On your device, please confirm to finalize the operation",
+ "unfreeze": "On your device, please confirm to finalize the operation",
+ "claimReward": "On your device, please confirm to finalize the operation",
+ "vote": "On your device, please confirm to finalize the operation",
+ "delegate": "On your device, please confirm to finalize the operation",
+ "redelegate": "On your device, please confirm to finalize the operation",
+ "undelegate": "On your device, please confirm to finalize the operation"
+ },
+ "warning": "Always verify the address displayed your device exactly matches the one given to you by the {{recipientWording}}",
+ "recipientWording": {
+ "send": "Always verify the address displayed your device exactly matches the one given to you by the recipient",
+ "delegate": "Always verify that your device displays the address exactly as provided by the validator",
+ "undelegate": "Always verify that your device displays the address exactly as provided by the validator",
+ "freeze": "Always verify that your device displays the address exactly as provided",
+ "unfreeze": "Always verify that your device displays the address exactly as provided",
+ "claimReward": "Always verify that your device displays the address exactly as provided",
+ "vote": "Always verify that your device displays the address exactly as provided",
+ "erc20": {
+ "approve": "Verify the operation details on your device before sending it."
+ },
+ "compound.supply": "Verify the deposit details on your device before sending it.",
+ "compound.withdraw": "Verify the withdrawal details on your device before sending it."
+ },
+ "name": "Name",
+ "votes": "Votes",
+ "infoWording": {
+ "freeze": "Frozen tokens are locked for a period of 3 days.",
+ "unfreeze": "Your {{resource}} points will be reduced, and all your votes will be cancelled.",
+ "claimReward": "Rewards can be claimed every 24 hours.",
+ "cosmos": {
+ "claimReward": "If the selected validator has pending rewards, they will automatically be claimed.",
+ "redelegate": "You will have to wait 21 days for undelegated assets to return to the available balance.",
+ "undelegate": "You will have to wait 21 days for undelegated assets to return to the available balance."
+ },
+ "lending": "Verify the operation details on your device before sending it."
+ },
+ "amount": "Amount",
+ "account": "Account",
+ "from": "From",
+ "to": "To",
+ "redelegationAmount": "Redelegated amount",
+ "gas": "Gas",
+ "validatorAddress": "Validator address",
+ "rewardAmount": "Reward amount",
+ "undelegatedAmount": "Undelegated amount",
+ "memo": "Memo"
+ },
+ "Terms": {
+ "title": "TERMS OF USE",
+ "read": "Read the Terms of Use",
+ "switchLabel": "I have read and agree with the <1>Terms of Service1>",
+ "switchLabelFull": "I have read and agree with the Privacy Policy.",
+ "cta": "Enter Ledger App",
+ "service": "Terms of service",
+ "subTitle": "Please take some time to review our Terms of service and Privacy Policy"
+ },
+ "exchange": {
+ "buy": {
+ "tabTitle": "Buy",
+ "selectCurrency": "Select a currency",
+ "selectAccount": "Select an account",
+ "connectDevice": "Connect your device",
+ "title": "Buy crypto via our partner",
+ "description": "Purchase crypto assets via Coinify and receive them directly in your Ledger account.",
+ "CTAButton": "Buy now",
+ "emptyState": {
+ "title": "No {{currency}} account",
+ "description": "Please add an account before buying {{currency}}.",
+ "CTAButton": "Add account"
+ },
+ "skipDeviceVerification": {
+ "confirm": "Confirm",
+ "cancel": "Cancel",
+ "address": "Address for {{currency}} account",
+ "warning": "Your address was not confirmed on your Ledger device. Please verify it for security"
+ }
+ },
+ "sell": {
+ "tabTitle": "Sell",
+ "selectCurrency": "Select a currency",
+ "selectAccount": "Select an account",
+ "connectDevice": "Connect your device",
+ "title": "Sell crypto via our partner",
+ "description": "Sell crypto assets directly from your Ledger account via Coinify and receive fiat in your bank account.",
+ "CTAButton": "Sell now",
+ "emptyState": {
+ "title": "No {{currency}} account",
+ "description": "You need to add an account before you can sell {{currency}}.",
+ "CTAButton": "Add account"
+ }
+ },
+ "history": {
+ "tabTitle": "History"
+ }
+ },
+ "banner": {
+ "exchangeBuyCrypto": {
+ "title": "BUY CRYPTO",
+ "description": "Purchase crypto assets via Coinify and receive them directly in your Ledger account."
+ },
+ "swap": {
+ "title": "SWAP CRYPTO",
+ "description": "Swap crypto assets directly from your Ledger accounts with our partner."
+ }
+ },
+ "walletconnect": {
+ "disclaimer": "Wants to connect to the following Ethereum account through your wallet :",
+ "reject": "Reject",
+ "connect": "Connect",
+ "connected": "Connected",
+ "disconnected": "Disconnected",
+ "warningdisconnected": "There is a connection problem between the dApp, WalletConnect and Ledger Live. Wait a few moments or relaunch the connection",
+ "info": "You can now access the {{name}} dApp on your web browser.",
+ "warning": "Sharing receive addresses from dApp is not secure. Always use Ledger Live when sharing your address to receive funds.",
+ "isconnecting": "is connecting, please wait...",
+ "disconnect": "Disconnect",
+ "retry": "Retry",
+ "close": "Close",
+ "message": "Message",
+ "messageHash": "Message Hash",
+ "domainHash": "Domain Hash",
+ "stringHash": "Hash",
+ "from": "From",
+ "successTitle": "Message signed",
+ "successDescription": "You have signed the message received from a third party application",
+ "stepperHeader": {
+ "summary": "Summary",
+ "selectDevice": "Select Device",
+ "connectDevice": "Connect Device",
+ "stepRange": "Step {{currentStep}} of {{totalSteps}}"
+ },
+ "stepVerification": {
+ "action": "Please confirm the operation on your device",
+ "accountName": "Account name"
+ },
+ "deeplinkingTitle": "Select an Ethereum account",
+ "addAccount": "Add new account"
+ },
+ "analytics": {
+ "title": "Analytics",
+ "allocation": {
+ "title": "Allocation"
+ },
+ "operations": {
+ "title": "Operations"
+ }
+ },
+ "notificationCenter": {
+ "title": "Notification center",
+ "announcement": "Announcement",
+ "liveStatus": "Ledger Live status",
+ "groupedToast": {
+ "text": "You have {{count}} unread notifications",
+ "cta": "See details"
+ },
+ "news": {
+ "title": "News",
+ "titleCount": "News ({{count}})",
+ "emptyState": {
+ "title": "No news for the moment",
+ "desc": "You will find here all the news related to Ledger and Ledger Live"
+ }
+ },
+ "status": {
+ "title": "Status",
+ "ok": {
+ "title": "Ledger Live is up and running",
+ "desc": "<0>Having trouble? Go on our 0><1>help page1>"
+ },
+ "error": {
+ "title": "Ledger Live is experiencing issues"
+ }
+ }
+ },
+ "platform": {
+ "catalog": {
+ "title": "Live Apps Catalog",
+ "branch": {
+ "soon": "coming soon",
+ "experimental": "experimental",
+ "debug": "debug"
+ },
+ "banner": {
+ "title": "Discover our Live Catalog",
+ "description": "Unlock a new world of crypto possibilities. One secure access to all services – DeFi, NFTs and more to come."
+ },
+ "twitterBanner": {
+ "description": "Tell us what's the next service you want to see with the hashtag",
+ "tweetText": "The next Ledger App should be..."
+ },
+ "pollCTA": {
+ "title": "Poll",
+ "description": "Which service do you want to see in Ledger Live?"
+ },
+ "developerCTA": {
+ "title": "For Developers",
+ "description": "All the information you need to integrate your applications on Ledger Live."
+ }
+ },
+ "disclaimer": {
+ "title": "External Application",
+ "description": "You are about to be redirected to an application not operated by Ledger.",
+ "legalAdvice": "This application is not operated by Ledger. Ledger is not responsible for any loss of funds or quality of service of such application.\n\nAlways make sure to carefully verify the information displayed on your device.",
+ "checkbox": "Do not remind me again.",
+ "CTA": "Continue"
+ },
+ "webPlatformPlayer": {
+ "infoPanel": {
+ "website": "website"
+ }
+ }
+ },
+ "nft": {
+ "account": {
+ "seeAllNfts": "See all NFT",
+ "seeFewerNfts": "See fewer NFT"
+ },
+ "gallery": {
+ "allNft": "All NFT"
+ },
+ "viewer": {
+ "properties": "Properties",
+ "description": "Description",
+ "tokenContract": "Token Address",
+ "tokenId": "Token ID",
+ "quantity": "Quantity"
+ },
+ "viewerModal": {
+ "viewOn": "View on",
+ "viewInExplorer": "View in explorer",
+ "txDetails": "Transaction details"
+ }
+ }
+}
diff --git a/src/locales/ja/common.json b/src/locales/ja/common.json
index 9e08807c3b..c46e8795e8 100644
--- a/src/locales/ja/common.json
+++ b/src/locales/ja/common.json
@@ -1966,7 +1966,7 @@
"cta": "閉じる"
},
"pendingOperation": {
- "description": "承認のため、スワップ処理がネットワークに送信されました。{{targetCurrency}}を受け取るまでに、最大1時間かかる場合があります。",
+ "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your swapped {{targetCurrency}} assets",
"label": "スワップID:",
"title": "保留中の処理",
"disclaimer": "{{provider}} のサポートが必要な場合に備えて、スワップID番号を控えておいてください。",
diff --git a/src/logic/version.tsx b/src/logic/version.tsx
new file mode 100644
index 0000000000..701bf0fb19
--- /dev/null
+++ b/src/logic/version.tsx
@@ -0,0 +1,45 @@
+import { Platform } from "react-native";
+import VersionNumber from "react-native-version-number";
+
+const { appVersion, buildVersion } = VersionNumber;
+
+const mega = 1048576;
+export const getAndroidArchitecture = (buildVersionArg = buildVersion) => {
+ const buildVersionNumber = parseInt(buildVersionArg, 10);
+ if (!buildVersionNumber) return "";
+
+ if (Platform.OS === "android" && buildVersionNumber) {
+ // https://github.com/LedgerHQ/ledger-live-mobile/blob/develop/android/app/build.gradle#L166
+ const platforms = ["armeabi-v7a", "x86", "arm64-v8a", "x86_64"];
+ return platforms[Math.floor(buildVersionNumber / mega) - 1];
+ }
+
+ return buildVersion;
+};
+
+export const getAndroidVersionCode = (buildVersionArg = buildVersion) => {
+ const buildVersionNumber = parseInt(buildVersionArg, 10);
+ if (!buildVersionNumber) return "";
+
+ return Platform.OS === "android" && buildVersionNumber
+ ? buildVersionNumber % mega
+ : buildVersionArg;
+};
+
+export const cleanBuildVersion = (buildVersionArg = buildVersion) => {
+ if (Platform.OS === "android" && buildVersionArg) {
+ return `${getAndroidArchitecture(buildVersionArg) ||
+ ""} ${getAndroidVersionCode(buildVersionArg) || ""}`;
+ }
+ return buildVersionArg;
+};
+
+export default function getFullAppVersion(
+ appVersionArg = appVersion,
+ buildVersionArg = buildVersion,
+ separator = " ",
+) {
+ return `${appVersionArg || ""}${separator}(${cleanBuildVersion(
+ buildVersionArg,
+ ) || ""})`;
+}
diff --git a/src/modals/Create.tsx b/src/modals/Create.tsx
new file mode 100644
index 0000000000..9d7a32a599
--- /dev/null
+++ b/src/modals/Create.tsx
@@ -0,0 +1,105 @@
+/* @flow */
+
+import React, { useCallback } from "react";
+import { useNavigation } from "@react-navigation/native";
+import { useTranslation } from "react-i18next";
+import { useSelector } from "react-redux";
+
+import { NavigatorName, ScreenName } from "../const";
+import {
+ accountsCountSelector,
+ hasLendEnabledAccountsSelector,
+} from "../reducers/accounts";
+import BottomModal from "../components/BottomModal";
+import BottomModalChoice from "../components/BottomModalChoice";
+import { Props as ModalProps } from "../components/BottomModal";
+import { readOnlyModeEnabledSelector } from "../reducers/settings";
+
+export default function CreateModal({ isOpened, onClose }: ModalProps) {
+ const navigation = useNavigation();
+ const { t } = useTranslation();
+
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+ const accountsCount = useSelector(accountsCountSelector);
+ const lendingEnabled = useSelector(hasLendEnabledAccountsSelector);
+
+ const onNavigate = useCallback(
+ (name: string, options?: { [key: string]: any }) => {
+ navigation.navigate(name, options);
+
+ if (onClose) {
+ onClose();
+ }
+ },
+ [navigation, onClose],
+ );
+
+ const onSendFunds = useCallback(
+ () =>
+ onNavigate(NavigatorName.SendFunds, {
+ screen: ScreenName.SendCoin,
+ }),
+ [onNavigate],
+ );
+ const onReceiveFunds = useCallback(
+ () =>
+ onNavigate(NavigatorName.ReceiveFunds, {
+ screen: ScreenName.ReceiveSelectAccount,
+ }),
+ [onNavigate],
+ );
+ const onSwap = useCallback(
+ () =>
+ onNavigate(NavigatorName.Swap, {
+ screen: ScreenName.Swap,
+ }),
+ [onNavigate],
+ );
+ const onExchange = useCallback(() => onNavigate(ScreenName.Exchange), [
+ onNavigate,
+ ]);
+ const onLending = useCallback(
+ () =>
+ onNavigate(NavigatorName.Lending, {
+ screen: ScreenName.LendingDashboard,
+ }),
+ [onNavigate],
+ );
+
+ return (
+
+ 0 && !readOnlyModeEnabled ? onSendFunds : null}
+ iconName="ArrowFromBottom"
+ />
+ 0 ? onReceiveFunds : null}
+ iconName="ArrowToBottom"
+ />
+
+ 0 && !readOnlyModeEnabled ? onSwap : null}
+ />
+ {lendingEnabled ? (
+ 0 && !readOnlyModeEnabled ? onLending : null}
+ />
+ ) : null}
+
+ );
+}
diff --git a/src/navigation/navigatorConfig.tsx b/src/navigation/navigatorConfig.tsx
new file mode 100644
index 0000000000..092bc7c6ab
--- /dev/null
+++ b/src/navigation/navigatorConfig.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import HeaderRightClose from "../components/HeaderRightClose";
+import HeaderTitle from "../components/HeaderTitle";
+import HeaderBackImage from "../components/HeaderBackImage";
+import styles from "./styles";
+
+export const defaultNavigationOptions = {
+ headerStyle: styles.header,
+ headerTitle: (props: any) => ,
+ headerBackTitleVisible: false,
+ headerBackImage: () => ,
+ headerTitleAllowFontScaling: false,
+};
+
+export const getStackNavigatorConfig = (c: any, closable: boolean = false) => ({
+ ...defaultNavigationOptions,
+ cardStyle: { backgroundColor: c.background.main || c.background },
+ headerStyle: {
+ backgroundColor: c.background.main || c.background,
+ borderBottomColor: c.neutral?.c40 || c.white,
+ // borderBottomWidth: 1,
+ elevation: 0, // remove shadow on Android
+ shadowOpacity: 0, // remove shadow on iOS
+ },
+ headerTitleAlign: "center",
+ headerTitleStyle: {
+ color: c.darkBlue,
+ },
+ headerRight: closable ? () => : undefined,
+});
diff --git a/src/navigation/styles.js b/src/navigation/styles.js
index e9cc09e92a..f8e3634cc4 100644
--- a/src/navigation/styles.js
+++ b/src/navigation/styles.js
@@ -29,7 +29,6 @@ function Styles() {
header: {
...headerStyle,
// $FlowFixMe
- ...headerStyleShadow,
},
headerNoShadow: {
...headerStyle,
diff --git a/src/navigation/tabNavigatorConfig.tsx b/src/navigation/tabNavigatorConfig.tsx
new file mode 100644
index 0000000000..802216ee20
--- /dev/null
+++ b/src/navigation/tabNavigatorConfig.tsx
@@ -0,0 +1,20 @@
+import { ColorPalette } from "@ledgerhq/native-ui";
+
+export const getLineTabNavigatorConfig = (colors: ColorPalette) => ({
+ screenOptions: {
+ tabBarActiveTintColor: colors.neutral.c100,
+ tabBarInactiveTintColor: colors.neutral.c80,
+ tabBarIndicatorStyle: {
+ backgroundColor: colors.primary.c70,
+ height: 3,
+ },
+ tabBarStyle: {
+ backgroundColor: colors.background.main,
+ borderBottomWidth: 1,
+ borderColor: colors.neutral.c40,
+ },
+ },
+ sceneContainerStyle: {
+ backgroundColor: colors.background.main,
+ },
+});
diff --git a/src/navigation/useDeepLinking.js b/src/navigation/useDeepLinking.js
index e3871f7abe..f4926dc464 100644
--- a/src/navigation/useDeepLinking.js
+++ b/src/navigation/useDeepLinking.js
@@ -29,7 +29,7 @@ function getSettingsScreen(pathname) {
screen = ScreenName.DeveloperSettings;
break;
default:
- screen = ScreenName.Settings;
+ screen = ScreenName.SettingsScreen;
}
return screen;
}
@@ -105,7 +105,7 @@ export function useDeepLinkHandler() {
const dapp =
path && filteredManifests.find(m => path.toLowerCase() === m.id);
- navigate(NavigatorName.Platform, {
+ navigate(NavigatorName.Discover, {
screen: ScreenName.PlatformCatalog,
params: dapp
? {
diff --git a/src/polyfill.js b/src/polyfill.js
index 46a43363b4..b8f3536631 100644
--- a/src/polyfill.js
+++ b/src/polyfill.js
@@ -1,3 +1,5 @@
+import "./generated/intlPolyfills";
+
/* eslint-disable no-console */
global.Buffer = require("buffer").Buffer;
diff --git a/src/reducers/settings.js b/src/reducers/settings.js
index 0d75a05476..fa5ff132f0 100755
--- a/src/reducers/settings.js
+++ b/src/reducers/settings.js
@@ -1,7 +1,6 @@
// @flow
/* eslint import/no-cycle: 0 */
import { handleActions } from "redux-actions";
-import { Platform } from "react-native";
import merge from "lodash/merge";
import {
findCurrencyByTicker,
@@ -23,6 +22,7 @@ import type { PortfolioRange } from "@ledgerhq/live-common/lib/portfolio/v2/type
import type { DeviceModelInfo } from "@ledgerhq/live-common/lib/types/manager";
import { currencySettingsDefaults } from "../helpers/CurrencySettingsDefaults";
import type { State } from ".";
+import { SLIDES } from "../components/Carousel";
import { getDefaultLanguageLocale, getDefaultLocale } from "../languages";
const bitcoin = getCryptoCurrencyById("bitcoin");
@@ -83,10 +83,12 @@ export type SettingsState = {
hasAvailableUpdate: boolean,
theme: Theme,
osTheme: ?string,
- carouselVisibility: number,
+ carouselVisibility: any,
discreetMode: boolean,
+ /** Used for translations. In the UI, this is the "language" setting. */
language: string,
languageIsSetByUser: boolean,
+ /** Used for number & date formatting. In the UI, this is the "region" setting. */
locale: ?string,
swap: {
hasAcceptedIPSharing: false,
@@ -120,7 +122,9 @@ export const INITIAL_STATE: SettingsState = {
hasAvailableUpdate: false,
theme: "system",
osTheme: undefined,
- carouselVisibility: 0,
+ carouselVisibility: Object.fromEntries(
+ SLIDES.map(slide => [slide.name, true]),
+ ),
discreetMode: false,
language: getDefaultLanguageLocale(),
languageIsSetByUser: false,
@@ -476,7 +480,7 @@ export const countervalueFirstSelector = (state: State) =>
state.settings.countervalueFirst;
export const readOnlyModeEnabledSelector = (state: State) =>
- Platform.OS !== "android" && state.settings.readOnlyModeEnabled;
+ state.settings.readOnlyModeEnabled;
export const blacklistedTokenIdsSelector = (state: State) =>
state.settings.blacklistedTokenIds;
diff --git a/src/screens/Account/AccountHeaderRight.tsx b/src/screens/Account/AccountHeaderRight.tsx
new file mode 100644
index 0000000000..55cb19daf6
--- /dev/null
+++ b/src/screens/Account/AccountHeaderRight.tsx
@@ -0,0 +1,76 @@
+import React, { useState, useCallback, useEffect } from "react";
+import { View } from "react-native";
+import { useSelector } from "react-redux";
+import { useNavigation, useRoute, useTheme } from "@react-navigation/native";
+import Icon from "react-native-vector-icons/dist/FontAwesome";
+import { NavigatorName, ScreenName } from "../../const";
+import Touchable from "../../components/Touchable";
+import Wrench from "../../icons/Wrench";
+import { accountScreenSelector } from "../../reducers/accounts";
+import TokenContextualModal from "../Settings/Accounts/TokenContextualModal";
+import { FiltersMedium, OthersMedium } from "@ledgerhq/native-ui/assets/icons";
+
+export default function AccountHeaderRight() {
+ const navigation = useNavigation();
+ const route = useRoute();
+ const { account, parentAccount } = useSelector(accountScreenSelector(route));
+
+ const [isOpened, setOpened] = useState(false);
+
+ const toggleModal = useCallback(() => setOpened(!isOpened), [isOpened]);
+ const closeModal = () => {
+ setOpened(false);
+ };
+
+ useEffect(() => {
+ if (!account) {
+ navigation.navigate(ScreenName.Accounts);
+ }
+ }, [account, navigation]);
+
+ if (!account) return null;
+
+ if (account.type === "TokenAccount" && parentAccount) {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+ }
+
+ if (account.type === "Account") {
+ return (
+ {
+ navigation.navigate(NavigatorName.AccountSettings, {
+ screen: ScreenName.AccountSettingsMain,
+ params: {
+ accountId: account.id,
+ },
+ });
+ }}
+ style={{ alignItems: "center", justifyContent: "center", margin: 16 }}
+ >
+
+
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/src/screens/Account/AccountHeaderTitle.tsx b/src/screens/Account/AccountHeaderTitle.tsx
new file mode 100644
index 0000000000..ca17bb95fe
--- /dev/null
+++ b/src/screens/Account/AccountHeaderTitle.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { TouchableWithoutFeedback, View, StyleSheet } from "react-native";
+import { useRoute } from "@react-navigation/native";
+import { useSelector } from "react-redux";
+import {
+ getAccountCurrency,
+ getAccountName,
+} from "@ledgerhq/live-common/lib/account";
+import { Text } from "@ledgerhq/native-ui";
+import { accountScreenSelector } from "../../reducers/accounts";
+import ParentCurrencyIcon from "../../components/ParentCurrencyIcon";
+import { scrollToTop } from "../../navigation/utils";
+
+export default function AccountHeaderTitle() {
+ const route = useRoute();
+ const { account } = useSelector(accountScreenSelector(route));
+
+ if (!account) return null;
+ return (
+
+
+
+
+
+
+ {getAccountName(account)}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ title: {
+ fontSize: 16,
+ paddingRight: 32,
+ },
+ headerContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginHorizontal: 32,
+ paddingVertical: 5,
+ },
+ iconContainer: {
+ marginRight: 8,
+ justifyContent: "center",
+ },
+});
diff --git a/src/screens/Account/ListHeaderComponent.tsx b/src/screens/Account/ListHeaderComponent.tsx
new file mode 100644
index 0000000000..bc7d55f936
--- /dev/null
+++ b/src/screens/Account/ListHeaderComponent.tsx
@@ -0,0 +1,286 @@
+import React, { ReactNode } from "react";
+import { ScrollView } from "react-native";
+import {
+ isAccountEmpty,
+ getMainAccount,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account";
+import {
+ Unit,
+ AccountLike,
+ Account,
+ Currency,
+} from "@ledgerhq/live-common/lib/types";
+import { ValueChange } from "@ledgerhq/live-common/lib/portfolio/v2/types";
+import { CompoundAccountSummary } from "@ledgerhq/live-common/lib/compound/types";
+
+import { Box, Flex, Text } from "@ledgerhq/native-ui";
+import CurrencyUnitValue from "../../components/CurrencyUnitValue";
+import Header from "./Header";
+import AccountGraphCard from "../../components/AccountGraphCard";
+import Touchable from "../../components/Touchable";
+import TransactionsPendingConfirmationWarning from "../../components/TransactionsPendingConfirmationWarning";
+import { Item } from "../../components/Graph/types";
+import SubAccountsList from "./SubAccountsList";
+import NftCollectionsList from "./NftCollectionsList";
+import CompoundSummary from "../Lending/Account/CompoundSummary";
+import CompoundAccountBodyHeader from "../Lending/Account/AccountBodyHeader";
+import perFamilyAccountHeader from "../../generated/AccountHeader";
+import perFamilyAccountSubHeader from "../../generated/AccountSubHeader";
+import perFamilyAccountBodyHeader from "../../generated/AccountBodyHeader";
+import perFamilyAccountBalanceSummaryFooter from "../../generated/AccountBalanceSummaryFooter";
+import FabActions from "../../components/FabActions";
+import { NoCountervaluePlaceholder } from "../../components/CounterValue.js";
+import DiscreetModeButton from "../../components/DiscreetModeButton";
+import Delta from "../../components/Delta";
+
+const renderAccountSummary = (
+ account,
+ parentAccount,
+ compoundSummary,
+) => () => {
+ const mainAccount = getMainAccount(account, parentAccount);
+ const AccountBalanceSummaryFooter =
+ perFamilyAccountBalanceSummaryFooter[mainAccount.currency.family];
+
+ const footers = [];
+
+ if (compoundSummary && account.type === "TokenAccount") {
+ footers.push(
+ ,
+ );
+ }
+
+ if (AccountBalanceSummaryFooter)
+ footers.push(
+ ,
+ );
+ if (!footers.length) return null;
+ return footers;
+};
+
+type HeaderTitleProps = {
+ useCounterValue?: boolean;
+ cryptoCurrencyUnit: Unit;
+ counterValueUnit: Unit;
+ item: Item;
+};
+
+const ListHeaderTitle = ({
+ account,
+ countervalueAvailable,
+ onSwitchAccountCurrency,
+ valueChange,
+ useCounterValue,
+ cryptoCurrencyUnit,
+ counterValueUnit,
+ item,
+}: HeaderTitleProps) => {
+ const items = [
+ { unit: cryptoCurrencyUnit, value: item.value },
+ { unit: counterValueUnit, value: item.countervalue },
+ ];
+
+ const shouldUseCounterValue = countervalueAvailable && useCounterValue;
+ if (shouldUseCounterValue) {
+ items.reverse();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {typeof items[1]?.value === "number" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+type Props = {
+ account?: AccountLike;
+ parentAccount?: Account;
+ countervalueAvailable: boolean;
+ useCounterValue: boolean;
+ range: any;
+ history: any;
+ countervalueChange: ValueChange;
+ cryptoChange: ValueChange;
+ counterValueCurrency: Currency;
+ onAccountPress: () => void;
+ onSwitchAccountCurrency: () => void;
+ compoundSummary?: CompoundAccountSummary;
+};
+
+export function getListHeaderComponents({
+ account,
+ parentAccount,
+ countervalueAvailable,
+ useCounterValue,
+ range,
+ history,
+ countervalueChange,
+ cryptoChange,
+ counterValueCurrency,
+ onAccountPress,
+ onSwitchAccountCurrency,
+ compoundSummary,
+}: Props): {
+ listHeaderComponents: ReactNode[];
+ stickyHeaderIndices?: number[];
+} {
+ if (!account)
+ return { listHeaderComponents: [], stickyHeaderIndices: undefined };
+
+ const mainAccount = getMainAccount(account, parentAccount);
+
+ const empty = isAccountEmpty(account);
+ const shouldUseCounterValue = countervalueAvailable && useCounterValue;
+
+ const AccountHeader = perFamilyAccountHeader[mainAccount.currency.family];
+ const AccountBodyHeader =
+ perFamilyAccountBodyHeader[mainAccount.currency.family];
+
+ const AccountSubHeader =
+ perFamilyAccountSubHeader[mainAccount.currency.family];
+
+ const stickyHeaderIndices = empty ? [] : [4];
+
+ return {
+ listHeaderComponents: [
+ ,
+ !!AccountSubHeader && ,
+ !empty && !!AccountHeader && (
+
+ ),
+ !empty && (
+
+
+
+ ),
+ ...(!empty
+ ? [
+
+
+
+
+ ,
+ ]
+ : []),
+ !empty && (
+
+
+
+ ),
+
+ ...(!empty && AccountBodyHeader
+ ? [
+ ,
+ ]
+ : []),
+ ...(!empty && account.type === "Account" && account.subAccounts
+ ? [
+
+
+ ,
+ ]
+ : []),
+ ...(!empty && account.type === "Account" && account.nfts?.length
+ ? []
+ : []),
+ ...(compoundSummary &&
+ account &&
+ account.type === "TokenAccount" &&
+ parentAccount
+ ? [
+ ,
+ ]
+ : []),
+ ],
+ stickyHeaderIndices,
+ };
+}
diff --git a/src/screens/Account/NftCollectionsList.tsx b/src/screens/Account/NftCollectionsList.tsx
new file mode 100644
index 0000000000..de780e102a
--- /dev/null
+++ b/src/screens/Account/NftCollectionsList.tsx
@@ -0,0 +1,150 @@
+import React, { useCallback, useMemo } from "react";
+
+import take from "lodash/take";
+import { Trans, useTranslation } from "react-i18next";
+import useEnv from "@ledgerhq/live-common/lib/hooks/useEnv";
+import { nftsByCollections } from "@ledgerhq/live-common/lib/nft";
+import { useNavigation, useTheme } from "@react-navigation/native";
+import { StyleSheet, View, FlatList } from "react-native";
+
+import { Account } from "@ledgerhq/live-common/lib/types";
+
+import { Box, Text } from "@ledgerhq/native-ui";
+import {
+ ArrowBottomMedium,
+ DroprightMedium,
+} from "@ledgerhq/native-ui/assets/icons";
+import NftCollectionRow from "../../components/Nft/NftCollectionRow";
+import { NavigatorName, ScreenName } from "../../const";
+import Link from "../../components/wrappedUi/Link";
+import Button from "../../components/wrappedUi/Button";
+
+const MAX_COLLECTIONS_TO_SHOW = 3;
+
+const collectionKeyExtractor = o => o.contract;
+
+type Props = {
+ account: Account;
+};
+
+export default function NftCollectionsList({ account }: Props) {
+ useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS");
+
+ const { t } = useTranslation();
+ const { colors } = useTheme();
+ const navigation = useNavigation();
+ const { nfts } = account;
+ const nftCollections = useMemo(() => nftsByCollections(nfts), [nfts]);
+ const data = take(nftCollections, MAX_COLLECTIONS_TO_SHOW);
+
+ const navigateToReceive = useCallback(
+ () =>
+ navigation.navigate(NavigatorName.ReceiveFunds, {
+ screen: ScreenName.ReceiveConnectDevice,
+ params: {
+ accountId: account.id,
+ },
+ }),
+ [account.id, navigation],
+ );
+
+ // Forced to use useCallback here to avoid a non sensical warning...
+ const navigateToCollection = useCallback(
+ collection =>
+ navigation.navigate(NavigatorName.Accounts, {
+ screen: ScreenName.NftCollection,
+ params: {
+ collection,
+ accountId: account.id,
+ },
+ initial: false,
+ }),
+ [account.id, navigation],
+ );
+
+ const navigateToGallery = useCallback(() => {
+ navigation.navigate(NavigatorName.Accounts, {
+ screen: ScreenName.NftGallery,
+ params: {
+ title: t("nft.gallery.allNft"),
+ accountId: account.id,
+ },
+ initial: false,
+ });
+ }, [account.id, navigation, t]);
+
+ const renderHeader = useCallback(
+ () => (
+
+ NFT
+
+
+
+
+ ),
+ [navigateToReceive],
+ );
+
+ const renderFooter = useCallback(
+ () => (
+
+ ),
+ [colors, navigateToGallery, nftCollections.length],
+ );
+
+ const renderItem = useCallback(
+ ({ item, index }) => (
+
+ navigateToCollection(item)}
+ />
+
+ ),
+ [data.length, navigateToCollection],
+ );
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ marginBottom: 16,
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+ collectionList: {
+ paddingLeft: 16,
+ paddingRight: 16,
+ paddingBottom: 24,
+ },
+});
diff --git a/src/screens/Account/SubAccountsList.tsx b/src/screens/Account/SubAccountsList.tsx
new file mode 100644
index 0000000000..ee32c3c68c
--- /dev/null
+++ b/src/screens/Account/SubAccountsList.tsx
@@ -0,0 +1,248 @@
+import React, { useCallback, useState, useMemo } from "react";
+import { Trans } from "react-i18next";
+import take from "lodash/take";
+import { StyleSheet, View, FlatList } from "react-native";
+import Icon from "react-native-vector-icons/dist/FontAwesome";
+import { useNavigation, useTheme } from "@react-navigation/native";
+import {
+ Account,
+ SubAccount,
+ TokenAccount,
+} from "@ledgerhq/live-common/lib/types";
+import useEnv from "@ledgerhq/live-common/lib/hooks/useEnv";
+import { listSubAccounts } from "@ledgerhq/live-common/lib/account";
+import { listTokenTypesForCryptoCurrency } from "@ledgerhq/live-common/lib/currencies";
+import { Button, Flex, Text } from "@ledgerhq/native-ui";
+import { DropdownMedium, DropupMedium } from "@ledgerhq/native-ui/assets/icons";
+import { NavigatorName, ScreenName } from "../../const";
+import SubAccountRow from "../../components/SubAccountRow";
+import Touchable from "../../components/Touchable";
+import TokenContextualModal from "../Settings/Accounts/TokenContextualModal";
+import perFamilySubAccountList from "../../generated/SubAccountList";
+
+const keyExtractor = o => o.id;
+
+const styles = StyleSheet.create({
+ footer: {
+ borderRadius: 4,
+ paddingHorizontal: 18,
+ paddingVertical: 12,
+ borderStyle: "dashed",
+ borderWidth: 1,
+ flexDirection: "row",
+ alignItems: "center",
+ overflow: "hidden",
+ },
+ footerText: {
+ flex: 1,
+ flexShrink: 1,
+ flexWrap: "wrap",
+ paddingLeft: 12,
+ flexDirection: "row",
+ },
+ header: {
+ marginBottom: 8,
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+});
+
+type Props = {
+ parentAccount: Account;
+ onAccountPress: (subAccount: SubAccount) => void;
+ accountId: string;
+ useCounterValue?: boolean;
+};
+
+export default function SubAccountsList({
+ parentAccount,
+ onAccountPress,
+ accountId,
+ useCounterValue,
+}: Props) {
+ useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS");
+
+ const { colors } = useTheme();
+ const navigation = useNavigation();
+ const [account, setAccount] = useState();
+ const [isCollapsed, setIsCollapsed] = useState(true);
+ const subAccounts = listSubAccounts(parentAccount);
+ const family = parentAccount.currency.family;
+ const specific = perFamilySubAccountList[family];
+
+ const hasSpecificTokenWording = specific && specific.hasSpecificTokenWording;
+ const ReceiveButton = specific && specific.ReceiveButton;
+
+ const Placeholder = specific && specific.Placeholder;
+
+ const isToken = useMemo(
+ () => listTokenTypesForCryptoCurrency(parentAccount.currency).length > 0,
+ [parentAccount],
+ );
+
+ const navigateToReceiveConnectDevice = useCallback(() => {
+ navigation.navigate(NavigatorName.ReceiveFunds, {
+ screen: ScreenName.ReceiveConnectDevice,
+ params: {
+ accountId,
+ },
+ });
+ }, [accountId, navigation]);
+
+ const renderHeader = useCallback(
+ () => (
+
+
+
+ {` (${subAccounts.length})`}
+
+
+ ),
+ [
+ isToken,
+ hasSpecificTokenWording,
+ family,
+ subAccounts.length,
+ ReceiveButton,
+ accountId,
+ navigateToReceiveConnectDevice,
+ colors.live,
+ ],
+ );
+
+ const renderFooter = useCallback(() => {
+ // If there are no sub accounts, we render the touchable rect
+ if (subAccounts.length === 0) {
+ return Placeholder ? (
+
+ ) : (
+
+
+
+
+
+
+
+ text
+
+
+ text
+
+
+
+
+
+
+ );
+ }
+ // If there is 3 or less sub accounts, no need for collapse button
+ if (subAccounts.length < 3) return null;
+
+ // else, we render the collapse button
+ return (
+
+ );
+ }, [
+ subAccounts.length,
+ isCollapsed,
+ isToken,
+ navigateToReceiveConnectDevice,
+ parentAccount.currency.family,
+ family,
+ hasSpecificTokenWording,
+ colors,
+ Placeholder,
+ accountId,
+ ]);
+
+ const renderItem = useCallback(
+ ({ item }) => (
+
+ setAccount(account)}
+ onSubAccountPress={onAccountPress}
+ useCounterValue={useCounterValue}
+ />
+
+ ),
+ [onAccountPress, parentAccount],
+ );
+
+ if (!isToken && subAccounts.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+ setAccount()}
+ isOpened={!!account}
+ account={account}
+ />
+ >
+ );
+}
diff --git a/src/screens/Account/hooks/useActions.tsx b/src/screens/Account/hooks/useActions.tsx
new file mode 100644
index 0000000000..1d9f55f234
--- /dev/null
+++ b/src/screens/Account/hooks/useActions.tsx
@@ -0,0 +1,153 @@
+import React, { useMemo } from "react";
+import { AccountLike, Account } from "@ledgerhq/live-common/lib/types";
+import {
+ getAccountCurrency,
+ getMainAccount,
+ getAccountSpendableBalance,
+} from "@ledgerhq/live-common/lib/account";
+import { useSelector } from "react-redux";
+import { Trans } from "react-i18next";
+import {
+ ArrowBottomMedium,
+ ArrowTopMedium,
+} from "@ledgerhq/native-ui/assets/icons";
+import { NavigatorName, ScreenName } from "../../../const";
+import { readOnlyModeEnabledSelector } from "../../../reducers/settings";
+import perFamilyAccountActions from "../../../generated/accountActions";
+import { isCurrencySupported } from "../../Exchange/coinifyConfig";
+import Lending from "../../../icons/Lending";
+import WalletConnect from "../../../icons/WalletConnect";
+import Exchange from "../../../icons/Exchange";
+import useCompoundAccountEnabled from "../../Lending/shared/useCompoundAccountEnabled";
+
+type Props = {
+ account: AccountLike;
+ parentAccount?: Account;
+ colors: any;
+};
+
+export default function useActions({ account, parentAccount, colors }: Props) {
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+ const currency = getAccountCurrency(account);
+
+ const balance = getAccountSpendableBalance(account);
+ const mainAccount = getMainAccount(account, parentAccount);
+ const decorators = perFamilyAccountActions[mainAccount.currency.family];
+
+ const walletConnectAvailable = currency.id === "ethereum";
+
+ const accountId = account.id;
+
+ const availableOnCompound = useCompoundAccountEnabled(account, parentAccount);
+
+ const canBeSold = isCurrencySupported(currency, "sell");
+
+ const extraSendActionParams = useMemo(
+ () =>
+ decorators && decorators.getExtraSendActionParams
+ ? decorators.getExtraSendActionParams({ account, parentAccount })
+ : {},
+ [account, parentAccount, decorators],
+ );
+
+ const extraReceiveActionParams = useMemo(
+ () =>
+ decorators && decorators.getExtraSendActionParams
+ ? decorators.getExtraReceiveActionParams({ account, parentAccount })
+ : {},
+ [account, parentAccount, decorators],
+ );
+
+ const SendAction = {
+ navigationParams: [
+ NavigatorName.SendFunds,
+ {
+ screen: ScreenName.SendSelectRecipient,
+ },
+ ],
+ label: ,
+ event: "AccountSend",
+ Icon: ArrowTopMedium,
+ disabled: balance.lte(0),
+ ...extraSendActionParams,
+ };
+
+ const ReceiveAction = {
+ navigationParams: [
+ NavigatorName.ReceiveFunds,
+ {
+ screen: ScreenName.ReceiveConnectDevice,
+ },
+ ],
+ label: ,
+ event: "AccountReceive",
+ Icon: ArrowBottomMedium,
+ ...extraReceiveActionParams,
+ };
+
+ const baseActions =
+ (decorators &&
+ decorators.getActions &&
+ decorators.getActions({
+ account,
+ parentAccount,
+ colors,
+ })) ||
+ [];
+
+ const actions = [
+ ...(!readOnlyModeEnabled ? [SendAction] : []),
+ ReceiveAction,
+ ...baseActions,
+ ...(!readOnlyModeEnabled && canBeSold
+ ? [
+ {
+ navigationParams: [NavigatorName.ExchangeSellFlow, { accountId }],
+ label: ,
+ Icon: Exchange,
+ event: "Sell Crypto Account Button",
+ eventProperties: {
+ currencyName: currency?.name,
+ },
+ },
+ ]
+ : []),
+ ...(walletConnectAvailable
+ ? [
+ {
+ navigationParams: [
+ NavigatorName.Base,
+ {
+ screen: ScreenName.WalletConnectScan,
+ params: {
+ accountId: account?.id,
+ },
+ },
+ ],
+ label: ,
+ Icon: WalletConnect,
+ event: "WalletConnect Account Button",
+ eventProperties: { currencyName: currency?.name },
+ },
+ ]
+ : []),
+ ...(availableOnCompound
+ ? [
+ {
+ enableActions: "lending",
+ label: (
+
+ ),
+ Icon: Lending,
+ event: "Lend Crypto Account Button",
+ eventProperties: { currencyName: currency?.name },
+ },
+ ]
+ : []),
+ ];
+
+ return actions;
+}
diff --git a/src/screens/Account/index.tsx b/src/screens/Account/index.tsx
new file mode 100644
index 0000000000..9b6a515b6d
--- /dev/null
+++ b/src/screens/Account/index.tsx
@@ -0,0 +1,321 @@
+import React, { useState, useRef, useCallback, useMemo } from "react";
+import { StyleSheet, View, SectionList, FlatList } from "react-native";
+import { SectionBase } from "react-native/Libraries/Lists/SectionList";
+import Animated, { Value, event } from "react-native-reanimated";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigation } from "@react-navigation/native";
+import {
+ isAccountEmpty,
+ groupAccountOperationsByDay,
+ getAccountCurrency,
+} from "@ledgerhq/live-common/lib/account";
+import {
+ Account,
+ AccountLike,
+ TokenAccount,
+ Operation,
+} from "@ledgerhq/live-common/lib/types";
+import debounce from "lodash/debounce";
+import {
+ getAccountCapabilities,
+ makeCompoundSummaryForAccount,
+} from "@ledgerhq/live-common/lib/compound/logic";
+import { Trans } from "react-i18next";
+import { Text } from "@ledgerhq/native-ui";
+import { switchCountervalueFirst } from "../../actions/settings";
+import { useBalanceHistoryWithCountervalue } from "../../actions/portfolio";
+import {
+ selectedTimeRangeSelector,
+ counterValueCurrencySelector,
+ countervalueFirstSelector,
+} from "../../reducers/settings";
+import { accountScreenSelector } from "../../reducers/accounts";
+import { TrackScreen } from "../../analytics";
+import accountSyncRefreshControl from "../../components/accountSyncRefreshControl";
+import OperationRow from "../../components/OperationRow";
+import SectionHeader from "../../components/SectionHeader";
+import NoMoreOperationFooter from "../../components/NoMoreOperationFooter";
+import LoadingFooter from "../../components/LoadingFooter";
+import { ScreenName } from "../../const";
+import EmptyStateAccount from "./EmptyStateAccount";
+import NoOperationFooter from "../../components/NoOperationFooter";
+import { useScrollToTop } from "../../navigation/utils";
+
+import { getListHeaderComponents } from "./ListHeaderComponent";
+
+type Props = {
+ navigation: any;
+ route: { params: RouteParams };
+};
+
+type RouteParams = {
+ accountId: string;
+ parentId?: string;
+};
+
+const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);
+const List = accountSyncRefreshControl(AnimatedSectionList);
+
+const AnimatedFlatListWithRefreshControl = Animated.createAnimatedComponent(
+ accountSyncRefreshControl(FlatList),
+);
+
+function renderSectionHeader({ section }: any) {
+ return ;
+}
+
+function keyExtractor(item: Operation) {
+ return item.id;
+}
+
+const stickySectionHeight = 56;
+
+export default function AccountScreen({ route }: Props) {
+ const { account, parentAccount } = useSelector(accountScreenSelector(route));
+ if (!account) return null;
+ return ;
+}
+
+function AccountScreenInner({
+ account,
+ parentAccount,
+}: {
+ account: AccountLike;
+ parentAccount: Account | undefined | null;
+}) {
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
+ const range = useSelector(selectedTimeRangeSelector);
+ const {
+ countervalueAvailable,
+ countervalueChange,
+ cryptoChange,
+ history,
+ } = useBalanceHistoryWithCountervalue({ account, range });
+ const useCounterValue = useSelector(countervalueFirstSelector);
+ const counterValueCurrency = useSelector(counterValueCurrencySelector);
+
+ const [opCount, setOpCount] = useState(100);
+ const ref = useRef();
+ const scrollY = useRef(new Value(0)).current;
+
+ useScrollToTop(ref);
+
+ const onEndReached = useCallback(() => {
+ setOpCount(opCount + 50);
+ }, [setOpCount, opCount]);
+
+ const onSwitchAccountCurrency = useCallback(() => {
+ dispatch(switchCountervalueFirst());
+ }, [dispatch]);
+
+ const onAccountPress = debounce((tokenAccount: TokenAccount) => {
+ navigation.push(ScreenName.Account, {
+ parentId: account.id,
+ accountId: tokenAccount.id,
+ });
+ }, 300);
+
+ const ListEmptyComponent = useCallback(
+ () =>
+ isAccountEmpty(account) && (
+
+ ),
+ [account, parentAccount, navigation],
+ );
+
+ const renderItem = useCallback(
+ ({
+ item,
+ index,
+ section,
+ }: {
+ item: Operation;
+ index: number;
+ section: SectionBase;
+ }) => {
+ if (!account) return null;
+
+ return (
+
+ );
+ },
+ [account, parentAccount],
+ );
+
+ const currency = getAccountCurrency(account);
+
+ const analytics = (
+
+ );
+
+ const { sections, completed } = groupAccountOperationsByDay(account, {
+ count: opCount,
+ });
+
+ const compoundCapabilities =
+ account.type === "TokenAccount" &&
+ !!account.compoundBalance &&
+ getAccountCapabilities(account);
+
+ const compoundSummary =
+ compoundCapabilities?.status && account.type === "TokenAccount"
+ ? makeCompoundSummaryForAccount(account, parentAccount)
+ : undefined;
+
+ const [isCollapsed, setIsCollapsed] = useState(true);
+
+ const { listHeaderComponents } = useMemo(
+ () =>
+ getListHeaderComponents({
+ account,
+ parentAccount,
+ countervalueAvailable,
+ useCounterValue,
+ range,
+ history,
+ countervalueChange,
+ cryptoChange,
+ counterValueCurrency,
+ onAccountPress,
+ onSwitchAccountCurrency,
+ compoundSummary,
+ isCollapsed,
+ setIsCollapsed,
+ }),
+ [
+ account,
+ compoundSummary,
+ counterValueCurrency,
+ countervalueAvailable,
+ countervalueChange,
+ cryptoChange,
+ history,
+ isCollapsed,
+ onAccountPress,
+ onSwitchAccountCurrency,
+ parentAccount,
+ range,
+ useCounterValue,
+ ],
+ );
+
+ const data = [
+ ...listHeaderComponents,
+ (
+
+
+
+ )}
+ ListFooterComponent={
+ !completed ? (
+
+ ) : sections.length === 0 ? (
+ isAccountEmpty(account) ? null : (
+
+ )
+ ) : (
+
+ )
+ }
+ ListEmptyComponent={ListEmptyComponent}
+ keyExtractor={keyExtractor}
+ renderItem={renderItem}
+ renderSectionHeader={renderSectionHeader}
+ onEndReached={onEndReached}
+ onScroll={event(
+ [
+ {
+ nativeEvent: {
+ contentOffset: { y: scrollY },
+ },
+ },
+ ],
+ { useNativeDriver: true },
+ )}
+ showsVerticalScrollIndicator={false}
+ accountId={account.id}
+ stickySectionHeadersEnabled={false}
+ />,
+ ];
+
+ return (
+
+ {analytics}
+ item}
+ keyExtractor={(item, index) => String(index)}
+ showsVerticalScrollIndicator={false}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: "column",
+ },
+ sectionList: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ balanceContainer: {
+ alignItems: "center",
+ marginBottom: 10,
+ },
+ balanceText: {
+ fontSize: 22,
+ paddingBottom: 4,
+ },
+ contentContainer: {
+ paddingBottom: 64,
+ flexGrow: 1,
+ },
+ accountFabActions: {
+ width: "100%",
+ height: 56,
+ position: "absolute",
+ bottom: 0,
+ left: 0,
+ },
+ stickyContainer: {
+ width: "100%",
+ height: stickySectionHeight,
+ paddingVertical: 8,
+ position: "absolute",
+ left: 0,
+ top: 0,
+ zIndex: -100,
+ opacity: 0,
+ },
+ stickyBg: {
+ width: "100%",
+ height: "100%",
+ position: "absolute",
+ zIndex: 0,
+ },
+});
diff --git a/src/screens/AccountSettings/AdvancedLogs.js b/src/screens/AccountSettings/AdvancedLogs.js
index d3d5d3c93a..24ee5bbb28 100644
--- a/src/screens/AccountSettings/AdvancedLogs.js
+++ b/src/screens/AccountSettings/AdvancedLogs.js
@@ -44,7 +44,7 @@ export default function AdvancedLogs({ route }: Props) {
});
return (
-
+
{t("common.sync.ago", { time: readableDate })}
@@ -58,14 +58,11 @@ export default function AdvancedLogs({ route }: Props) {
}
const styles = StyleSheet.create({
- root: {
- flex: 1,
- padding: 16,
- paddingBottom: 64,
- },
body: {
flexDirection: "column",
flex: 1,
+ padding: 16,
+ paddingBottom: 64,
},
sync: {
marginBottom: 16,
diff --git a/src/screens/AccountSettings/DeleteAccountModal.tsx b/src/screens/AccountSettings/DeleteAccountModal.tsx
new file mode 100644
index 0000000000..31ea937a51
--- /dev/null
+++ b/src/screens/AccountSettings/DeleteAccountModal.tsx
@@ -0,0 +1,55 @@
+import React, { memo } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { Account } from "@ledgerhq/live-common/lib/types";
+import { BottomDrawer, Flex, Text } from "@ledgerhq/native-ui";
+import { InfoMedium } from "@ledgerhq/native-ui/assets/icons";
+import Button from "../../components/wrappedUi/Button";
+
+type Props = {
+ onRequestClose: () => void;
+ deleteAccount: () => void;
+ account: Account;
+ isOpen: boolean;
+};
+
+function DeleteAccountModal({ isOpen, onRequestClose, deleteAccount }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t("account.settings.delete.confirmationDesc")}
+
+
+ {t("account.settings.delete.confirmationWarn")}
+
+
+
+
+
+
+ );
+}
+
+export default memo(DeleteAccountModal);
diff --git a/src/screens/AccountSettings/DeleteAccountRow.tsx b/src/screens/AccountSettings/DeleteAccountRow.tsx
new file mode 100644
index 0000000000..39908ed3a6
--- /dev/null
+++ b/src/screens/AccountSettings/DeleteAccountRow.tsx
@@ -0,0 +1,26 @@
+import React, { memo } from "react";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "styled-components/native";
+import SettingsRow from "../../components/SettingsRow";
+
+type Props = {
+ onPress: () => void;
+};
+
+function DeleteAccountRow({ onPress }: Props) {
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
+
+export default memo(DeleteAccountRow);
diff --git a/src/screens/AccountSettings/EditAccountName.js b/src/screens/AccountSettings/EditAccountName.js
index 8e6d9bf0a0..f85b33e362 100644
--- a/src/screens/AccountSettings/EditAccountName.js
+++ b/src/screens/AccountSettings/EditAccountName.js
@@ -92,10 +92,7 @@ class EditAccountName extends PureComponent {
forceInset={forceInset}
>
-
+
void;
+};
+
+type State = {
+ accountName: string;
+};
+
+const mapStateToProps = (state, { route }) =>
+ accountScreenSelector(route)(state);
+
+const mapDispatchToProps = {
+ updateAccount,
+};
+
+class EditAccountName extends PureComponent {
+ state = {
+ accountName: "",
+ };
+
+ onChangeText = (accountName: string) => {
+ this.setState({ accountName });
+ };
+
+ onNameEndEditing = () => {
+ const { updateAccount, account, navigation } = this.props;
+ const { accountName } = this.state;
+ const {
+ onAccountNameChange,
+ account: accountFromAdd,
+ } = this.props.route.params;
+
+ const isImportingAccounts = !!accountFromAdd;
+ const cleanAccountName = accountName.trim();
+
+ if (cleanAccountName.length) {
+ if (isImportingAccounts) {
+ onAccountNameChange(cleanAccountName, accountFromAdd);
+ } else {
+ updateAccount({
+ ...account,
+ name: cleanAccountName,
+ });
+ }
+ navigation.goBack();
+ }
+ };
+
+ render() {
+ const { account, colors } = this.props;
+ const { accountName } = this.state;
+ const { account: accountFromAdd } = this.props.route.params;
+
+ const initialAccountName = account ? account.name : accountFromAdd.name;
+
+ return (
+
+
+ this.setState({ accountName })}
+ onSubmitEditing={this.onNameEndEditing}
+ clearButtonMode="while-editing"
+ placeholder={i18next.t("account.settings.accountName.placeholder")}
+ />
+
+
+
+ );
+ }
+}
+
+const m: React.ComponentType<{}> = compose(
+ connect(mapStateToProps, mapDispatchToProps),
+ withTheme,
+)(EditAccountName);
+
+export default m;
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ },
+ safeArea: {
+ flex: 1,
+ },
+ body: {
+ flexDirection: "column",
+ flex: 1,
+ },
+ textInputAS: {
+ padding: 16,
+ fontSize: 20,
+ ...getFontStyle({ semiBold: true }),
+ },
+ buttonContainer: {
+ marginHorizontal: 16,
+ },
+ flex: {
+ flex: 1,
+ flexDirection: "column",
+ justifyContent: "flex-end",
+ paddingBottom: 16,
+ },
+});
diff --git a/src/screens/AccountSettings/EditAccountUnits.js b/src/screens/AccountSettings/EditAccountUnits.js
index 27f3234ebd..2cc95b34c1 100644
--- a/src/screens/AccountSettings/EditAccountUnits.js
+++ b/src/screens/AccountSettings/EditAccountUnits.js
@@ -39,7 +39,7 @@ export default function EditAccountUnits({ navigation, route }: Props) {
const accountUnits = account.currency.units;
return (
-
+
+ accountScreenSelector(route)(state);
+
+const mapDispatchToProps = {
+ deleteAccount,
+};
+
+class AccountSettings extends PureComponent {
+ state = {
+ isModalOpened: false,
+ };
+
+ onRequestClose = () => {
+ this.setState({ isModalOpened: false });
+ };
+
+ onPress = () => {
+ this.setState({ isModalOpened: true });
+ };
+
+ deleteAccount = () => {
+ const { account, deleteAccount, navigation } = this.props;
+ deleteAccount(account);
+ navigation.popToTop();
+ };
+
+ render() {
+ const { navigation, account } = this.props;
+ const { isModalOpened } = this.state;
+
+ if (!account) return null;
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AccountSettings);
diff --git a/src/screens/Accounts/AccountOrder.tsx b/src/screens/Accounts/AccountOrder.tsx
new file mode 100644
index 0000000000..82a5059ee0
--- /dev/null
+++ b/src/screens/Accounts/AccountOrder.tsx
@@ -0,0 +1,26 @@
+import React, { useState } from "react";
+import { SortAltMedium } from "@ledgerhq/native-ui/assets/icons";
+import Touchable from "../../components/Touchable";
+import AccountOrderModal from "./AccountOrderModal";
+import { useRefreshAccountsOrderingEffect } from "../../actions/general";
+
+export default function AccountOrder() {
+ const [isOpened, setIsOpened] = useState(false);
+
+ function onPress(): void {
+ setIsOpened(true);
+ }
+
+ function onClose(): void {
+ setIsOpened(false);
+ }
+
+ useRefreshAccountsOrderingEffect({ onUnmount: true });
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/screens/Accounts/AccountOrderModal.tsx b/src/screens/Accounts/AccountOrderModal.tsx
new file mode 100644
index 0000000000..f4f9c05656
--- /dev/null
+++ b/src/screens/Accounts/AccountOrderModal.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { BottomDrawer } from "@ledgerhq/native-ui";
+import OrderOption from "./OrderOption";
+
+const choices = ["balance|desc", "balance|asc", "name|asc", "name|desc"];
+
+type Props = {
+ isOpened: boolean;
+ onClose: () => void;
+};
+
+export default function AccountOrderModal({ onClose, isOpened }: Props) {
+ const { t } = useTranslation();
+ return (
+
+ {choices.map(id => (
+
+ ))}
+
+ );
+}
diff --git a/src/screens/Accounts/AccountRow.tsx b/src/screens/Accounts/AccountRow.tsx
new file mode 100644
index 0000000000..2616e8df78
--- /dev/null
+++ b/src/screens/Accounts/AccountRow.tsx
@@ -0,0 +1,156 @@
+import React, { useCallback, useMemo } from "react";
+import { TouchableOpacity } from "react-native";
+import useEnv from "@ledgerhq/live-common/lib/hooks/useEnv";
+import {
+ getAccountCurrency,
+ getAccountName,
+ getAccountUnit,
+} from "@ledgerhq/live-common/lib/account";
+import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+import {
+ Account,
+ Currency,
+ TokenAccount,
+} from "@ledgerhq/live-common/lib/types";
+import { Flex, ProgressLoader, Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import { useSelector } from "react-redux";
+import { useCalculate } from "@ledgerhq/live-common/lib/countervalues/react";
+import { BigNumber } from "bignumber.js";
+import { ScreenName } from "../../const";
+import CurrencyUnitValue from "../../components/CurrencyUnitValue";
+import CounterValue from "../../components/CounterValue";
+import CurrencyIcon from "../../components/CurrencyIcon";
+import { ensureContrast } from "../../colors";
+import Delta from "../../components/Delta";
+import { useBalanceHistoryWithCountervalue } from "../../actions/portfolio";
+import { counterValueCurrencySelector } from "../../reducers/settings";
+
+type Props = {
+ account: Account | TokenAccount;
+ accountId: string;
+ navigation: any;
+ isLast: boolean;
+ onSetAccount: (arg: TokenAccount) => void;
+ portfolioValue: number;
+};
+
+const AccountRow = ({
+ navigation,
+ account,
+ accountId,
+ portfolioValue,
+}: Props) => {
+ // makes it refresh if this changes
+ useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS");
+ const { colors } = useTheme();
+
+ const currency = getAccountCurrency(account);
+ const name = getAccountName(account);
+ const unit = getAccountUnit(account);
+
+ const color = useMemo(
+ () => ensureContrast(getCurrencyColor(currency), colors.constant.white),
+ [colors, currency],
+ );
+
+ const counterValueCurrency: Currency = useSelector(
+ counterValueCurrencySelector,
+ );
+
+ const countervalue = useCalculate({
+ from: currency,
+ to: counterValueCurrency,
+ value:
+ account.balance instanceof BigNumber
+ ? account.balance.toNumber()
+ : account.balance,
+ disableRounding: true,
+ });
+
+ const portfolioPercentage = useMemo(
+ () => (countervalue ? countervalue / Math.max(1, portfolioValue) : 0), // never divide by potential zero, we dont want to go towards infinity
+ [countervalue, portfolioValue],
+ );
+
+ const { countervalueChange } = useBalanceHistoryWithCountervalue({
+ account,
+ range: "day",
+ });
+
+ const onAccountPress = useCallback(() => {
+ if (account.type === "Account") {
+ navigation.navigate(ScreenName.Account, {
+ accountId,
+ isForwardedFromAccounts: true,
+ });
+ } else if (account.type === "TokenAccount") {
+ navigation.navigate(ScreenName.Account, {
+ parentId: account?.parentId,
+ accountId: account.id,
+ });
+ }
+ }, [account, accountId, navigation]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default React.memo(AccountRow);
diff --git a/src/screens/Accounts/AddAccount.tsx b/src/screens/Accounts/AddAccount.tsx
new file mode 100644
index 0000000000..468e3fcbd3
--- /dev/null
+++ b/src/screens/Accounts/AddAccount.tsx
@@ -0,0 +1,44 @@
+import React, { useState } from "react";
+import { useNavigation } from "@react-navigation/native";
+import { Flex } from "@ledgerhq/native-ui";
+import { PlusMedium } from "@ledgerhq/native-ui/assets/icons";
+import Touchable from "../../components/Touchable";
+import AddAccountsModal from "../AddAccounts/AddAccountsModal";
+
+export default function AddAccount() {
+ const navigation = useNavigation();
+
+ const [isAddModalOpened, setIsAddModalOpened] = useState(false);
+
+ function openAddModal() {
+ setIsAddModalOpened(true);
+ }
+
+ function closeAddModal() {
+ setIsAddModalOpened(false);
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/screens/Accounts/OrderOption.tsx b/src/screens/Accounts/OrderOption.tsx
new file mode 100644
index 0000000000..2bf345a674
--- /dev/null
+++ b/src/screens/Accounts/OrderOption.tsx
@@ -0,0 +1,50 @@
+import React, { useCallback } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components/native";
+import { Checkbox, Text } from "@ledgerhq/native-ui";
+import { useRefreshAccountsOrdering } from "../../actions/general";
+import { setOrderAccounts } from "../../actions/settings";
+import { orderAccountsSelector } from "../../reducers/settings";
+import Touchable from "../../components/Touchable";
+
+type Props = {
+ id: string;
+};
+
+const StyledTouchableRow = styled(Touchable)`
+ align-items: center;
+ justify-content: space-between;
+ flex-direction: row;
+ margin-bottom: ${p => p.theme.space[8]}px;
+`;
+
+export default function OrderOption({ id }: Props) {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const orderAccounts = useSelector(orderAccountsSelector);
+ const refreshAccountsOrdering = useRefreshAccountsOrdering();
+
+ const onPress = useCallback(() => {
+ dispatch(setOrderAccounts(`${id}`));
+ refreshAccountsOrdering();
+ }, [dispatch, id, refreshAccountsOrdering]);
+
+ const selected = orderAccounts === id;
+ return (
+
+
+ {t(`orderOption.choices.${id}`)}
+
+ {selected && }
+
+ );
+}
diff --git a/src/screens/Accounts/index.tsx b/src/screens/Accounts/index.tsx
new file mode 100644
index 0000000000..ee013ef727
--- /dev/null
+++ b/src/screens/Accounts/index.tsx
@@ -0,0 +1,153 @@
+import React, { useCallback, useRef, useState, useEffect } from "react";
+import { FlatList, TouchableOpacity } from "react-native";
+import { useSelector } from "react-redux";
+import { useFocusEffect } from "@react-navigation/native";
+import { Account } from "@ledgerhq/live-common/lib/types";
+import { findCryptoCurrencyByKeyword } from "@ledgerhq/live-common/lib/currencies";
+import {
+ Box,
+ Flex,
+ Icons,
+ ScrollContainerHeader,
+ Text,
+} from "@ledgerhq/native-ui";
+
+import { flattenAccounts } from "@ledgerhq/live-common/lib/account";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useTranslation } from "react-i18next";
+import { useRefreshAccountsOrdering } from "../../actions/general";
+import { accountsSelector } from "../../reducers/accounts";
+import globalSyncRefreshControl from "../../components/globalSyncRefreshControl";
+import TrackScreen from "../../analytics/TrackScreen";
+
+import NoAccounts from "./NoAccounts";
+import AccountRow from "./AccountRow";
+import MigrateAccountsBanner from "../MigrateAccounts/Banner";
+import { useScrollToTop } from "../../navigation/utils";
+import TokenContextualModal from "../Settings/Accounts/TokenContextualModal";
+import { ScreenName } from "../../const";
+import { withDiscreetMode } from "../../context/DiscreetModeContext";
+import { usePortfolio } from "../../actions/portfolio";
+import AddAccount from "./AddAccount";
+import AccountOrder from "./AccountOrder";
+
+const List = globalSyncRefreshControl(FlatList);
+
+type Props = {
+ navigation: any;
+ route: { params?: { currency?: string } };
+};
+
+function Accounts({ navigation, route }: Props) {
+ const accounts = useSelector(accountsSelector);
+ const ref = useRef();
+ useScrollToTop(ref);
+ const portfolio = usePortfolio();
+ const { t } = useTranslation();
+
+ const refreshAccountsOrdering = useRefreshAccountsOrdering();
+ useFocusEffect(refreshAccountsOrdering);
+
+ const { params } = route;
+
+ const [account, setAccount] = useState(undefined);
+
+ const flattenedAccounts = flattenAccounts(accounts);
+
+ // Deep linking params redirect
+ useEffect(() => {
+ if (params) {
+ if (params.currency) {
+ const currency = findCryptoCurrencyByKeyword(
+ params.currency.toUpperCase(),
+ );
+ if (currency) {
+ const account = accounts.find(acc => acc.currency.id === currency.id);
+
+ if (account) {
+ // reset params so when we come back the redirection doesn't loop
+ navigation.setParams({ ...params, currency: undefined });
+ navigation.navigate(ScreenName.Account, {
+ accountId: account.id,
+ isForwardedFromAccounts: true,
+ });
+ }
+ }
+ }
+ }
+ }, [params, accounts, navigation]);
+
+ const renderItem = useCallback(
+ ({ item, index }: { item: Account; index: number }) => (
+
+ ),
+ [navigation, accounts.length],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ TopRightSection={
+
+
+ {!flattenedAccounts.length ? null : }
+
+
+
+ }
+ MiddleSection={
+
+ {t("distribution.title")}
+
+ }
+ >
+
+ item.id}
+ ListEmptyComponent={}
+ />
+
+ setAccount(undefined)}
+ isOpened={!!account}
+ account={account}
+ />
+
+
+
+
+ );
+}
+
+export default withDiscreetMode(Accounts);
diff --git a/src/screens/AddAccounts/04-Success.js b/src/screens/AddAccounts/04-Success.js
index 7afb9ae07b..b8f006b048 100644
--- a/src/screens/AddAccounts/04-Success.js
+++ b/src/screens/AddAccounts/04-Success.js
@@ -5,7 +5,6 @@ import { Trans } from "react-i18next";
import { StyleSheet, View } from "react-native";
import type { CryptoCurrency } from "@ledgerhq/live-common/lib/types";
-import Icon from "react-native-vector-icons/dist/Feather";
import { useTheme } from "@react-navigation/native";
import { ScreenName, NavigatorName } from "../../const";
import { rgba } from "../../colors";
@@ -14,6 +13,7 @@ import LText from "../../components/LText";
import Button from "../../components/Button";
import IconCheck from "../../icons/Check";
import CurrencyIcon from "../../components/CurrencyIcon";
+import { Icons } from "@ledgerhq/native-ui";
type Props = {
navigation: any,
@@ -25,11 +25,6 @@ type RouteParams = {
deviceId: string,
};
-const IconPlus = () => {
- const { colors } = useTheme();
- return ;
-};
-
export default function AddAccountsSuccess({ navigation, route }: Props) {
const { colors } = useTheme();
@@ -62,13 +57,14 @@ export default function AddAccountsSuccess({ navigation, route }: Props) {
event="AddAccountsDone"
containerStyle={styles.button}
type="primary"
+ outline={false}
title={}
onPress={primaryCTA}
/>
}
/>
diff --git a/src/screens/AddAccounts/AddAccountsModal.tsx b/src/screens/AddAccounts/AddAccountsModal.tsx
new file mode 100644
index 0000000000..4bb43ce798
--- /dev/null
+++ b/src/screens/AddAccounts/AddAccountsModal.tsx
@@ -0,0 +1,78 @@
+import React, { useCallback } from "react";
+import { TouchableOpacity, TouchableOpacityProps } from "react-native";
+import { useTranslation } from "react-i18next";
+import { useSelector } from "react-redux";
+import { BottomDrawer, Box, Flex, Text } from "@ledgerhq/native-ui";
+import { NavigatorName } from "../../const";
+import { readOnlyModeEnabledSelector } from "../../reducers/settings";
+import Illustration from "../../images/illustration/Illustration";
+import NanoXFolded from "../../images/devices/NanoXFolded";
+
+import ChoiceCard from "../../components/ChoiceCard";
+
+const images = {
+ light: {
+ withYourLedger: require("../../images/illustration/Light/_067.png"),
+ importFromYourDesktop: require("../../images/illustration/Light/_074.png"),
+ },
+ dark: {
+ withYourLedger: require("../../images/illustration/Dark/_067.png"),
+ importFromYourDesktop: require("../../images/illustration/Dark/_074.png"),
+ },
+};
+
+type Props = {
+ navigation: any;
+ isOpened: boolean;
+ onClose: () => void;
+};
+
+export default function AddAccountsModal({
+ navigation,
+ onClose,
+ isOpened,
+}: Props) {
+ const { t } = useTranslation();
+ const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
+
+ const onClickAdd = useCallback(() => {
+ navigation.navigate(NavigatorName.AddAccounts);
+ onClose();
+ }, [navigation, onClose]);
+
+ const onClickImport = useCallback(() => {
+ navigation.navigate(NavigatorName.ImportAccounts);
+ onClose();
+ }, [navigation, onClose]);
+
+ return (
+
+ {!readOnlyModeEnabled && (
+ }
+ onPress={onClickAdd}
+ />
+ )}
+
+
+ }
+ onPress={onClickImport}
+ />
+
+ );
+}
diff --git a/src/screens/Analytics/Allocation.tsx b/src/screens/Analytics/Allocation.tsx
new file mode 100644
index 0000000000..83434d2bc2
--- /dev/null
+++ b/src/screens/Analytics/Allocation.tsx
@@ -0,0 +1,64 @@
+import React, { useCallback } from "react";
+import { Dimensions, FlatList } from "react-native";
+import styled from "styled-components/native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import { useTranslation } from "react-i18next";
+import RingChart from "./RingChart";
+import { useDistribution } from "../../actions/general";
+import DistributionCard, { DistributionItem } from "./DistributionCard";
+import { TrackScreen } from "../../analytics";
+
+const Container = styled(Flex).attrs({
+ paddingHorizontal: 16,
+ paddingVertical: 20,
+})``;
+
+const AssetWrapperContainer = styled(Flex).attrs({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: "center",
+ alignItems: "center",
+})``;
+
+export default function Allocation() {
+ const size = Dimensions.get("window").width * (1 / 2);
+ const distribution = useDistribution();
+ const { colors } = useTheme();
+ const { t } = useTranslation();
+
+ const renderItem = useCallback(
+ ({ item }: { item: DistributionItem; index: number }) => (
+
+ ),
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+
+ {distribution.list.length}
+
+
+ {t("distribution.assets", { count: distribution.list.length })}
+
+
+
+ item.currency.id}
+ style={{ width: "100%" }}
+ />
+
+
+ );
+}
diff --git a/src/screens/Analytics/DistributionCard.tsx b/src/screens/Analytics/DistributionCard.tsx
new file mode 100644
index 0000000000..8c1cb70c4f
--- /dev/null
+++ b/src/screens/Analytics/DistributionCard.tsx
@@ -0,0 +1,116 @@
+// @flow
+import React, { useMemo } from "react";
+import { StyleSheet } from "react-native";
+import type {
+ CryptoCurrency,
+ TokenCurrency,
+} from "@ledgerhq/live-common/lib/types/currencies";
+import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+
+import styled, { useTheme } from "styled-components/native";
+import { Text, Flex } from "@ledgerhq/native-ui";
+import CurrencyUnitValue from "../../components/CurrencyUnitValue";
+import ProgressBar from "../../components/ProgressBar";
+import CounterValue from "../../components/CounterValue";
+import ParentCurrencyIcon from "../../components/ParentCurrencyIcon";
+import { ensureContrast } from "../../colors";
+import { useSelector } from "react-redux";
+import { localeSelector } from "../../reducers/settings";
+
+export type DistributionItem = {
+ currency: CryptoCurrency | TokenCurrency,
+ distribution: number, // % of the total (normalized in 0-1)
+ amount: number,
+ countervalue: number, // countervalue of the amount that was calculated based of the rate provided
+};
+
+type Props = {
+ item: DistributionItem,
+};
+
+const Container = styled(Flex).attrs({
+ alignItems: "center",
+ justifyContent: "center",
+ marginBottom: 32,
+ width: "100%",
+})``;
+
+const IconContainer = styled(Flex).attrs({
+ marginRight: 6,
+ alignItems: "center",
+ justifyContent: "center",
+})``;
+
+const CoinInfoContainer = styled(Flex).attrs({
+ flexGrow: 1,
+ flexShrink: 1,
+ flexDirection: "column",
+})``;
+
+const CurrencyRow = styled(Flex).attrs({
+ flexDirection: "row",
+ justifyContent: "space-between",
+ flexWrap: "wrap",
+})``;
+
+const RateRow = styled(Flex).attrs({
+ flexDirection: "row",
+ justifyContent: "space-between",
+ flexWrap: "wrap",
+})``;
+
+
+const DistributionRow = styled(Flex).attrs({
+ marginTop: 4,
+ flexDirection: "row",
+ alignItems: "center",
+})``;
+
+export default function DistributionCard({
+ item: { currency, amount, distribution },
+}: Props) {
+ const { colors } = useTheme();
+ const locale = useSelector(localeSelector);
+ const color = useMemo(
+ () => ensureContrast(getCurrencyColor(currency), colors.background.main),
+ [colors, currency],
+ );
+ const percentage = Math.round(distribution * 1e4) / 1e2;
+
+ return (
+
+
+
+
+
+
+
+
+ {currency.name}
+
+
+ {`${percentage.toLocaleString(locale)}%`}
+
+
+ {distribution ? (
+ <>
+
+
+
+
+
+
+
+
+ >
+ ) : null}
+
+
+ {distribution ? (
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/screens/Analytics/Operations.tsx b/src/screens/Analytics/Operations.tsx
new file mode 100644
index 0000000000..63660c6e3c
--- /dev/null
+++ b/src/screens/Analytics/Operations.tsx
@@ -0,0 +1,143 @@
+import React, { memo, useState, useCallback } from "react";
+import { SectionList } from "react-native";
+import { Flex } from "@ledgerhq/native-ui";
+
+import { useSelector } from "react-redux";
+import { useFocusEffect } from "@react-navigation/native";
+import type { SectionBase } from "react-native/Libraries/Lists/SectionList";
+import type { Operation } from "@ledgerhq/live-common/lib/types";
+import { groupAccountsOperationsByDay } from "@ledgerhq/live-common/lib/account/groupOperations";
+import { isAccountEmpty } from "@ledgerhq/live-common/lib/account/helpers";
+
+import { Trans } from "react-i18next";
+import { useRefreshAccountsOrdering } from "../../actions/general";
+import {
+ accountsSelector,
+ flattenAccountsSelector,
+} from "../../reducers/accounts";
+
+import NoOperationFooter from "../../components/NoOperationFooter";
+import NoMoreOperationFooter from "../../components/NoMoreOperationFooter";
+
+import EmptyStatePortfolio from "../Portfolio/EmptyStatePortfolio";
+import NoOpStatePortfolio from "../Portfolio/NoOpStatePortfolio";
+import OperationRow from "../../components/OperationRow";
+import SectionHeader from "../../components/SectionHeader";
+import LoadingFooter from "../../components/LoadingFooter";
+import Button from "../../components/Button";
+import { ScreenName } from "../../const";
+import { TrackScreen } from "../../analytics";
+
+type Props = {
+ navigation: any;
+};
+
+export function Operations({ navigation }: Props) {
+ const [opCount, setOpCount] = useState(50);
+
+ function onEndReached() {
+ setOpCount(opCount + 50);
+ }
+
+ const accounts = useSelector(accountsSelector);
+ const allAccounts = useSelector(flattenAccountsSelector);
+
+ const refreshAccountsOrdering = useRefreshAccountsOrdering();
+ useFocusEffect(refreshAccountsOrdering);
+
+ const { sections, completed } = groupAccountsOperationsByDay(accounts, {
+ count: opCount,
+ withSubAccounts: true,
+ });
+
+ function ListEmptyComponent() {
+ if (accounts.length === 0) {
+ return ;
+ }
+
+ if (accounts.every(isAccountEmpty)) {
+ return ;
+ }
+
+ return null;
+ }
+
+ function keyExtractor(item: Operation) {
+ return item.id;
+ }
+
+ function renderItem({
+ item,
+ index,
+ section,
+ }: {
+ item: Operation;
+ index: number;
+ section: SectionBase;
+ }) {
+ const account = allAccounts.find(a => a.id === item.accountId);
+ const parentAccount =
+ account && account.type !== "Account"
+ ? accounts.find(a => a.id === account.parentId)
+ : null;
+
+ if (!account) return null;
+
+ return (
+
+ );
+ }
+
+ function renderSectionHeader({ section }: { section: { day: Date } }) {
+ return ;
+ }
+
+ const onTransactionButtonPress = useCallback(() => {
+ navigation.navigate(ScreenName.PortfolioOperationHistory);
+ }, [navigation]);
+
+ return (
+
+
+ }
+ onPress={onTransactionButtonPress}
+ />
+
+ ) : (
+
+ )
+ ) : accounts.every(isAccountEmpty) ? null : sections.length ? (
+
+ ) : (
+
+ )
+ }
+ ListEmptyComponent={ListEmptyComponent}
+ />
+
+
+ );
+}
+
+export default memo(Operations);
diff --git a/src/screens/Analytics/RingChart.tsx b/src/screens/Analytics/RingChart.tsx
new file mode 100644
index 0000000000..7d6684b231
--- /dev/null
+++ b/src/screens/Analytics/RingChart.tsx
@@ -0,0 +1,92 @@
+// @flow
+
+import React, { PureComponent } from "react";
+import * as d3shape from "d3-shape";
+import { View } from "react-native";
+import Svg, { Path, G, Circle } from "react-native-svg";
+import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies";
+import type { DistributionItem } from "./DistributionCard";
+import { ensureContrast, withTheme } from "../../colors";
+
+type Props = {
+ data: Array,
+ size: number,
+ colors: any,
+};
+
+class RingChart extends PureComponent {
+ arcGenerator = d3shape.arc();
+ offsetX = 0;
+ offsetY = 0;
+ paths: any = {};
+
+ constructor(props: Props) {
+ super(props);
+ this.generatePaths();
+ }
+
+ generatePaths = () => {
+ const { data } = this.props;
+ this.paths = data.reduce(this.reducer, {
+ items: [],
+ angle: 0,
+ });
+ };
+
+ componentDidUpdate() {
+ this.generatePaths();
+ }
+
+ reducer = (data: any, item: DistributionItem, index: number) => {
+ const increment = item.distribution * 2 * Math.PI;
+ const innerRadius = 0;
+
+ const pathData = this.arcGenerator({
+ startAngle: data.angle,
+ endAngle: data.angle + increment,
+ innerRadius,
+ outerRadius: 30,
+ });
+
+ const parsedItem = {
+ color: ensureContrast(
+ getCurrencyColor(item.currency),
+ this.props.colors.background.main,
+ ),
+ pathData,
+ endAngle: data.angle + increment,
+ id: item.currency.id,
+ index,
+ };
+
+ return {
+ items: [...data.items, parsedItem],
+ angle: data.angle + increment,
+ };
+ };
+
+ render() {
+ const { size, colors } = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default withTheme(RingChart);
diff --git a/src/screens/BuyDeviceScreen.tsx b/src/screens/BuyDeviceScreen.tsx
new file mode 100644
index 0000000000..6c28dcd9cc
--- /dev/null
+++ b/src/screens/BuyDeviceScreen.tsx
@@ -0,0 +1,138 @@
+import React, { useCallback } from "react";
+import {
+ Flex,
+ Icons,
+ Text,
+ IconBoxList,
+ Link as TextLink,
+} from "@ledgerhq/native-ui";
+import Video from "react-native-video";
+import styled, { useTheme } from "styled-components/native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useNavigation } from "@react-navigation/native";
+import { useTranslation } from "react-i18next";
+import { Linking } from "react-native";
+import Button from "../components/wrappedUi/Button";
+import { urls } from "../config/urls";
+import { useNavigationInterceptor } from "./Onboarding/onboardingContext";
+import { NavigatorName, ScreenName } from "../const";
+
+const StyledSafeAreaView = styled(SafeAreaView)`
+ flex: 1;
+ background-color: ${({ theme }) => theme.colors.background.main};
+`;
+
+const sourceDark = require("../../assets/videos/NanoX_LL_black.mp4");
+const sourceLight = require("../../assets/videos/NanoX_LL_White.mp4");
+
+const items = [
+ {
+ title: "buyDevice.0.title",
+ desc: "buyDevice.0.desc",
+ Icon: Icons.CrownMedium,
+ },
+ {
+ title: "buyDevice.1.title",
+ desc: "buyDevice.1.desc",
+ Icon: Icons.LendMedium,
+ },
+ {
+ title: "buyDevice.2.title",
+ desc: "buyDevice.2.desc",
+ Icon: Icons.ClaimRewardsMedium,
+ },
+ {
+ title: "buyDevice.3.title",
+ desc: "buyDevice.3.desc",
+ Icon: Icons.NanoXAltMedium,
+ },
+];
+
+export default function BuyDeviceScreen() {
+ const { t } = useTranslation();
+ const navigation = useNavigation();
+ const { theme, colors } = useTheme();
+ const { setShowWelcome, setFirstTimeOnboarding } = useNavigationInterceptor();
+
+ const handleBack = useCallback(() => navigation.goBack(), [navigation]);
+
+ const setupDevice = useCallback(() => {
+ setShowWelcome(false);
+ setFirstTimeOnboarding(false);
+ navigation.navigate(NavigatorName.BaseOnboarding, {
+ screen: NavigatorName.Onboarding,
+ params: {
+ screen: ScreenName.OnboardingDeviceSelection,
+ },
+ });
+ }, [navigation, setFirstTimeOnboarding, setShowWelcome]);
+
+ const buyLedger = useCallback(() => {
+ Linking.openURL(urls.buyNanoX);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {t("buyDevice.title")}
+
+
+ {t("buyDevice.desc")}
+
+
+ ({
+ ...item,
+ title: t(item.title),
+ description: t(item.desc),
+ }))}
+ />
+
+
+
+
+ {t("buyDevice.footer")}
+
+
+
+ );
+}
diff --git a/src/screens/Discover/DiscoverCard.tsx b/src/screens/Discover/DiscoverCard.tsx
new file mode 100644
index 0000000000..2def8725cb
--- /dev/null
+++ b/src/screens/Discover/DiscoverCard.tsx
@@ -0,0 +1,109 @@
+import React from "react";
+import { TouchableOpacityProps } from "react-native";
+import { Flex, Text } from "@ledgerhq/native-ui";
+import { useTheme } from "styled-components/native";
+import Touchable from "../../components/Touchable";
+import LedgerLogoRec from "../../icons/LedgerLogoRec";
+
+const DiscoverCard = ({
+ title,
+ titleProps,
+ subTitle,
+ subTitleProps,
+ labelBadge,
+ onPress,
+ Image,
+ disabled,
+ ...props
+}: {
+ title: string;
+ titleProps?: any;
+ subTitle?: string;
+ subTitleProps?: any;
+ labelBadge?: string;
+ Image: React.ReactNode;
+ onPress: TouchableOpacityProps["onPress"];
+ disabled?: boolean;
+}) => {
+ const { colors } = useTheme();
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+ {subTitle && (
+
+ {subTitle}
+
+ )}
+
+
+
+ {Image}
+
+
+
+ {labelBadge && (
+
+
+ {labelBadge}
+
+
+ )}
+
+ );
+};
+
+export default DiscoverCard;
diff --git a/src/screens/Discover/index.tsx b/src/screens/Discover/index.tsx
new file mode 100644
index 0000000000..c50120c9e7
--- /dev/null
+++ b/src/screens/Discover/index.tsx
@@ -0,0 +1,148 @@
+import React, { memo, useCallback, useMemo } from "react";
+import { Linking, Platform, ScrollView } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import styled from "styled-components/native";
+import { Flex, Text, Link as TextLink } from "@ledgerhq/native-ui";
+import { useTranslation } from "react-i18next";
+import { useNavigation } from "@react-navigation/native";
+import useFeature from "@ledgerhq/live-common/lib/featureFlags/useFeature";
+import Illustration from "../../images/illustration/Illustration";
+import { NavigatorName, ScreenName } from "../../const";
+import DiscoverCard from "./DiscoverCard";
+import { urls } from "../../config/urls";
+
+const discoverImg = {
+ dark: require("../../images/illustration/Dark/_030.png"),
+ light: require("../../images/illustration/Light/_030.png"),
+};
+
+const learnImg = {
+ dark: require("../../images/illustration/Dark/_Learn.png"),
+ light: require("../../images/illustration/Light/_Learn.png"),
+};
+
+const appsImg = {
+ dark: require("../../images/illustration/Dark/_Apps.png"),
+ light: require("../../images/illustration/Light/_Apps.png"),
+};
+
+const earnImg = {
+ dark: require("../../images/illustration/Dark/_Earn.png"),
+ light: require("../../images/illustration/Light/_Earn.png"),
+};
+
+const StyledSafeAreaView = styled(SafeAreaView)`
+ flex: 1;
+ backgroundcolor: ${({ theme }) => theme.colors.background.main};
+`;
+
+function Discover() {
+ const { t } = useTranslation();
+ const navigation = useNavigation();
+
+ const onTellMeMore = useCallback(() => {
+ Linking.openURL(urls.discover.academy);
+ }, []);
+
+ const learn = useFeature("learn");
+
+ const featuresList = useMemo(
+ () =>
+ [
+ {
+ title: t("discover.sections.learn.title"),
+ subTitle: t("discover.sections.learn.desc"),
+ onPress: () => {
+ // TODO: FIX @react-navigation/native using Typescript
+ // @ts-ignore next-line
+ navigation.navigate(ScreenName.Learn);
+ },
+ disabled: !learn?.enabled,
+ labelBadge: !learn?.enabled ? t("discover.comingSoon") : undefined,
+ Image: (
+
+ ),
+ },
+ {
+ title: t("discover.sections.ledgerApps.title"),
+ subTitle: t("discover.sections.ledgerApps.desc"),
+ onPress: () => {
+ if (Platform.OS !== "ios") {
+ // TODO: FIX @react-navigation/native using Typescript
+ // @ts-ignore next-line
+ navigation.navigate(NavigatorName.Discover, {
+ screen: ScreenName.PlatformCatalog,
+ });
+ } else Linking.openURL(urls.discover.ledgerApps);
+ },
+ Image: (
+
+ ),
+ },
+ {
+ title: t("discover.sections.earn.title"),
+ subTitle: t("discover.sections.earn.desc"),
+ onPress: () => {
+ Linking.openURL(urls.discover.earn);
+ },
+ labelBadge: t("discover.mostPopular"),
+ Image: (
+
+ ),
+ },
+ ].sort((a, b) => (b.disabled ? -1 : 0)),
+ [learn?.enabled, navigation, t],
+ );
+
+ return (
+
+
+
+
+ {t("discover.title")}
+
+ {t("discover.desc")}
+
+
+ {t("discover.link")}
+
+
+
+
+
+
+ {featuresList.map(
+ ({ title, subTitle, onPress, disabled, labelBadge, Image }, i) => (
+
+ ),
+ )}
+
+
+ );
+}
+
+export default memo(Discover);
diff --git a/src/screens/EditDeviceName.js b/src/screens/EditDeviceName.js
index 9e0cd6a715..21a1c1f8a5 100644
--- a/src/screens/EditDeviceName.js
+++ b/src/screens/EditDeviceName.js
@@ -155,7 +155,7 @@ class EditDeviceName extends PureComponent<
- {error ? : null}
+