diff --git a/scripts/v3check.js b/scripts/v3check.js new file mode 100644 index 0000000000..83a0118878 --- /dev/null +++ b/scripts/v3check.js @@ -0,0 +1,123 @@ +const exec = require("child_process").exec; + +/* eslint-disable no-console */ + +const LOG_COMMANDS = false; +const TEST_FILES = false; +const TS_TEST_FILES = [ + // "src/screens/Settings/General/index.tsx", + "src/components/CurrencyRate.tsx", +]; + +async function executeAsync(command) { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(error); + } else if (stderr) { + reject(stderr); + } else { + resolve(stdout); + } + }); + }); +} + +async function listTsJsFilesPairs() { + const jsFilesPath = {}; + const jsTsPairs = []; + await executeAsync('git ls-files | grep -e ".\\.js$"').then(res => + res.split("\n").map(path => (jsFilesPath[path] = true)), + ); + return executeAsync('git ls-files | grep -e ".\\.ts$" -e ".\\.tsx$"') + .then(res => + res.split("\n").forEach(path => { + const beforeExtensionPath = path + .split(".") + .slice(0, -1) + .join("."); + const jsFilePath = `${beforeExtensionPath}.js`; + if (jsFilesPath[jsFilePath]) { + jsTsPairs.push({ + tsFilePath: path, + jsFilePath, + }); + } + }), + ) + .then(() => jsTsPairs); +} + +const branchRebrand = "LL-7742"; +const branchRelease = "release/v3.0.x"; +const mainBranchName = "develop"; + +async function getFileCreationDate(filePath, firstParent) { + const command = `git log --format=%aD --first-parent ${firstParent} --no-merges ${filePath} | tail -1`; + LOG_COMMANDS && console.log(`getFileCreationDate command:\n\t${command}`); + const rawDate = await executeAsync(command); + return { date: rawDate ? new Date(rawDate) : null, command }; +} + +async function listCommits(filePath, sinceDateIsoString) { + const command = `git log \ + --first-parent \ + ${mainBranchName} \ + ${sinceDateIsoString ? `--since=${sinceDateIsoString}` : ""} \ + --pretty=format:"%aI %H %s" \ + ${filePath}`; + LOG_COMMANDS && console.log(`listCommits command:\n\t${command}`); + const rawResult = await executeAsync(command); + const commitsRaw = rawResult.split("\n").filter(l => !!l); + return commitsRaw.map(commitRaw => { + const [dateStr, hashStr, ...rest] = commitRaw.split(" "); + const message = rest.join(" "); + return { + date: new Date(dateStr), + hashStr, + message, + command, + }; + }); +} + +function pbcopy(data) { + const proc = require("child_process").spawn("pbcopy"); + proc.stdin.write(data); + proc.stdin.end(); +} + +async function generateMarkdownTodoList() { + const pairs = await listTsJsFilesPairs(); + let todoList = ""; + const res = await Promise.all( + pairs + .filter( + ({ tsFilePath }) => !TEST_FILES || TS_TEST_FILES.includes(tsFilePath), + ) + .map(async ({ tsFilePath, jsFilePath }) => { + const { date: tsFileCreationDate } = await getFileCreationDate( + tsFilePath, + branchRebrand, + ).then(res => + res === null ? getFileCreationDate(tsFilePath, branchRelease) : res, + ); + const commits = tsFileCreationDate + ? await listCommits(jsFilePath, tsFileCreationDate.toISOString()) + : []; + return { tsFilePath, jsFilePath, commits }; + }), + ); + res.forEach(({ jsFilePath, commits }) => { + if (commits.length === 0) return; + console.log("jsFilePath", jsFilePath); + todoList += `- [ ] **${jsFilePath}**\n`; + commits.forEach(({ message, date, hashStr }) => { + todoList += ` - [ ] ${date.toDateString()} ${hashStr} ${message}\n`; + }); + todoList += "\n"; + }); + return todoList; +} + +generateMarkdownTodoList().then(pbcopy); diff --git a/src/components/GenericErrorView.tsx b/src/components/GenericErrorView.tsx index 0cebbfebc5..e050a6f75d 100644 --- a/src/components/GenericErrorView.tsx +++ b/src/components/GenericErrorView.tsx @@ -1,19 +1,15 @@ -import React from "react"; +import React, { memo, useEffect } from "react"; import { Trans } from "react-i18next"; +import { NativeModules } from "react-native"; import { Box, Button, Flex, IconBox, Text } from "@ledgerhq/native-ui"; import { CloseMedium } from "@ledgerhq/native-ui/assets/icons"; +import { BluetoothRequired } from "@ledgerhq/errors"; 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, -}: { +type Props = { 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 "" @@ -22,7 +18,23 @@ const GenericErrorView = ({ withDescription?: boolean; withIcon?: boolean; hasExportLogButton?: boolean; -}) => { +}; + +const GenericErrorView = ({ + error, + outerError, + withDescription = true, + withIcon = true, + hasExportLogButton = true, +}: Props) => { + useEffect(() => { + if (error instanceof BluetoothRequired) { + NativeModules.BluetoothHelperModule.prompt().catch(() => { + /* ignore */ + }); + } + }, [error]); + const onExport = useExportLogs(); const titleError = outerError || error; @@ -78,4 +90,4 @@ const GenericErrorView = ({ ); }; -export default GenericErrorView; +export default memo(GenericErrorView); diff --git a/src/components/OperationRow.tsx b/src/components/OperationRow.tsx index ba10136e9f..11a0544884 100644 --- a/src/components/OperationRow.tsx +++ b/src/components/OperationRow.tsx @@ -26,6 +26,7 @@ import CounterValue from "./CounterValue"; import OperationIcon from "./OperationIcon"; import { ScreenName } from "../const"; import OperationRowDate from "./OperationRowDate"; +import OperationRowNftName from "./OperationRowNftName"; import perFamilyOperationDetails from "../generated/operationDetails"; @@ -60,13 +61,13 @@ const BodyLeftContainer = styled(Flex).attrs({ flex: 1, })``; -const BodyRightContainer = styled(Flex).attrs({ +const BodyRightContainer = styled(Flex).attrs(p => ({ flexDirection: "column", justifyContent: "flex-start", alignItems: "flex-end", - flexShrink: 0, + flexShrink: p.flexShrink ?? 0, pl: 4, -})``; +}))``; type Props = { operation: Operation; @@ -109,6 +110,11 @@ export default function OperationRow({ else navigation.navigate(...params); }, 300); + const isNftOperation = + ["NFT_IN", "NFT_OUT"].includes(operation.type) && + operation.contract && + operation.tokenId; + const renderAmountCellExtra = useCallback(() => { const mainAccount = getMainAccount(account, parentAccount); const currency = getAccountCurrency(account); @@ -201,7 +207,11 @@ export default function OperationRow({ {renderAmountCellExtra()} - {amount.isZero() ? null : ( + {isNftOperation ? ( + + + + ) : amount.isZero() ? null : ( { + const { status, metadata } = useNftMetadata( + operation.contract, + operation.tokenId, + ); + + return ( + <> + + + {metadata?.nftName || "-"} + + + + ID {operation.tokenId} + + + ); +}; + +const styles = StyleSheet.create({ + skeleton: { + height: 8, + width: 120, + borderRadius: 4, + }, +}); + +export default OperationRowNftName; diff --git a/src/components/SelectDevice/index.tsx b/src/components/SelectDevice/index.tsx index 39a9e62e82..062a5b9d7b 100644 --- a/src/components/SelectDevice/index.tsx +++ b/src/components/SelectDevice/index.tsx @@ -3,7 +3,10 @@ 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 { + useNavigation, + useTheme as useNavTheme, +} 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"; @@ -16,10 +19,11 @@ 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"; +import PairLight from "../../screens/Onboarding/assets/nanoX/pairDevice/light.json"; +import PairDark from "../../screens/Onboarding/assets/nanoX/pairDevice/dark.json"; + type Props = { onBluetoothDeviceAction?: (device: Device) => void; onSelect: (device: Device) => void; @@ -223,11 +227,14 @@ const WithoutDeviceHeader = () => ( ); // Fixme Use the illustration instead of the png -const UsbPlaceholder = () => ( - - - -); +const UsbPlaceholder = () => { + const { dark } = useNavTheme(); + return ( + + + + ); +}; function getAll({ knownDevices }, { devices }): Device[] { return [ diff --git a/src/screens/Onboarding/steps/language.tsx b/src/screens/Onboarding/steps/language.tsx index df90b08d33..de0bb96d70 100644 --- a/src/screens/Onboarding/steps/language.tsx +++ b/src/screens/Onboarding/steps/language.tsx @@ -11,33 +11,11 @@ import { import { StackScreenProps } from "@react-navigation/stack"; import { useDispatch } from "react-redux"; import { useLocale } from "../../../context/Locale"; -import { supportedLocales } from "../../../languages"; +import { languages, supportedLocales } from "../../../languages"; import Button from "../../../components/Button"; import { ScreenName } from "../../../const"; import { setLanguage } from "../../../actions/settings"; -const languages = { - de: "Deutsch", - el: "Ελληνικά", - en: "English", - es: "Español", - fi: "suomi", - fr: "Français", - hu: "magyar", - it: "italiano", - ja: "日本語", - ko: "한국어", - nl: "Nederlands", - no: "Norsk", - pl: "polski", - pt: "português", - ru: "Русский", - sr: "српски", - sv: "svenska", - tr: "Türkçe", - zh: "简体中文", -}; - function OnboardingStepLanguage({ navigation }: StackScreenProps<{}>) { const { locale: currentLocale } = useLocale(); const dispatch = useDispatch(); diff --git a/src/screens/Onboarding/steps/welcome.tsx b/src/screens/Onboarding/steps/welcome.tsx index 817a5d2537..d6b63ffcfb 100644 --- a/src/screens/Onboarding/steps/welcome.tsx +++ b/src/screens/Onboarding/steps/welcome.tsx @@ -9,7 +9,6 @@ import { Linking } from "react-native"; import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { useDispatch } from "react-redux"; import Button from "../../../components/Button"; -import { useLocale } from "../../../context/Locale"; import { ScreenName } from "../../../const"; import StyledStatusBar from "../../../components/StyledStatusBar"; import { urls } from "../../../config/urls"; @@ -41,7 +40,9 @@ function OnboardingStepWelcome({ navigation }: any) { [navigation], ); - const { locale } = useLocale(); + const { + i18n: { language: locale }, + } = useTranslation(); const onTermsLink = useCallback( () => @@ -52,7 +53,8 @@ function OnboardingStepWelcome({ navigation }: any) { ); const onPrivacyLink = useCallback( - () => Linking.openURL(urls.privacyPolicy[locale]), + () => + Linking.openURL((urls.privacyPolicy as Record)[locale]), [locale], ); diff --git a/src/screens/PairDevices/Scanning.tsx b/src/screens/PairDevices/Scanning.tsx index c69de42e95..e76f877b9b 100644 --- a/src/screens/PairDevices/Scanning.tsx +++ b/src/screens/PairDevices/Scanning.tsx @@ -34,7 +34,7 @@ export default function Scanning({ onTimeout, onError, onSelect }: Props) { const knownDevice = knownDevices.find(d => d.id === item.id); const deviceMeta = { deviceId: item.id, - deviceName: item.name, + deviceName: item.localName ?? item.name, wired: false, modelId: "nanoX" as DeviceModelId, }; diff --git a/src/screens/Settings/Debug/index.tsx b/src/screens/Settings/Debug/index.tsx index 4562545e38..d115aa6a19 100644 --- a/src/screens/Settings/Debug/index.tsx +++ b/src/screens/Settings/Debug/index.tsx @@ -25,6 +25,7 @@ import ToggleMockServiceStatusButton from "./ToggleMockStatusIncident"; import SettingsNavigationScrollView from "../SettingsNavigationScrollView"; import MockModeRow from "../General/MockModeRow"; import GenerateMockAccount from "./GenerateMockAccountsSelect"; +import OpenDebugEnv from "./OpenDebugEnv"; export function DebugMocks() { return ( @@ -44,6 +45,7 @@ export function DebugMocks() { + diff --git a/src/screens/Settings/General/ThemeSettingsRow.tsx b/src/screens/Settings/General/ThemeSettingsRow.tsx index aad281a172..1e7fbda8ea 100644 --- a/src/screens/Settings/General/ThemeSettingsRow.tsx +++ b/src/screens/Settings/General/ThemeSettingsRow.tsx @@ -43,7 +43,7 @@ export default function ThemeSettingsRow() { onClose={onClose} title={t("settings.display.theme")} > - {(["light", "dark"] as Theme[]).map((theme, i) => ( + {(["system", "light", "dark"] as Theme[]).map((theme, i) => (