diff --git a/.changeset/good-students-drop.md b/.changeset/good-students-drop.md new file mode 100644 index 000000000000..a17cb4a26e1e --- /dev/null +++ b/.changeset/good-students-drop.md @@ -0,0 +1,6 @@ +--- +"live-mobile": patch +"@ledgerhq/native-ui": patch +--- + +Add the show qr code implementation for WS flow. Create tabSelector in RN UI Lib diff --git a/apps/ledger-live-mobile/.unimportedrc.json b/apps/ledger-live-mobile/.unimportedrc.json index 96637af194d8..c03bf0fe17cf 100644 --- a/apps/ledger-live-mobile/.unimportedrc.json +++ b/apps/ledger-live-mobile/.unimportedrc.json @@ -17,7 +17,9 @@ "src/contentCards/cards/vertical/*", "src/**/__integrations__/*.tsx", "src/MobileStorageProvider.ts", - "src/newArch/components/Dummy/*.tsx" + "src/newArch/features/WalletSync/components/Error/index.tsx", + "src/newArch/features/WalletSync/screens/Synchronize/PinCodeDisplay.tsx", + "src/newArch/features/WalletSync/screens/Synchronize/PinCodeInput.tsx" ], "ignoreUnused": [ "@react-native-masked-view/masked-view", diff --git a/apps/ledger-live-mobile/jest.config.js b/apps/ledger-live-mobile/jest.config.js index 18281ec804c1..afa2c7e78d6c 100644 --- a/apps/ledger-live-mobile/jest.config.js +++ b/apps/ledger-live-mobile/jest.config.js @@ -18,6 +18,7 @@ const transformIncludePatterns = [ "react-native-ble-plx", "react-native-android-location-services-dialog-box", "react-native-vector-icons", + "react-native-qrcode-svg", ]; /** @type {import('ts-jest').JestConfigWithTsJest} */ diff --git a/apps/ledger-live-mobile/src/images/bigSquareLogo.png b/apps/ledger-live-mobile/src/images/bigSquareLogo.png new file mode 100644 index 000000000000..69b95e8872a0 Binary files /dev/null and b/apps/ledger-live-mobile/src/images/bigSquareLogo.png differ diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index ec5eefb43c03..cc23f5f0917d 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -6792,6 +6792,31 @@ "connectDevice": { "title": "Use your Ledger" } + }, + "qrCode": { + "show": { + "title": "Show QR", + "explanation": { + "title": "Scan and synchronize your accounts using another Ledger Live app", + "steps": { + "step1": "Open the Ledger Live app you want to sync", + "step2": "Go to <0>Settings <1>> <0>General <1>> <0>Ledger Sync <1>> <0>Synchronize", + "step3": "Scan QR code until loader hits 100%." + } + } + }, + "scan": { + "title": "Scan" + }, + "pinCode": { + "title": "Enter your code", + "desc": "Type the code displayed on the Ledger Live you want to sync with.", + "error": { + "title": "Codes do not match", + "desc": "Make sure the code you type is the one displayed on the other Ledger Live instance.", + "tryAgain": "Try again" + } + } } } }, diff --git a/apps/ledger-live-mobile/src/newArch/components/Dummy/Drawer.tsx b/apps/ledger-live-mobile/src/newArch/components/Dummy/Drawer.tsx deleted file mode 100644 index 5809a0502d92..000000000000 --- a/apps/ledger-live-mobile/src/newArch/components/Dummy/Drawer.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import QueuedDrawer from "LLM/components/QueuedDrawer"; -import { Text } from "@ledgerhq/native-ui"; - -type Props = { - isOpen: boolean; - handleClose: () => void; -}; - -const Drawer = ({ isOpen, handleClose }: Props) => { - return ( - - {"Dummy Drawer"} - - ); -}; - -export default Drawer; diff --git a/apps/ledger-live-mobile/src/newArch/components/QueuedDrawer/index.tsx b/apps/ledger-live-mobile/src/newArch/components/QueuedDrawer/index.tsx index ea9c7a153cae..bc100759a8c3 100644 --- a/apps/ledger-live-mobile/src/newArch/components/QueuedDrawer/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/components/QueuedDrawer/index.tsx @@ -63,6 +63,8 @@ const QueuedDrawer = ({ isRequestingToBeOpened = false, isForcingToBeOpened = false, onClose, + onBack, + hasBackButton, onModalHide, noCloseButton, preventBackdropClick, @@ -150,6 +152,8 @@ const QueuedDrawer = ({ preventBackdropClick={areDrawersLocked || preventBackdropClick} onClose={handleCloseUserEvent} onModalHide={handleModalHide} + onBack={onBack} + hasBackButton={hasBackButton} noCloseButton={areDrawersLocked || noCloseButton} modalStyle={style} containerStyle={containerStyle} diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/addAccount.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/addAccount.integration.test.tsx index 5a304672f2a5..eae148e6b200 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/addAccount.integration.test.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/addAccount.integration.test.tsx @@ -95,6 +95,7 @@ describe("AddAccount", () => { await user.press(await screen.getByText(/import via another ledger live app/i)); }); await expect(await screen.findByText(/choose your sync method/i)).toBeVisible(); + await expect(await screen.findByText(/Scan a QR code/i)); }); /**====== Import from desktop Test =======*/ diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/shared.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/shared.tsx index 84154c2cd779..f1bb8dafc7a0 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/shared.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/__integrations__/shared.tsx @@ -14,7 +14,6 @@ const MockComponent = () => { const openAddModal = () => setAddModalOpened(true); const closeAddModal = () => setAddModalOpened(false); - const reopenAddModal = () => setAddModalOpened(true); return ( <> @@ -30,11 +29,7 @@ const MockComponent = () => { > {t("portfolio.emptyState.buttons.import")} - + ); }; diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx new file mode 100644 index 000000000000..86e57a449f6b --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx @@ -0,0 +1,82 @@ +import React, { useCallback, useEffect, useState } from "react"; +import SelectAddAccountMethod from "./SelectAddAccountMethod"; +import ChooseSyncMethod from "LLM/features/WalletSync/screens/Synchronize/ChooseMethod"; +import QrCodeMethod from "LLM/features/WalletSync/screens/Synchronize/QrCodeMethod"; +import { TrackScreen } from "~/analytics"; +import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { Steps } from "../../../types/enum/addAccount"; +import { AnalyticsPage } from "LLM/features/WalletSync/hooks/useLedgerSyncAnalytics"; + +type Props = { + startingStep: Steps; + currency?: CryptoCurrency | TokenCurrency | null; + doesNotHaveAccount?: boolean; + onStepChange?: (step: Steps) => void; + onGoBack?: (callback: () => void) => void; +}; + +const StepFlow = ({ + startingStep, + doesNotHaveAccount, + currency, + onGoBack, + onStepChange, +}: Props) => { + const [currentStep, setCurrentStep] = useState(startingStep); + + useEffect(() => { + if (onStepChange) onStepChange(currentStep); + }, [currentStep, onStepChange]); + + const navigateToChooseSyncMethod = () => setCurrentStep(Steps.ChooseSyncMethod); + const navigateToQrCodeMethod = () => setCurrentStep(Steps.QrCodeMethod); + + const getPreviousStep = useCallback( + (step: Steps): Steps => { + switch (step) { + case Steps.QrCodeMethod: + return Steps.ChooseSyncMethod; + case Steps.ChooseSyncMethod: + return Steps.AddAccountMethod; + default: + return startingStep; + } + }, + [startingStep], + ); + + useEffect(() => { + if (onGoBack) onGoBack(() => setCurrentStep(prevStep => getPreviousStep(prevStep))); + }, [getPreviousStep, onGoBack]); + + const getScene = () => { + switch (currentStep) { + case Steps.AddAccountMethod: + return ( + <> + + + + ); + case Steps.ChooseSyncMethod: + return ( + <> + + + + ); + case Steps.QrCodeMethod: + return ; + default: + return null; + } + }; + + return getScene(); +}; + +export default StepFlow; diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/index.tsx index e3052d4e88e2..e4b61c4aec18 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/index.tsx @@ -1,60 +1,55 @@ -import React from "react"; +import React, { useState } from "react"; import useAddAccountViewModel from "./useAddAccountViewModel"; import QueuedDrawer from "~/components/QueuedDrawer"; -import { TrackScreen } from "~/analytics"; -import SelectAddAccountMethod from "./components/SelectAddAccountMethod"; import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; -import ChooseSyncMethod from "LLM/features/WalletSync/screens/Synchronize/ChooseMethod"; +import DrawerHeader from "LLM/features/WalletSync/components/Synchronize/DrawerHeader"; +import { Flex } from "@ledgerhq/native-ui"; +import StepFlow from "./components/StepFlow"; +import { Steps } from "../../types/enum/addAccount"; -type ViewProps = { - isAddAccountDrawerVisible: boolean; - doesNotHaveAccount?: boolean; - currency?: CryptoCurrency | TokenCurrency | null; - isWalletSyncDrawerVisible: boolean; - onCloseAddAccountDrawer: () => void; - reopenDrawer: () => void; - onRequestToOpenWalletSyncDrawer: () => void; - onCloseWalletSyncDrawer: () => void; -}; +type ViewProps = ReturnType & AddAccountProps; type AddAccountProps = { isOpened: boolean; currency?: CryptoCurrency | TokenCurrency | null; doesNotHaveAccount?: boolean; onClose: () => void; - reopenDrawer: () => void; }; +const StartingStep = Steps.AddAccountMethod; + function View({ isAddAccountDrawerVisible, doesNotHaveAccount, currency, - isWalletSyncDrawerVisible, onCloseAddAccountDrawer, - onRequestToOpenWalletSyncDrawer, - onCloseWalletSyncDrawer, }: ViewProps) { + const [currentStep, setCurrentStep] = useState(StartingStep); + + const CustomDrawerHeader = () => ; + + const handleStepChange = (step: Steps) => setCurrentStep(step); + + let goBackCallback: () => void; + return ( - <> - - - goBackCallback()} + > + + (goBackCallback = callback)} /> - - - - - + + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/useAddAccountViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/useAddAccountViewModel.ts index c5702e01b3c8..b5c2dcad9417 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/useAddAccountViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/useAddAccountViewModel.ts @@ -1,17 +1,12 @@ -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { track } from "~/analytics"; -import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; type AddAccountDrawerProps = { isOpened: boolean; - currency?: CryptoCurrency | TokenCurrency | null; onClose: () => void; - reopenDrawer: () => void; }; -const useAddAccountViewModel = ({ isOpened, onClose, reopenDrawer }: AddAccountDrawerProps) => { - const [isWalletSyncDrawerVisible, setWalletSyncDrawerVisible] = useState(false); - +const useAddAccountViewModel = ({ isOpened, onClose }: AddAccountDrawerProps) => { const trackButtonClick = useCallback((button: string) => { track("button_clicked", { button, @@ -24,22 +19,9 @@ const useAddAccountViewModel = ({ isOpened, onClose, reopenDrawer }: AddAccountD onClose(); }, [trackButtonClick, onClose]); - const onCloseWalletSyncDrawer = () => { - setWalletSyncDrawerVisible(false); - reopenDrawer(); - }; - - const onRequestToOpenWalletSyncDrawer = () => { - onCloseAddAccountDrawer(); - setWalletSyncDrawerVisible(true); - }; - return { isAddAccountDrawerVisible: isOpened, - isWalletSyncDrawerVisible, onCloseAddAccountDrawer, - onCloseWalletSyncDrawer, - onRequestToOpenWalletSyncDrawer, }; }; diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/types/enum/addAccount.ts b/apps/ledger-live-mobile/src/newArch/features/Accounts/types/enum/addAccount.ts new file mode 100644 index 000000000000..45b2587c4af4 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/types/enum/addAccount.ts @@ -0,0 +1,5 @@ +export enum Steps { + AddAccountMethod = "AddAccountMethod", + ChooseSyncMethod = "ChooseSyncMethod", + QrCodeMethod = "QrCodeMethod", +} diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/walletSyncSettings.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/walletSyncSettings.integration.test.tsx index 16b6d080ee28..4a6f8b27a710 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/walletSyncSettings.integration.test.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/walletSyncSettings.integration.test.tsx @@ -5,40 +5,64 @@ import { WalletSyncSettingsNavigator } from "./shared"; import { State } from "~/reducers/types"; describe("WalletSyncSettings", () => { - it("Should open wallet sync activation flow page from settings", async () => { - const { user } = render(, { - overrideInitialState: (state: State) => ({ - ...state, - settings: { - ...state.settings, - readOnlyModeEnabled: false, - overriddenFeatureFlags: { - llmWalletSync: { - enabled: true, - params: { - environment: "STAGING", - watchConfig: {}, - }, - }, + const initialState = (state: State) => ({ + ...state, + settings: { + ...state.settings, + readOnlyModeEnabled: false, + overriddenFeatureFlags: { + llmWalletSync: { + enabled: true, + params: { + environment: "STAGING", + watchConfig: {}, }, }, - }), - }); + }, + }, + }); - // Check if the ledger sync row is visible + it("Should display the ledger sync row", async () => { + render(, { overrideInitialState: initialState }); await expect(await screen.findByText(/ledger sync/i)).toBeVisible(); + }); - // On Press the ledger sync row + it("Should open the activation drawer when ledger sync row is pressed", async () => { + const { user } = render(, { + overrideInitialState: initialState, + }); await user.press(await screen.findByText(/ledger sync/i)); - - // Check if the activation screen is visible await expect(await screen.findByText(/sync your accounts across all platforms/i)).toBeVisible(); await expect(await screen.findByText(/already created a key?/i)).toBeVisible(); + }); + + it("Should open the drawer when 'already created a key' button is pressed", async () => { + const { user } = render(, { + overrideInitialState: initialState, + }); + await user.press(await screen.findByText(/ledger sync/i)); + await user.press(await screen.findByText(/already created a key?/i)); + await expect(await screen.findByText(/choose your sync method/i)).toBeVisible(); + }); - // On Press the already created a key link + it("Should open the QR code scene when 'scan a qr code' toggle is pressed", async () => { + const { user } = render(, { + overrideInitialState: initialState, + }); + await user.press(await screen.findByText(/ledger sync/i)); await user.press(await screen.findByText(/already created a key?/i)); + await user.press(await screen.findByText(/scan a qr code/i)); + await expect(await screen.findByText(/show qr/i)).toBeVisible(); + }); - // Check if the drawer is visible - await expect(await screen.findByText(/Choose your sync method/i)).toBeVisible(); + it("Should display the QR code when 'show qr' toggle is pressed", async () => { + const { user } = render(, { + overrideInitialState: initialState, + }); + await user.press(await screen.findByText(/ledger sync/i)); + await user.press(await screen.findByText(/already created a key?/i)); + await user.press(await screen.findByText(/scan a qr code/i)); + await user.press(await screen.findByText(/show qr/i)); + await expect(await screen.getByTestId("ws-show-qr-code")).toBeVisible(); }); }); diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/Actions.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/Actions.tsx index aaa2773b9122..d53968aad461 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/Actions.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/Actions.tsx @@ -2,11 +2,11 @@ import React from "react"; import { Flex, Button, Link } from "@ledgerhq/native-ui"; import { useTranslation } from "react-i18next"; import { - useWalletSyncAnalytics, + useLedgerSyncAnalytics, AnalyticsButton, AnalyticsPage, AnalyticsFlow, -} from "LLM/features/WalletSync/hooks/useWalletSyncAnalytics"; +} from "LLM/features/WalletSync/hooks/useLedgerSyncAnalytics"; type Props = { onPressSyncAccounts: () => void; @@ -15,13 +15,13 @@ type Props = { const Actions = ({ onPressSyncAccounts, onPressHasAlreadyCreatedAKey }: Props) => { const { t } = useTranslation(); - const { onClickTrack } = useWalletSyncAnalytics(); + const { onClickTrack } = useLedgerSyncAnalytics(); const onPressSync = () => { onClickTrack({ button: AnalyticsButton.SyncYourAccounts, - page: AnalyticsPage.ActivateWalletSync, - flow: AnalyticsFlow.WalletSync, + page: AnalyticsPage.ActivateLedgerSync, + flow: AnalyticsFlow.LedgerSync, }); onPressSyncAccounts(); }; @@ -29,8 +29,8 @@ const Actions = ({ onPressSyncAccounts, onPressHasAlreadyCreatedAKey }: Props) = const onPressHasAlreadyAKey = () => { onClickTrack({ button: AnalyticsButton.AlreadyCreatedKey, - page: AnalyticsPage.ActivateWalletSync, - flow: AnalyticsFlow.WalletSync, + page: AnalyticsPage.ActivateLedgerSync, + flow: AnalyticsFlow.LedgerSync, }); onPressHasAlreadyCreatedAKey(); }; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx new file mode 100644 index 000000000000..efa95edf3f4d --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import Activation from "."; +import { TrackScreen } from "~/analytics"; +import ChooseSyncMethod from "../../screens/Synchronize/ChooseMethod"; +import QrCodeMethod from "../../screens/Synchronize/QrCodeMethod"; +import { Steps } from "../../types/Activation"; +import { AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics"; + +type Props = { + currentStep: Steps; + navigateToChooseSyncMethod: () => void; + navigateToQrCodeMethod: () => void; +}; + +const ActivationFlow = ({ + currentStep, + navigateToChooseSyncMethod, + navigateToQrCodeMethod, +}: Props) => { + const getScene = () => { + switch (currentStep) { + case Steps.Activation: + return ( + <> + + + + ); + case Steps.ChooseSyncMethod: + return ( + <> + + + + ); + case Steps.QrCodeMethod: + return ; + default: + return null; + } + }; + + return getScene(); +}; + +export default ActivationFlow; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/index.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/index.tsx index 626acbb014ba..11b1d8641329 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/index.tsx @@ -4,36 +4,22 @@ import IconsHeader from "./IconsHeader"; import { Flex, Text } from "@ledgerhq/native-ui"; import { useTranslation } from "react-i18next"; import { useTheme } from "styled-components/native"; -import { TrackScreen } from "~/analytics"; import { useInitMemberCredentials } from "../../hooks/useInitMemberCredentials"; -import QueuedDrawer from "~/components/QueuedDrawer"; -import ChooseSyncMethod from "../../screens/Synchronize/ChooseMethod"; -type Props = T extends true - ? { isInsideDrawer: T; openSyncMethodDrawer: () => void } - : { isInsideDrawer?: T; openSyncMethodDrawer?: undefined }; +type Props = { onSyncMethodPress: () => void }; -const Activation: React.FC> = ({ isInsideDrawer, openSyncMethodDrawer }) => { +const Activation: React.FC = ({ onSyncMethodPress }) => { const { colors } = useTheme(); const { t } = useTranslation(); - useInitMemberCredentials(); - const [isChooseMethodDrawerOpen, setIsChooseMethodDrawerOpen] = React.useState(false); - const onPressSyncAccounts = () => { - isInsideDrawer ? openSyncMethodDrawer() : setIsChooseMethodDrawerOpen(true); - }; + useInitMemberCredentials(); - const onPressHasAlreadyCreatedAKey = () => { - isInsideDrawer ? openSyncMethodDrawer() : setIsChooseMethodDrawerOpen(true); - }; + const onPressSyncAccounts = () => onSyncMethodPress(); - const onPressCloseDrawer = () => { - setIsChooseMethodDrawerOpen(false); - }; + const onPressHasAlreadyCreatedAKey = () => onSyncMethodPress(); return ( - @@ -54,14 +40,6 @@ const Activation: React.FC> = ({ isInsideDrawer, openSyncMethodDr onPressHasAlreadyCreatedAKey={onPressHasAlreadyCreatedAKey} onPressSyncAccounts={onPressSyncAccounts} /> - {!isInsideDrawer && ( - - - - )} ); }; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Error/index.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Error/index.tsx new file mode 100644 index 000000000000..57d6e188870c --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Error/index.tsx @@ -0,0 +1,45 @@ +import { Box, Button, Flex, Icons, Text } from "@ledgerhq/native-ui"; +import React from "react"; +import styled, { useTheme } from "styled-components/native"; +type Props = { + title: string; + desc: string; + mainButton: { + label: string; + onPress: () => void; + }; +}; + +export function ErrorComponent({ title, desc, mainButton }: Props) { + const { colors } = useTheme(); + return ( + + + + + + + {title} + + + {desc} + + + + + + + ); +} + +const Container = styled(Box)` + background-color: ${p => p.theme.colors.opacityDefault.c05}; + + height: 72px; + width: 72px; + display: flex; + align-items: center; + justify-content: center; +`; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/ManageInstances/DeletionError.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/ManageInstances/DeletionError.tsx index cbba027bd789..3c8b47f18198 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/ManageInstances/DeletionError.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/ManageInstances/DeletionError.tsx @@ -2,10 +2,10 @@ import React from "react"; import { Box, Icons, Flex, Text, Button, Link } from "@ledgerhq/native-ui"; import { useTranslation } from "react-i18next"; import { - useWalletSyncAnalytics, + useLedgerSyncAnalytics, AnalyticsButton, AnalyticsPage, -} from "../../hooks/useWalletSyncAnalytics"; +} from "../../hooks/useLedgerSyncAnalytics"; import styled, { useTheme } from "styled-components/native"; import TrackScreen from "~/analytics/TrackScreen"; @@ -32,7 +32,7 @@ type Props = { }; export const DeletionError = ({ error, tryAgain, goToDelete, understood }: Props) => { - const { onClickTrack } = useWalletSyncAnalytics(); + const { onClickTrack } = useLedgerSyncAnalytics(); const onTryAgain = () => { tryAgain?.(); diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/DrawerHeader.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/DrawerHeader.tsx new file mode 100644 index 000000000000..07b57e5b264d --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/DrawerHeader.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Flex, Icons, Text } from "@ledgerhq/native-ui"; +import { useTheme } from "styled-components/native"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity } from "react-native"; + +type Props = { + onClose: () => void; +}; + +const DrawerHeader: React.FC = ({ onClose }) => { + const { colors } = useTheme(); + const { t } = useTranslation(); + + return ( + + + + {t("walletSync.walletSyncActivated.synchronize.title")} + + + + + + + + + + ); +}; + +export default DrawerHeader; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx new file mode 100644 index 000000000000..a35fc9036c86 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { Flex, Text, NumberedList, ScrollContainer } from "@ledgerhq/native-ui"; +import styled, { useTheme } from "styled-components/native"; +import QRCode from "react-native-qrcode-svg"; +import getWindowDimensions from "~/logic/getWindowDimensions"; +import { Trans, useTranslation } from "react-i18next"; + +const Italic = styled(Text)` + font-style: italic; +`; +// Won't work since we don't have inter italic font + +type Props = { + qrCodeValue: string; +}; + +const QrCode = ({ qrCodeValue }: Props) => { + const { colors } = useTheme(); + const { width } = getWindowDimensions(); + const { t } = useTranslation(); + + const backgroundBorderRadius = 23; + const backgroundPadding = 15.36; + const backgroundSize = 280; + const distanceBetweenQRCodeAndScreenBorder = 48; + + const QRSize = Math.round(width - distanceBetweenQRCodeAndScreenBorder); + const maxQRCodeSize = backgroundSize - backgroundPadding * 2; + const QRCodeSize = Math.min(QRSize - backgroundPadding, maxQRCodeSize); + + const steps = [ + { + description: ( + + {t("walletSync.synchronize.qrCode.show.explanation.steps.step1")} + + ), + }, + { + description: ( + + , + , + ]} + /> + + ), + }, + { + description: ( + + {t("walletSync.synchronize.qrCode.show.explanation.steps.step3")} + + ), + }, + ]; + + return ( + + + + + + + {t("walletSync.synchronize.qrCode.show.explanation.title")} + + + + + ); +}; + +export default QrCode; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts similarity index 85% rename from apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts rename to apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts index 61a1f719a0ca..5f84a9deeea8 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts @@ -1,10 +1,11 @@ import { track } from "~/analytics"; export enum AnalyticsPage { - ActivateWalletSync = "Activate Wallet Sync", + ActivateLedgerSync = "Activate Ledger Sync", ChooseSyncMethod = "Choose sync method", BackupCreationSuccess = "Backup creation success", ScanQRCode = "Scan QR code", + ShowQRCode = "Show QR code", SyncWithQrCode = "Sync with QR code", PinCode = "Pin code", PinCodesDoNotMatch = "Pin codes don't match", @@ -15,12 +16,12 @@ export enum AnalyticsPage { ManageBackup = "Manage backup", ConfirmDeleteBackup = "Confirm delete backup", SyncWithNoKey = "Sync with no key", - WalletSyncActivated = "Wallet Sync activated", + LedgerSyncActivated = "Ledger Sync activated", AutoRemove = "Remove current instance", } export enum AnalyticsFlow { - WalletSync = "Wallet Sync", + LedgerSync = "Ledger Sync", } export enum AnalyticsButton { @@ -52,8 +53,8 @@ type OnClickTrack = { flow?: (typeof AnalyticsFlow)[keyof typeof AnalyticsFlow]; }; -export function useWalletSyncAnalytics() { - const onClickTrack = ({ button, page, flow }: OnClickTrack) => { +export function useLedgerSyncAnalytics() { + const onClickTrack = ({ button, page, flow = AnalyticsFlow.LedgerSync }: OnClickTrack) => { track("button_clicked", { button, page, flow }); }; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationDrawer.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationDrawer.tsx index b779777e00eb..f65830b54c74 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationDrawer.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationDrawer.tsx @@ -1,40 +1,60 @@ import React from "react"; import QueuedDrawer from "LLM/components/QueuedDrawer"; -import Activation from "../../components/Activation"; import { TrackScreen } from "~/analytics"; -import ChooseSyncMethod from "../Synchronize/ChooseMethod"; +import { useWindowDimensions } from "react-native"; +import { Flex } from "@ledgerhq/native-ui"; +import ActivationFlow from "../../components/Activation/ActivationFlow"; +import { Steps } from "../../types/Activation"; +import DrawerHeader from "../../components/Synchronize/DrawerHeader"; +import useActivationDrawerModel from "./useActivationDrawerModel"; + +type ViewProps = ReturnType; type Props = { isOpen: boolean; - reopenDrawer: () => void; + startingStep: Steps; handleClose: () => void; }; -const ActivationDrawer = ({ isOpen, handleClose, reopenDrawer }: Props) => { - const [isSyncMethodDrawerOpen, setIsSyncMethodDrawerOpen] = React.useState(false); - - const onPressCloseDrawer = () => { - setIsSyncMethodDrawerOpen(false); - reopenDrawer(); - }; - - const openSyncMethodDrawer = () => { - setIsSyncMethodDrawerOpen(true); - handleClose(); - }; +function View({ + isOpen, + currentStep, + hasCustomHeader, + canGoBack, + navigateToChooseSyncMethod, + navigateToQrCodeMethod, + goBackToPreviousStep, + handleClose, + onCloseDrawer, +}: ViewProps) { + const { height } = useWindowDimensions(); + const maxDrawerHeight = height - 180; + const CustomDrawerHeader = () => ; return ( <> - - - - - - + + + + ); +} + +const ActivationDrawer = (props: Props) => { + return ; }; export default ActivationDrawer; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/index.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/index.tsx index 9ba0dc1ce683..76fcd55b4933 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/index.tsx @@ -1,8 +1,15 @@ import React from "react"; import SafeAreaView from "~/components/SafeAreaView"; import Activation from "../../components/Activation"; +import ActivationDrawer from "./ActivationDrawer"; +import { Steps } from "../../types/Activation"; function View() { + const [showDrawer, setShowDrawer] = React.useState(false); + + const onOpenDrawer = () => setShowDrawer(true); + const onCloseDrawer = () => setShowDrawer(false); + return ( - + + + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/useActivationDrawerModel.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/useActivationDrawerModel.ts new file mode 100644 index 000000000000..408856bf9941 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/useActivationDrawerModel.ts @@ -0,0 +1,67 @@ +import { useCallback, useState } from "react"; +import { Steps } from "../../types/Activation"; +import { + AnalyticsButton, + AnalyticsPage, + useLedgerSyncAnalytics, +} from "../../hooks/useLedgerSyncAnalytics"; + +type Props = { + isOpen: boolean; + startingStep: Steps; + handleClose: () => void; +}; + +const useActivationDrawerModel = ({ isOpen, startingStep, handleClose }: Props) => { + const { onClickTrack } = useLedgerSyncAnalytics(); + const [currentStep, setCurrentStep] = useState(startingStep); + + const hasCustomHeader = currentStep === Steps.QrCodeMethod; + const canGoBack = currentStep === Steps.ChooseSyncMethod && startingStep === Steps.Activation; + + const getPreviousStep = useCallback( + (step: Steps): Steps => { + switch (step) { + case Steps.ChooseSyncMethod: + return Steps.Activation; + case Steps.QrCodeMethod: + return Steps.ChooseSyncMethod; + default: + return startingStep; + } + }, + [startingStep], + ); + + const navigateToChooseSyncMethod = () => setCurrentStep(Steps.ChooseSyncMethod); + + const navigateToQrCodeMethod = () => { + onClickTrack({ + button: AnalyticsButton.ScanQRCode, + page: AnalyticsPage.ChooseSyncMethod, + }); + setCurrentStep(Steps.QrCodeMethod); + }; + + const resetStep = () => setCurrentStep(startingStep); + const goBackToPreviousStep = () => setCurrentStep(getPreviousStep(currentStep)); + + const onCloseDrawer = () => { + resetStep(); + handleClose(); + }; + + return { + isOpen, + currentStep, + hasCustomHeader, + canGoBack, + navigateToChooseSyncMethod, + navigateToQrCodeMethod, + onCloseDrawer, + handleClose, + goBackToPreviousStep, + }; +}; + +export default useActivationDrawerModel; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Manage/index.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Manage/index.tsx index 5ab335723745..e41f5a953dbf 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Manage/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Manage/index.tsx @@ -1,13 +1,13 @@ import { Box, Flex, Text, Icons, InfiniteLoader, Alert } from "@ledgerhq/native-ui"; -import React, { useCallback } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Option, OptionProps } from "./Option"; import styled from "styled-components"; import { AnalyticsButton, AnalyticsPage, - useWalletSyncAnalytics, -} from "../../hooks/useWalletSyncAnalytics"; + useLedgerSyncAnalytics, +} from "../../hooks/useLedgerSyncAnalytics"; import { Separator } from "../../components/Separator"; import { TouchableOpacity } from "react-native"; import { TrustchainNotFound } from "../../hooks/useGetMembers"; @@ -15,6 +15,8 @@ import ManageKeyDrawer from "../ManageKey/ManageKeyDrawer"; import { useManageKeyDrawer } from "../ManageKey/useManageKeyDrawer"; import ManageInstanceDrawer from "../ManageInstances/ManageInstancesDrawer"; import { useManageInstancesDrawer } from "../ManageInstances/useManageInstanceDrawer"; +import ActivationDrawer from "../Activation/ActivationDrawer"; +import { Steps } from "../../types/Activation"; const WalletSyncManage = () => { const { t } = useTranslation(); @@ -24,24 +26,27 @@ const WalletSyncManage = () => { const { data, isLoading, isError, error } = manageInstancesHook.memberHook; - const { onClickTrack } = useWalletSyncAnalytics(); + const { onClickTrack } = useLedgerSyncAnalytics(); - const goToManageBackup = useCallback(() => { - manageKeyHook.openDrawer(); - onClickTrack({ button: AnalyticsButton.ManageKey, page: AnalyticsPage.WalletSyncActivated }); - }, [manageKeyHook, onClickTrack]); + const [isSyncDrawerOpen, setIsSyncDrawerOpen] = useState(false); const goToSync = () => { - //dispatch(setFlow({ flow: Flow.Synchronize, step: Step.SynchronizeMode })); + setIsSyncDrawerOpen(true); + onClickTrack({ button: AnalyticsButton.Synchronize, page: AnalyticsPage.LedgerSyncActivated }); + }; - onClickTrack({ button: AnalyticsButton.Synchronize, page: AnalyticsPage.WalletSyncActivated }); + const closeSyncDrawer = () => setIsSyncDrawerOpen(false); + + const goToManageBackup = () => { + manageKeyHook.openDrawer(); + onClickTrack({ button: AnalyticsButton.ManageKey, page: AnalyticsPage.LedgerSyncActivated }); }; const goToManageInstances = () => { manageInstancesHook.openDrawer(); onClickTrack({ button: AnalyticsButton.ManageSynchronizations, - page: AnalyticsPage.WalletSyncActivated, + page: AnalyticsPage.LedgerSyncActivated, }); }; @@ -117,9 +122,11 @@ const WalletSyncManage = () => { - {/** - * DRAWERS - */} + diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ChooseMethod.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ChooseMethod.tsx index 94651bbbd808..0c91472036f9 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ChooseMethod.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ChooseMethod.tsx @@ -6,21 +6,31 @@ import { useNavigation } from "@react-navigation/native"; import { NavigatorName, ScreenName } from "~/const"; import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; import { WalletSyncNavigatorStackParamList } from "~/components/RootNavigator/types/WalletSyncNavigator"; +import { + useLedgerSyncAnalytics, + AnalyticsButton, + AnalyticsPage, +} from "../../hooks/useLedgerSyncAnalytics"; type NavigationProps = BaseComposite< StackNavigatorProps >; -const ChooseSyncMethod = () => { +type Props = { + onScanMethodPress: () => void; +}; + +const ChooseSyncMethod = ({ onScanMethodPress }: Props) => { const { t } = useTranslation(); const navigation = useNavigation(); + const { onClickTrack } = useLedgerSyncAnalytics(); const onConnectDeviceMethodPress = () => { + onClickTrack({ button: AnalyticsButton.UseYourLedger, page: AnalyticsPage.ChooseSyncMethod }); navigation.navigate(NavigatorName.WalletSync, { screen: ScreenName.WalletSyncActivationProcess, }); }; - const onScanMethodPress = () => {}; return ( diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/PinCodeDisplay.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/PinCodeDisplay.tsx new file mode 100644 index 000000000000..5cf1bb0f86f7 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/PinCodeDisplay.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Flex, Text } from "@ledgerhq/native-ui"; +import styled, { useTheme } from "styled-components/native"; +import { AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics"; +import TrackScreen from "~/analytics/TrackScreen"; + +type Props = { + pinCode: string; +}; + +export default function PinCodeDisplay({ pinCode }: Props) { + const { t } = useTranslation(); + const { colors } = useTheme(); + + return ( + + + + {t("walletSync.synchronize.qrCode.pinCode.title")} + + + + {t("walletSync.synchronize.qrCode.pinCode.desc")} + + + + {pinCode?.split("").map((digit, index) => ( + + + {digit} + + + ))} + + + ); +} + +const NumberContainer = styled(Flex)` + border-radius: 8px; + height: 50px; + width: 50px; +`; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/PinCodeInput.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/PinCodeInput.tsx new file mode 100644 index 000000000000..7f5ad7f9facc --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/PinCodeInput.tsx @@ -0,0 +1,109 @@ +import React, { forwardRef, useImperativeHandle, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Flex, Text } from "@ledgerhq/native-ui"; +import styled from "styled-components/native"; +import { AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics"; +import TrackScreen from "~/analytics/TrackScreen"; +import { NativeSyntheticEvent, TextInput, TextInputKeyPressEventData } from "react-native"; + +export default function PinCodeInput() { + const { t } = useTranslation(); + const inputRefs = [useRef(null), useRef(null), useRef(null)]; + const [digits, setDigits] = useState(["", "", ""]); + + const handleChange = (value: string, index: number) => { + const newDigits = [...digits]; + newDigits[index] = value; + setDigits(newDigits); + if (value && index < digits.length - 1) { + inputRefs[index + 1].current?.focus(); + } + }; + + const handleKeyPress = (e: NativeSyntheticEvent, index: number) => { + if (e.nativeEvent.key === "Backspace") { + if (!digits[index] && index > 0) { + inputRefs[index - 1].current?.focus(); + } else { + const newDigits = [...digits]; + newDigits[index] = ""; + setDigits(newDigits); + } + } + }; + + return ( + + + + {t("walletSync.synchronize.qrCode.pinCode.title")} + + + {t("walletSync.synchronize.qrCode.pinCode.desc")} + + + {digits.map((digit, index) => ( + handleChange(value, index)} + onKeyPress={e => handleKeyPress(e, index)} + index={index} + ref={inputRefs[index]} + /> + ))} + + + ); +} + +interface DigitInputProps { + value: string; + onChange: (value: string) => void; + onKeyPress: (e: NativeSyntheticEvent, index: number) => void; + index: number; +} + +interface TextInputRef { + focus: () => void; +} + +const DigitInput = forwardRef( + ({ value, onChange, onKeyPress, index }, forwardedRef) => { + const [isFocused, setIsFocused] = useState(false); + const inputRef = useRef(null); + + useImperativeHandle(forwardedRef, () => ({ + focus: () => inputRef.current?.focus(), + })); + + const handleChange = (text: string) => { + if (text.length <= 1 && /^\d*$/.test(text)) { + onChange(text); + } + }; + + return ( + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + isFocused={isFocused} + onKeyPress={e => onKeyPress(e, index)} + textAlign="center" + /> + ); + }, +); + +const NumberContainer = styled(TextInput)<{ isFocused: boolean }>` + border-radius: 8px; + height: 50px; + width: 50px; + border: 1px solid + ${({ theme, isFocused }) => (isFocused ? theme.colors.primary.c80 : theme.colors.neutral.c40)}; +`; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx new file mode 100644 index 000000000000..40d64001a887 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import { Flex, TabSelector } from "@ledgerhq/native-ui"; +import QrCode from "LLM/features/WalletSync/components/Synchronize/QrCode"; +import { Options, OptionsType } from "LLM/features/WalletSync/types/Activation"; +import { useTranslation } from "react-i18next"; +import { + useLedgerSyncAnalytics, + AnalyticsPage, + AnalyticsButton, +} from "../../hooks/useLedgerSyncAnalytics"; +import { TrackScreen } from "~/analytics"; + +const QrCodeMethod = () => { + const [selectedOption, setSelectedOption] = useState(Options.SCAN); + const { onClickTrack } = useLedgerSyncAnalytics(); + const { t } = useTranslation(); + + const handleSelectOption = (option: OptionsType) => { + setSelectedOption(option); + const button = + option === Options.SCAN ? AnalyticsButton.ScanQRCode : AnalyticsButton.ShowQRCode; + onClickTrack({ + button, + page: AnalyticsPage.ScanQRCode, + }); + }; + + const renderSwitch = () => { + switch (selectedOption) { + case Options.SCAN: + return ( + <> + + + + ); + case Options.SHOW_QR: + return ( + <> + + + + ); + } + }; + + return ( + + + {renderSwitch()} + + ); +}; + +export default QrCodeMethod; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts new file mode 100644 index 000000000000..8b0f6fb7368c --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts @@ -0,0 +1,12 @@ +export enum Options { + SCAN = "scan", + SHOW_QR = "showQR", +} + +export type OptionsType = Options.SCAN | Options.SHOW_QR; + +export enum Steps { + Activation = "Activation", + ChooseSyncMethod = "ChooseSyncMethod", + QrCodeMethod = "QrCodeMethod", +} diff --git a/apps/ledger-live-mobile/src/screens/Accounts/AddAccount.tsx b/apps/ledger-live-mobile/src/screens/Accounts/AddAccount.tsx index a863673789e4..f088ef7105a6 100644 --- a/apps/ledger-live-mobile/src/screens/Accounts/AddAccount.tsx +++ b/apps/ledger-live-mobile/src/screens/Accounts/AddAccount.tsx @@ -23,10 +23,6 @@ function AddAccount({ currencyId }: { currencyId?: string }) { setIsAddModalOpened(false); } - function reopenAddModal() { - setIsAddModalOpened(true); - } - return ( <> @@ -42,12 +38,7 @@ function AddAccount({ currencyId }: { currencyId?: string }) { - + ); } diff --git a/apps/ledger-live-mobile/src/screens/Assets/index.tsx b/apps/ledger-live-mobile/src/screens/Assets/index.tsx index 09e57f0f6cf1..04968e83f926 100644 --- a/apps/ledger-live-mobile/src/screens/Assets/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Assets/index.tsx @@ -65,8 +65,6 @@ function Assets() { const closeAddModal = useCallback(() => setAddModalOpened(false), [setAddModalOpened]); - const reopenAddModal = useCallback(() => setAddModalOpened(true), [setAddModalOpened]); - const renderItem = useCallback( ({ item }: { item: Asset }) => ( @@ -118,11 +116,7 @@ function Assets() { /> - + ); diff --git a/apps/ledger-live-mobile/src/screens/Onboarding/steps/accessExistingWallet.tsx b/apps/ledger-live-mobile/src/screens/Onboarding/steps/accessExistingWallet.tsx index da4e3cdab234..5aaf642fbc34 100644 --- a/apps/ledger-live-mobile/src/screens/Onboarding/steps/accessExistingWallet.tsx +++ b/apps/ledger-live-mobile/src/screens/Onboarding/steps/accessExistingWallet.tsx @@ -15,6 +15,7 @@ import OnboardingView from "./OnboardingView"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { logDrawer } from "LLM/components/QueuedDrawer/utils/logDrawer"; import ActivationDrawer from "LLM/features/WalletSync/screens/Activation/ActivationDrawer"; +import { Steps } from "LLM/features/WalletSync/types/Activation"; type NavigationProps = StackNavigatorProps< OnboardingNavigatorParamList & BaseNavigatorStackParamList, @@ -55,11 +56,6 @@ function AccessExistingWallet() { logDrawer("Wallet Sync Welcome back", "close"); }, []); - const reopenDrawer = useCallback(() => { - setIsDrawerVisible(true); - logDrawer("Wallet Sync Welcome back", "reopen"); - }, []); - return ( + ); diff --git a/apps/ledger-live-mobile/src/screens/Portfolio/EmptyStatePortfolio.tsx b/apps/ledger-live-mobile/src/screens/Portfolio/EmptyStatePortfolio.tsx index 4bc717d5506e..ebb9b0f3b80d 100644 --- a/apps/ledger-live-mobile/src/screens/Portfolio/EmptyStatePortfolio.tsx +++ b/apps/ledger-live-mobile/src/screens/Portfolio/EmptyStatePortfolio.tsx @@ -33,10 +33,6 @@ function EmptyStatePortfolio({ showHelp = true }: Props) { const closeAddModal = useCallback(() => setAddModalOpened(false), [setAddModalOpened]); - const reopenAddModal = useCallback(() => { - setAddModalOpened(true); - }, [setAddModalOpened]); - const navigateToManager = useCallback(() => { navigation.navigate(NavigatorName.MyLedger); }, [navigation]); @@ -100,7 +96,7 @@ function EmptyStatePortfolio({ showHelp = true }: Props) { diff --git a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx index aba3472b105f..38d72b130a02 100644 --- a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx @@ -127,7 +127,6 @@ function PortfolioScreen({ navigation }: NavigationProps) { }, [setAddModalOpened]); const closeAddModal = useCallback(() => setAddModalOpened(false), [setAddModalOpened]); - const reopenAddModal = useCallback(() => setAddModalOpened(true), [setAddModalOpened]); const refreshAccountsOrdering = useRefreshAccountsOrdering(); useFocusEffect(refreshAccountsOrdering); @@ -227,7 +226,6 @@ function PortfolioScreen({ navigation }: NavigationProps) { diff --git a/apps/ledger-live-mobile/src/screens/Settings/General/WalletSyncRow.tsx b/apps/ledger-live-mobile/src/screens/Settings/General/WalletSyncRow.tsx index c78861279a63..d7ae699d017c 100644 --- a/apps/ledger-live-mobile/src/screens/Settings/General/WalletSyncRow.tsx +++ b/apps/ledger-live-mobile/src/screens/Settings/General/WalletSyncRow.tsx @@ -1,21 +1,27 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import SettingsRow from "~/components/SettingsRow"; import { useTranslation } from "react-i18next"; import { useNavigation } from "@react-navigation/native"; import { NavigatorName, ScreenName } from "~/const"; import { - useWalletSyncAnalytics, + useLedgerSyncAnalytics, AnalyticsPage, AnalyticsButton, -} from "LLM/features/WalletSync/hooks/useWalletSyncAnalytics"; +} from "LLM/features/WalletSync/hooks/useLedgerSyncAnalytics"; import { useSelector } from "react-redux"; import { trustchainSelector } from "@ledgerhq/trustchain/store"; +import ActivationDrawer from "LLM/features/WalletSync/screens/Activation/ActivationDrawer"; +import { Steps } from "LLM/features/WalletSync/types/Activation"; const WalletSyncRow = () => { const { t } = useTranslation(); - const { onClickTrack } = useWalletSyncAnalytics(); + const { onClickTrack } = useLedgerSyncAnalytics(); const navigation = useNavigation(); + const [isDrawerVisible, setIsDrawerVisible] = useState(false); + const closeDrawer = useCallback(() => { + setIsDrawerVisible(false); + }, []); const trustchain = useSelector(trustchainSelector); const navigateToWalletSyncActivationScreen = useCallback(() => { @@ -27,21 +33,27 @@ const WalletSyncRow = () => { screen: ScreenName.WalletSyncActivated, }); } else { - navigation.navigate(NavigatorName.WalletSync, { - screen: ScreenName.WalletSyncActivationInit, - }); + setIsDrawerVisible(true); } }, [navigation, onClickTrack, trustchain?.rootId]); return ( - + <> + + + + ); }; diff --git a/libs/ui/packages/native/src/components/Form/TabSelector/index.tsx b/libs/ui/packages/native/src/components/Form/TabSelector/index.tsx new file mode 100644 index 000000000000..701ad3b2712f --- /dev/null +++ b/libs/ui/packages/native/src/components/Form/TabSelector/index.tsx @@ -0,0 +1,122 @@ +import React, { useEffect } from "react"; +import Text from "../../Text"; +import Flex from "../../Layout/Flex"; +import styled, { useTheme } from "styled-components/native"; +import { TouchableOpacity } from "react-native"; +import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated"; + +const StyledTouchableOpacity = styled(TouchableOpacity)` + flex: 1; + overflow: hidden; +`; + +const StyledFlex = styled(Flex)<{ isSelected: boolean }>` + width: 100%; + height: 100%; + justify-content: center; + align-items: center; +`; + +const StyledText = styled(Text)<{ isSelected: boolean }>` + line-height: 14.52px; + text-align: center; + font-size: 12px; + color: ${(p) => + p.isSelected ? p.theme.colors.constant.black : p.theme.colors.opacityDefault.c50}; +`; + +interface OptionButtonProps { + option: T; + selectedOption: T; + handleSelectOption: (option: T) => void; + label: string; +} + +const OptionButton = ({ + option, + selectedOption, + handleSelectOption, + label, +}: OptionButtonProps) => { + const isSelected = selectedOption === option; + + return ( + handleSelectOption(option)}> + + + {label} + + + + ); +}; + +interface TabSelectorProps { + options: T[]; + selectedOption: T; + handleSelectOption: (option: T) => void; + labels: { [key in T]: string }; +} + +export default function TabSelector({ + options, + selectedOption, + handleSelectOption, + labels, +}: TabSelectorProps): JSX.Element { + const { colors } = useTheme(); + const translateX = useSharedValue(-39); + + useEffect(() => { + translateX.value = withSpring(selectedOption === options[0] ? -40 : 40, { + damping: 30, + stiffness: 80, + }); + }, [selectedOption, translateX, options]); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ translateX: translateX.value }], + }; + }); + + return ( + + + {options.map((option) => ( + + ))} + + ); +} diff --git a/libs/ui/packages/native/src/components/Form/index.ts b/libs/ui/packages/native/src/components/Form/index.ts index b90403476184..3f14bbd68d27 100644 --- a/libs/ui/packages/native/src/components/Form/index.ts +++ b/libs/ui/packages/native/src/components/Form/index.ts @@ -4,3 +4,4 @@ export { default as Slider } from "./Slider"; export { default as Switch } from "./Switch"; export { default as Toggle } from "./Toggle"; export { default as SelectableList } from "./SelectableList"; +export { default as TabSelector } from "./TabSelector"; diff --git a/libs/ui/packages/native/src/components/Layout/Modals/BaseModal/index.tsx b/libs/ui/packages/native/src/components/Layout/Modals/BaseModal/index.tsx index e4014ca782f1..e83ba0d5ed25 100644 --- a/libs/ui/packages/native/src/components/Layout/Modals/BaseModal/index.tsx +++ b/libs/ui/packages/native/src/components/Layout/Modals/BaseModal/index.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useCallback } from "react"; import ReactNativeModal, { ModalProps } from "react-native-modal"; import styled from "styled-components/native"; -import { StyleProp, ViewStyle } from "react-native"; +import { StyleProp, TouchableOpacity, ViewStyle } from "react-native"; import sizes from "../../../../helpers/getDeviceSize"; import Text from "../../../Text"; @@ -9,7 +9,7 @@ import { IconOrElementType } from "../../../Icon/type"; import { BoxedIcon } from "../../../Icon"; import { Flex } from "../../index"; import { space } from "styled-system"; -import { Close } from "@ledgerhq/icons-ui/native"; +import { Close, ArrowLeft } from "@ledgerhq/icons-ui/native"; import { useTheme } from "styled-components/native"; const { width, height } = sizes; @@ -17,6 +17,7 @@ const { width, height } = sizes; export type BaseModalProps = { isOpen?: boolean; onClose?: () => void; + onBack?: () => void; modalStyle?: StyleProp; safeContainerStyle?: StyleProp; containerStyle?: StyleProp; @@ -28,6 +29,7 @@ export type BaseModalProps = { subtitle?: string; children?: React.ReactNode; noCloseButton?: boolean; + hasBackButton?: boolean; CustomHeader?: React.ComponentType<{ children?: ReactNode }>; } & Partial; @@ -54,6 +56,13 @@ const CloseContainer = styled.View` z-index: 10; `; +const BackContainer = styled.View` + display: flex; + align-items: flex-start; + margin-bottom: ${(p) => p.theme.space[6]}px; + z-index: 10; +`; + const ClosePressableExtendedBounds = styled.TouchableOpacity.attrs({ p: 3, })` @@ -127,10 +136,26 @@ export function ModalHeaderCloseButton({ ); } +export function ModalHeaderBackButton({ + onBack, +}: Pick): React.ReactElement { + const { colors } = useTheme(); + + return ( + + + + + + ); +} + export default function BaseModal({ isOpen, onClose = () => {}, + onBack = () => {}, noCloseButton, + hasBackButton, safeContainerStyle = {}, containerStyle = {}, modalStyle = {}, @@ -181,7 +206,23 @@ export default function BaseModal({ )} - {!CustomHeader && !noCloseButton && } + + {!CustomHeader && onBack && hasBackButton && ( + + + + )} + {!CustomHeader && !noCloseButton && ( + + + + )} + { + const [selectedOption, setSelectedOption] = useState(args.selectedOption); + + return ( + + + + ); +}; + +TabSelectorStory.storyName = "TabSelectorStory"; + +const TabSelectorStoryArgs = { + options: ["option1", "option2"], + selectedOption: "option1", + labels: { + option1: "Option 1", + option2: "Option 2", + }, +}; + +TabSelectorStory.args = TabSelectorStoryArgs; diff --git a/libs/ui/packages/native/storybook/stories/index.ts b/libs/ui/packages/native/storybook/stories/index.ts index d630613a862a..f5edc8322ffa 100644 --- a/libs/ui/packages/native/storybook/stories/index.ts +++ b/libs/ui/packages/native/storybook/stories/index.ts @@ -31,6 +31,7 @@ export * as NumberInputStory from "./Form/Input/NumberInput.stories"; export * as LintStory from "./Link/Link.stories"; export * as SwitchStory from "./Form/Switch.stories"; export * as ToggleStory from "./Form/Toggle.stories"; +export * as TabSelector from "./Form/TabSelector.stories"; export * as ChipTabsStory from "./Tabs/ChipTabs/ChipTabs.stories"; export * as GraphTabsStory from "./Tabs/GraphTabs/GraphTabs.stories"; export * as BadgeStory from "./Tag/Badge.stories";