diff --git a/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx b/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx index bef10b261..81ae64368 100644 --- a/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx +++ b/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next'; import { BrowserViewSections } from '@lib/scripts/types'; import { useWalletActivities } from '@hooks/useWalletActivities'; import { + MULTIDELEGATION_DAPP_COMPATIBILITY_LS_KEY, MULTIDELEGATION_FIRST_VISIT_LS_KEY, MULTIDELEGATION_FIRST_VISIT_SINCE_PORTFOLIO_PERSISTENCE_LS_KEY, STAKING_BROWSER_PREFERENCES_LS_KEY @@ -86,6 +87,8 @@ export const MultiDelegationStakingPopup = (): JSX.Element => { MULTIDELEGATION_FIRST_VISIT_LS_KEY, true ); + const [multidelegationDAppCompatibility, { updateLocalStorage: setMultidelegationDAppCompatibility }] = + useLocalStorage(MULTIDELEGATION_DAPP_COMPATIBILITY_LS_KEY, true); const [ multidelegationFirstVisitSincePortfolioPersistence, { updateLocalStorage: setMultidelegationFirstVisitSincePortfolioPersistence } @@ -109,6 +112,8 @@ export const MultiDelegationStakingPopup = (): JSX.Element => { setStakingBrowserPreferencesPersistence, multidelegationFirstVisit, triggerMultidelegationFirstVisit: () => setMultidelegationFirstVisit(false), + multidelegationDAppCompatibility, + triggerMultidelegationDAppCompatibility: () => setMultidelegationDAppCompatibility(false), multidelegationFirstVisitSincePortfolioPersistence, triggerMultidelegationFirstVisitSincePortfolioPersistence: () => { setMultidelegationFirstVisit(false); diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index f419ac428..7bad86582 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -554,6 +554,7 @@ describe('Testing useWalletManager hook', () => { 'hideBalance', 'isForgotPasswordFlow', 'multidelegationFirstVisit', + 'isMultiDelegationDAppCompatibilityModalVisible', 'multidelegationFirstVisitSincePortfolioPersistence' ] }); diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 3fe0e28da..2523a4c54 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -614,6 +614,7 @@ export const useWalletManager = (): UseWalletManager => { 'hideBalance', 'isForgotPasswordFlow', 'multidelegationFirstVisit', + 'isMultiDelegationDAppCompatibilityModalVisible', 'multidelegationFirstVisitSincePortfolioPersistence' ]; diff --git a/apps/browser-extension-wallet/src/types/local-storage.ts b/apps/browser-extension-wallet/src/types/local-storage.ts index b7ccfd532..c00b87193 100644 --- a/apps/browser-extension-wallet/src/types/local-storage.ts +++ b/apps/browser-extension-wallet/src/types/local-storage.ts @@ -58,6 +58,7 @@ export interface ILocalStorage { analyticsStatus?: EnhancedAnalyticsOptInStatus; isForgotPasswordFlow?: boolean; multidelegationFirstVisit?: boolean; + isMultiDelegationDAppCompatibilityModalVisible?: boolean; multidelegationFirstVisitSincePortfolioPersistence?: boolean; unconfirmedTransactions: UnconfirmedTransaction[]; stakingBrowserPreferences: StakingBrowserPreferences; diff --git a/apps/browser-extension-wallet/src/utils/constants.ts b/apps/browser-extension-wallet/src/utils/constants.ts index c0d464571..5a92e41bd 100644 --- a/apps/browser-extension-wallet/src/utils/constants.ts +++ b/apps/browser-extension-wallet/src/utils/constants.ts @@ -107,4 +107,5 @@ export const COINGECKO_URL = 'https://www.coingecko.com'; export const MULTIDELEGATION_FIRST_VISIT_SINCE_PORTFOLIO_PERSISTENCE_LS_KEY = 'multidelegationFirstVisitSincePortfolioPersistence'; export const MULTIDELEGATION_FIRST_VISIT_LS_KEY = 'multidelegationFirstVisit'; +export const MULTIDELEGATION_DAPP_COMPATIBILITY_LS_KEY = 'isMultiDelegationDAppCompatibilityModalVisible'; export const STAKING_BROWSER_PREFERENCES_LS_KEY = 'stakingBrowserPreferences'; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx index 805454e5e..e8c279d22 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx @@ -13,6 +13,7 @@ import { useAnalyticsContext, useCurrencyStore, useExternalLinkOpener } from '@p import { DEFAULT_STAKING_BROWSER_PREFERENCES, OutsideHandlesProvider } from '@lace/staking'; import { useBalances, useCustomSubmitApi, useFetchCoinPrice, useLocalStorage } from '@hooks'; import { + MULTIDELEGATION_DAPP_COMPATIBILITY_LS_KEY, MULTIDELEGATION_FIRST_VISIT_LS_KEY, MULTIDELEGATION_FIRST_VISIT_SINCE_PORTFOLIO_PERSISTENCE_LS_KEY, STAKING_BROWSER_PREFERENCES_LS_KEY @@ -32,6 +33,8 @@ export const StakingContainer = (): React.ReactElement => { MULTIDELEGATION_FIRST_VISIT_LS_KEY, true ); + const [multidelegationDAppCompatibility, { updateLocalStorage: setMultidelegationDAppCompatibility }] = + useLocalStorage(MULTIDELEGATION_DAPP_COMPATIBILITY_LS_KEY, true); const [ multidelegationFirstVisitSincePortfolioPersistence, { updateLocalStorage: setMultidelegationFirstVisitSincePortfolioPersistence } @@ -125,6 +128,8 @@ export const StakingContainer = (): React.ReactElement => { compactNumber: compactNumberWithUnit, multidelegationFirstVisit, triggerMultidelegationFirstVisit: () => setMultidelegationFirstVisit(false), + multidelegationDAppCompatibility, + triggerMultidelegationDAppCompatibility: () => setMultidelegationDAppCompatibility(false), multidelegationFirstVisitSincePortfolioPersistence, triggerMultidelegationFirstVisitSincePortfolioPersistence: () => { setMultidelegationFirstVisit(false); diff --git a/packages/e2e-tests/src/elements/staking/MultidelegationDAppIssueModal.ts b/packages/e2e-tests/src/elements/staking/MultidelegationDAppIssueModal.ts new file mode 100644 index 000000000..349e2756b --- /dev/null +++ b/packages/e2e-tests/src/elements/staking/MultidelegationDAppIssueModal.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-undef */ +import { ChainablePromiseElement } from 'webdriverio'; + +class MultidelegationDAppIssueModal { + private TITLE = '[data-testid="stake-modal-title"]'; + private DESCRIPTION = '[data-testid="stake-modal-description"]'; + private GOT_IT_BUTTON = '[data-testid="multidelegation-dapp-modal-button"]'; + + get title(): ChainablePromiseElement { + return $(this.TITLE); + } + + get description(): ChainablePromiseElement { + return $(this.DESCRIPTION); + } + + get gotItButton(): ChainablePromiseElement { + return $(this.GOT_IT_BUTTON); + } +} + +export default new MultidelegationDAppIssueModal(); diff --git a/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature b/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature index a0b538118..3b83d139c 100644 --- a/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature +++ b/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature @@ -28,6 +28,7 @@ Feature: Delegating funds to new pool E2E And I navigate to Transactions extended page Then the Received transaction is displayed with value: "5.00 tADA" and tokens count 1 And I disable showing Multidelegation beta banner + And I disable showing Multidelegation DApps issue modal And I navigate to Staking extended page And I open Browse pools tab And I switch to list view on "Browse pools" tab diff --git a/packages/e2e-tests/src/fixture/localStorageInitializer.ts b/packages/e2e-tests/src/fixture/localStorageInitializer.ts index 4964b5aa0..ff0a2f20c 100755 --- a/packages/e2e-tests/src/fixture/localStorageInitializer.ts +++ b/packages/e2e-tests/src/fixture/localStorageInitializer.ts @@ -85,6 +85,10 @@ class LocalStorageInitializer { await localStorageManager.setItem('analyticsStatus', ''); }; + disableShowingMultidelegationDAppsIssueModal = async () => { + await localStorageManager.setItem('isMultiDelegationDAppCompatibilityModalVisible', 'false'); + }; + initialiseBasicLocalStorageData = async ( walletName: string, chainName: 'Preprod' | 'Preview' | 'Mainnet' diff --git a/packages/e2e-tests/src/hooks/beforeTagHooks.ts b/packages/e2e-tests/src/hooks/beforeTagHooks.ts index 7a4c96497..4352d3c01 100644 --- a/packages/e2e-tests/src/hooks/beforeTagHooks.ts +++ b/packages/e2e-tests/src/hooks/beforeTagHooks.ts @@ -152,11 +152,13 @@ Before( Before({ tags: '@Staking-NonDelegatedFunds-Extended' }, async () => { await extendedViewWalletInitialization(TestWalletName.TAWalletNonDelegatedFunds); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); }); Before({ tags: '@Staking-NonDelegatedFunds-Popup' }, async () => { await popupViewWalletInitialization(TestWalletName.TAWalletNonDelegatedFunds); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); }); Before( @@ -174,6 +176,7 @@ Before({ tags: '@AdaHandle-popup' }, async () => await popupViewWalletInitializa Before({ tags: '@Multidelegation-SwitchingPools-Extended-E2E' }, async () => { await extendedViewWalletInitialization(TestWalletName.WalletMultidelegationSwitchPoolsE2E); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); }); Before( @@ -189,23 +192,27 @@ Before( Before({ tags: '@Multidelegation-DelegatedFunds-SinglePool-Popup' }, async () => { await popupViewWalletInitialization(TestWalletName.MultidelegationDelegatedSingle); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); await localStorageInitializer.initializeShowMultiAddressDiscoveryModal(false); }); Before({ tags: '@Multidelegation-DelegatedFunds-SinglePool-Extended' }, async () => { await extendedViewWalletInitialization(TestWalletName.MultidelegationDelegatedSingle); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); await localStorageInitializer.initializeShowMultiAddressDiscoveryModal(false); }); Before({ tags: '@Multidelegation-DelegatedFunds-MultiplePools-Popup' }, async () => { await popupViewWalletInitialization(TestWalletName.MultidelegationDelegatedMulti); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); await localStorageInitializer.initializeShowMultiAddressDiscoveryModal(false); }); Before({ tags: '@Multidelegation-DelegatedFunds-MultiplePools-Extended' }, async () => { await extendedViewWalletInitialization(TestWalletName.MultidelegationDelegatedMulti); await localStorageInitializer.disableShowingMultidelegationBetaBanner(); + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); await localStorageInitializer.initializeShowMultiAddressDiscoveryModal(false); }); diff --git a/packages/e2e-tests/src/steps/commonSteps.ts b/packages/e2e-tests/src/steps/commonSteps.ts index 5d6f73f06..2d298faff 100755 --- a/packages/e2e-tests/src/steps/commonSteps.ts +++ b/packages/e2e-tests/src/steps/commonSteps.ts @@ -321,6 +321,10 @@ Given(/^I disable showing Multidelegation persistence banner$/, async () => { await localStorageInitializer.disableShowingMultidelegationPersistenceBanner(); }); +Given(/^I disable showing Multidelegation DApps issue modal$/, async () => { + await localStorageInitializer.disableShowingMultidelegationDAppsIssueModal(); +}); + Given(/^I enable showing Analytics consent banner$/, async () => { await localStorageInitializer.enableShowingAnalyticsBanner(); await browser.refresh(); diff --git a/packages/e2e-tests/src/steps/multidelegationSteps.ts b/packages/e2e-tests/src/steps/multidelegationSteps.ts index a2d360546..16b18c116 100644 --- a/packages/e2e-tests/src/steps/multidelegationSteps.ts +++ b/packages/e2e-tests/src/steps/multidelegationSteps.ts @@ -33,6 +33,7 @@ import MoreOptionsComponentAssert from '../assert/multidelegation/MoreOptionsCom import { mapColumnNameStringToEnum, mapSortingOptionNameStringToEnum } from '../utils/stakePoolListContent'; import { browser } from '@wdio/globals'; import { StakePoolSortingOption } from '../enums/StakePoolSortingOption'; +import MultidelegationDAppIssueModal from '../elements/staking/MultidelegationDAppIssueModal'; const validPassword = 'N_8J@bne87A'; @@ -584,3 +585,9 @@ Then( ); } ); + +When(/^I close the modal about issues with multidelegation and DApps$/, async () => { + if (await MultidelegationDAppIssueModal.gotItButton.isDisplayed()) { + await MultidelegationDAppIssueModal.gotItButton.click(); + } +}); diff --git a/packages/staking/.storybook/StakingStorybookProvider.tsx b/packages/staking/.storybook/StakingStorybookProvider.tsx index bebb7a0cf..7734a5159 100644 --- a/packages/staking/.storybook/StakingStorybookProvider.tsx +++ b/packages/staking/.storybook/StakingStorybookProvider.tsx @@ -43,6 +43,8 @@ const outsideHandlesMocks: OutsideHandlesContextValue = { compactNumber: undefined, multidelegationFirstVisit: undefined, triggerMultidelegationFirstVisit: undefined, + multidelegationDAppCompatibility: undefined, + triggerMultidelegationDAppCompatibility: undefined, multidelegationFirstVisitSincePortfolioPersistence: undefined, triggerMultidelegationFirstVisitSincePortfolioPersistence: undefined, walletAddress: undefined, diff --git a/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListRow.tsx b/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListRow.tsx index 25de0685d..0f8b538ea 100644 --- a/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListRow.tsx +++ b/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListRow.tsx @@ -1,9 +1,11 @@ import { PostHogAction } from '@lace/common'; import { Table } from '@lace/ui'; -import React from 'react'; +import { MultidelegationDAppCompatibilityModal } from 'features/modals/MultidelegationDAppCompatibilityModal'; +import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useOutsideHandles } from '../../outside-handles-provider'; import { MAX_POOLS_COUNT, StakePoolDetails, isPoolSelectedSelector, useDelegationPortfolioStore } from '../../store'; + import { config } from './config'; export const StakePoolsListRow = ({ stakePool, hexId, id, ...data }: StakePoolDetails): React.ReactElement => { @@ -16,12 +18,15 @@ export const StakePoolsListRow = ({ stakePool, hexId, id, ...data }: StakePoolDe selectionsFull: store.selectedPortfolio.length === MAX_POOLS_COUNT, })); + const { multidelegationDAppCompatibility, triggerMultidelegationDAppCompatibility } = useOutsideHandles(); + const [showDAppCompatibilityModal, setShowDAppCompatibilityModal] = useState(false); + const onClick = () => { portfolioMutators.executeCommand({ data: stakePool, type: 'ShowPoolDetailsFromList' }); analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsStakePoolDetailClick); }; - const onSelect = () => { + const onPoolSelect = useCallback(() => { if (poolAlreadySelected) { portfolioMutators.executeCommand({ data: hexId, type: 'UnselectPoolFromList' }); analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsUnselectClick); @@ -29,20 +34,41 @@ export const StakePoolsListRow = ({ stakePool, hexId, id, ...data }: StakePoolDe portfolioMutators.executeCommand({ data: [stakePool], type: 'SelectPoolFromList' }); analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsStakeClick); } - }; + }, [analytics, hexId, poolAlreadySelected, portfolioMutators, stakePool]); + + const onDAppCompatibilityConfirm = useCallback(() => { + triggerMultidelegationDAppCompatibility(); + onPoolSelect(); + }, [onPoolSelect, triggerMultidelegationDAppCompatibility]); + + const onSelect = useCallback(() => { + if (multidelegationDAppCompatibility && !poolAlreadySelected) { + setShowDAppCompatibilityModal(true); + } else { + onPoolSelect(); + } + }, [multidelegationDAppCompatibility, onPoolSelect, poolAlreadySelected]); return ( - > - columns={config.columns} - cellRenderers={config.renderer} - data={data} - selected={poolAlreadySelected} - onClick={onClick} - selectionDisabledMessage={t('browsePools.tooltips.maxNumberPoolsSelected')} - dataTestId="stake-pool" - withSelection - keyProp={id} - {...((!selectionsFull || poolAlreadySelected) && { onSelect })} - /> + <> + > + columns={config.columns} + cellRenderers={config.renderer} + data={data} + selected={poolAlreadySelected} + onClick={onClick} + selectionDisabledMessage={t('browsePools.tooltips.maxNumberPoolsSelected')} + dataTestId="stake-pool" + withSelection + keyProp={id} + {...((!selectionsFull || poolAlreadySelected) && { onSelect })} + /> + {showDAppCompatibilityModal && ( + + )} + ); }; diff --git a/packages/staking/src/features/Drawer/Drawer.tsx b/packages/staking/src/features/Drawer/Drawer.tsx index 4b7c1038c..b1ce3584b 100644 --- a/packages/staking/src/features/Drawer/Drawer.tsx +++ b/packages/staking/src/features/Drawer/Drawer.tsx @@ -97,7 +97,7 @@ export const Drawer = ({ const contentsMap = useMemo( (): Record => ({ [DrawerDefaultStep.PoolDetails]: , - [DrawerManagementStep.Preferences]: , + [DrawerManagementStep.Preferences]: , [DrawerManagementStep.Confirmation]: , [DrawerManagementStep.Sign]: , [DrawerManagementStep.Success]: , diff --git a/packages/staking/src/features/Drawer/StakePoolDetail.tsx b/packages/staking/src/features/Drawer/StakePoolDetail.tsx index 8518e93b8..17517d6a6 100644 --- a/packages/staking/src/features/Drawer/StakePoolDetail.tsx +++ b/packages/staking/src/features/Drawer/StakePoolDetail.tsx @@ -6,8 +6,9 @@ import { Button, Flex } from '@lace/ui'; import cn from 'classnames'; import { StakePoolCardProgressBar } from 'features/BrowsePools'; import { isOversaturated } from 'features/BrowsePools/utils'; +import { MultidelegationDAppCompatibilityModal } from 'features/modals/MultidelegationDAppCompatibilityModal'; import { TFunction } from 'i18next'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useOutsideHandles } from '../outside-handles-provider'; import { @@ -254,6 +255,7 @@ const makeSelector = poolInCurrentPortfolio, poolSelected, selectionsEmpty: selectedPortfolio.length === 0, + userAlreadyMultidelegated: currentPortfolio.length > 1, }; }; @@ -312,15 +314,22 @@ const makeActionButtons = ( export const StakePoolDetailFooter = ({ popupView }: StakePoolDetailFooterProps): React.ReactElement => { const { t } = useTranslation(); - const { analytics } = useOutsideHandles(); + const { analytics, multidelegationDAppCompatibility, triggerMultidelegationDAppCompatibility } = useOutsideHandles(); const { walletStoreWalletType } = useOutsideHandles(); + const [showDAppCompatibilityModal, setShowDAppCompatibilityModal] = useState(false); const { openPoolDetails, portfolioMutators, viewedStakePool } = useDelegationPortfolioStore((store) => ({ openPoolDetails: stakePoolDetailsSelector(store), portfolioMutators: store.mutators, viewedStakePool: store.viewedStakePool, })); - const { ableToSelect, ableToStakeOnlyOnThisPool, selectionsEmpty, poolInCurrentPortfolio, poolSelected } = - useDelegationPortfolioStore(makeSelector(openPoolDetails)); + const { + ableToSelect, + ableToStakeOnlyOnThisPool, + selectionsEmpty, + poolInCurrentPortfolio, + poolSelected, + userAlreadyMultidelegated, + } = useDelegationPortfolioStore(makeSelector(openPoolDetails)); const isInMemory = walletStoreWalletType === WalletType.InMemory; @@ -329,16 +338,7 @@ export const StakePoolDetailFooter = ({ popupView }: StakePoolDetailFooterProps) portfolioMutators.executeCommand({ type: 'BeginSingleStaking' }); }, [analytics, portfolioMutators]); - useEffect(() => { - if (isInMemory) return; - if (popupView) return; - const hasPersistedHwStakepool = !!localStorage.getItem('TEMP_POOLID'); - if (!hasPersistedHwStakepool) return; - onStakeOnThisPool(); - localStorage.removeItem('TEMP_POOLID'); - }, [isInMemory, onStakeOnThisPool, popupView]); - - const onSelectClick = useCallback(() => { + const selectPoolFromDetails = useCallback(() => { if (!viewedStakePool) return; analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsStakePoolDetailAddStakingPoolClick); portfolioMutators.executeCommand({ @@ -347,6 +347,27 @@ export const StakePoolDetailFooter = ({ popupView }: StakePoolDetailFooterProps) }); }, [viewedStakePool, portfolioMutators, analytics]); + const onSelectClick = useCallback(() => { + if (!userAlreadyMultidelegated && multidelegationDAppCompatibility) { + setShowDAppCompatibilityModal(true); + } else { + selectPoolFromDetails(); + } + }, [multidelegationDAppCompatibility, selectPoolFromDetails, userAlreadyMultidelegated]); + + const onDAppCompatibilityConfirm = useCallback(() => { + triggerMultidelegationDAppCompatibility(); + selectPoolFromDetails(); + }, [selectPoolFromDetails, triggerMultidelegationDAppCompatibility]); + + useEffect(() => { + if (isInMemory || popupView) return; + const hasPersistedHwStakepool = !!localStorage.getItem('TEMP_POOLID'); + if (!hasPersistedHwStakepool) return; + onStakeOnThisPool(); + localStorage.removeItem('TEMP_POOLID'); + }, [isInMemory, onStakeOnThisPool, popupView]); + const onUnselectClick = useCallback(() => { if (!viewedStakePool) return; analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsStakePoolDetailUnselectPoolClick); @@ -392,18 +413,27 @@ export const StakePoolDetailFooter = ({ popupView }: StakePoolDetailFooterProps) const [callToActionButton, ...secondaryButtons] = actionButtons; return ( - - {callToActionButton && ( - + + {callToActionButton && ( + + )} + {secondaryButtons.map(({ callback, dataTestId, label }) => ( + + ))} + + {showDAppCompatibilityModal && ( + )} - {secondaryButtons.map(({ callback, dataTestId, label }) => ( - - ))} - + ); }; diff --git a/packages/staking/src/features/Drawer/preferences/StepPreferencesContent.tsx b/packages/staking/src/features/Drawer/preferences/StepPreferencesContent.tsx index efa81e704..6c8f06184 100644 --- a/packages/staking/src/features/Drawer/preferences/StepPreferencesContent.tsx +++ b/packages/staking/src/features/Drawer/preferences/StepPreferencesContent.tsx @@ -2,6 +2,8 @@ import { Wallet } from '@lace/cardano'; import { PostHogAction } from '@lace/common'; import { Box, ControlButton, Flex, PIE_CHART_DEFAULT_COLOR_SET, PieChartColor, Text } from '@lace/ui'; +import { MultidelegationDAppCompatibilityModal } from 'features/modals/MultidelegationDAppCompatibilityModal'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DelegationCard, DelegationStatus } from '../../DelegationCard'; import { useOutsideHandles } from '../../outside-handles-provider'; @@ -34,22 +36,37 @@ const getDraftDelegationStatus = ({ draftPortfolio }: DelegationPortfolioStore): throw new Error('Unexpected delegation status'); }; -export const StepPreferencesContent = () => { +export type StepPreferencesContentProps = { + popupView?: boolean; +}; + +export const StepPreferencesContent = ({ popupView }: StepPreferencesContentProps) => { const { t } = useTranslation(); + const [showDAppCompatibilityModal, setShowDAppCompatibilityModal] = useState(false); const { analytics } = useOutsideHandles(); const { balancesBalance, walletStoreWalletUICardanoCoin: { symbol }, compactNumber, + multidelegationDAppCompatibility, + triggerMultidelegationDAppCompatibility, } = useOutsideHandles(); - const { draftPortfolio, activeDelegationFlow, portfolioMutators, delegationStatus, cardanoCoinSymbol } = - useDelegationPortfolioStore((state) => ({ - activeDelegationFlow: state.activeDelegationFlow, - cardanoCoinSymbol: state.cardanoCoinSymbol, - delegationStatus: getDraftDelegationStatus(state), - draftPortfolio: state.draftPortfolio || [], - portfolioMutators: state.mutators, - })); + + const { + draftPortfolio, + activeDelegationFlow, + portfolioMutators, + delegationStatus, + cardanoCoinSymbol, + userAlreadyMultidelegated, + } = useDelegationPortfolioStore((state) => ({ + activeDelegationFlow: state.activeDelegationFlow, + cardanoCoinSymbol: state.cardanoCoinSymbol, + delegationStatus: getDraftDelegationStatus(state), + draftPortfolio: state.draftPortfolio || [], + portfolioMutators: state.mutators, + userAlreadyMultidelegated: state.currentPortfolio.length > 1, + })); const displayData = draftPortfolio.map((draftPool, i) => { const { @@ -84,67 +101,90 @@ export const StepPreferencesContent = () => { analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsStakePoolDetailUnselectPoolClick); }; const addPoolButtonDisabled = draftPortfolio.length === MAX_POOLS_COUNT; - const onAddPoolButtonClick = () => { + + const onAddPool = useCallback(() => { analytics.sendEventToPostHog(PostHogAction.StakingBrowsePoolsManageDelegationAddStakePoolClick); portfolioMutators.executeCommand({ type: 'AddStakePools', }); - }; + }, [analytics, portfolioMutators]); + + const onAddPoolButtonClick = useCallback(() => { + if (!userAlreadyMultidelegated && multidelegationDAppCompatibility) { + setShowDAppCompatibilityModal(true); + } else { + onAddPool(); + } + }, [multidelegationDAppCompatibility, onAddPool, userAlreadyMultidelegated]); + + const onDAppCompatibilityConfirm = useCallback(() => { + triggerMultidelegationDAppCompatibility(); + onAddPool(); + }, [onAddPool, triggerMultidelegationDAppCompatibility]); return ( - - - - - - - {t('drawer.preferences.selectedStakePools', { count: draftPortfolio.length })} - - + <> + + + + + + + {t('drawer.preferences.selectedStakePools', { count: draftPortfolio.length })} + + + + + {displayData.length === 0 && ( + + + + )} + {displayData.map( + ( + { color, id, name, stakeValue, onChainPercentage, savedIntegerPercentage, sliderIntegerPercentage }, + idx + ) => ( + { + portfolioMutators.executeCommand({ + data: { id, newSliderPercentage: value }, + type: 'UpdateStakePercentage', + }); + }} + /> + ) + )} + - - {displayData.length === 0 && ( - - - - )} - {displayData.map( - ( - { color, id, name, stakeValue, onChainPercentage, savedIntegerPercentage, sliderIntegerPercentage }, - idx - ) => ( - { - portfolioMutators.executeCommand({ - data: { id, newSliderPercentage: value }, - type: 'UpdateStakePercentage', - }); - }} - /> - ) - )} - - + {showDAppCompatibilityModal && ( + + )} + ); }; diff --git a/packages/staking/src/features/i18n/translations/en.ts b/packages/staking/src/features/i18n/translations/en.ts index 0778ff1a3..02ff8a1ee 100644 --- a/packages/staking/src/features/i18n/translations/en.ts +++ b/packages/staking/src/features/i18n/translations/en.ts @@ -160,6 +160,10 @@ export const en: Translations = { 'modals.changingPreferences.description': "That's totally fine! Just please note that you'll continue receiving rewards from your former pool(s) for two epochs. After that, you'll start to receiving rewards from your new pool(s).", 'modals.changingPreferences.title': 'Changing staking preferences?', + 'modals.dapp.button': 'Got it', + 'modals.dapp.description': + "Multi-delegation allows you to delegate to multiple pools using a single payment key for an enhanced user experience. However, as not all dApps support this new mechanism, if you encounter issues with dApps after multi-delegating, refer to the 'Multi-staking' section of our FAQ to revert your wallet to its previous state before multi-delegating.", + 'modals.dapp.title': 'Multi-delegation', 'modals.poolsManagement.buttons.cancel': 'Cancel', 'modals.poolsManagement.buttons.confirm': 'Fine by me', 'modals.poolsManagement.description.adjustment': diff --git a/packages/staking/src/features/i18n/types.ts b/packages/staking/src/features/i18n/types.ts index f1562258d..dc43b6dec 100644 --- a/packages/staking/src/features/i18n/types.ts +++ b/packages/staking/src/features/i18n/types.ts @@ -237,6 +237,11 @@ type KeysStructure = { description: ''; }; }; + dapp: { + title: ''; + description: ''; + button: ''; + }; poolsManagement: { title: ''; buttons: { diff --git a/packages/staking/src/features/modals/MultidelegationDAppCompatibilityModal.tsx b/packages/staking/src/features/modals/MultidelegationDAppCompatibilityModal.tsx new file mode 100644 index 000000000..962c975a3 --- /dev/null +++ b/packages/staking/src/features/modals/MultidelegationDAppCompatibilityModal.tsx @@ -0,0 +1,60 @@ +import { Flex } from '@lace/ui'; +import { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { StakingModal } from './StakingModal'; + +const FAQ_URL = + 'https://www.lace.io/faq?question=why-do-some-dapps-behave-unexpectedly-when-they-start-using-multi-delegation'; + +interface MultidelegationBetaModalProps { + visible: boolean; + onConfirm: () => void; + popupView?: boolean; +} + +const CONFIRMATION_DELAY_IN_MS = 2000; + +export const MultidelegationDAppCompatibilityModal = ({ + visible, + onConfirm, + popupView, +}: MultidelegationBetaModalProps): React.ReactElement => { + const { t } = useTranslation(); + const [confirmDisabled, setConfirmDisabled] = useState(true); + + useEffect(() => { + setTimeout(() => { + setConfirmDisabled(false); + }, CONFIRMATION_DELAY_IN_MS); + }, []); + + return ( + + {t('modals.dapp.title')} + + } + description={ + , + }} + /> + } + actions={[ + { + body: t('modals.dapp.button'), + dataTestId: 'multidelegation-dapp-modal-button', + disabled: confirmDisabled, + onClick: onConfirm, + }, + ]} + /> + ); +}; diff --git a/packages/staking/src/features/outside-handles-provider/types.ts b/packages/staking/src/features/outside-handles-provider/types.ts index 6ab94b359..fbd54ca1a 100644 --- a/packages/staking/src/features/outside-handles-provider/types.ts +++ b/packages/staking/src/features/outside-handles-provider/types.ts @@ -103,6 +103,8 @@ export type OutsideHandlesContextValue = { compactNumber: (value: number | string, decimal?: number) => string; multidelegationFirstVisit: boolean; triggerMultidelegationFirstVisit: () => void; + multidelegationDAppCompatibility: boolean; + triggerMultidelegationDAppCompatibility: () => void; multidelegationFirstVisitSincePortfolioPersistence: boolean; triggerMultidelegationFirstVisitSincePortfolioPersistence: () => void; walletAddress: string; diff --git a/packages/staking/src/features/staking/OneTimeModals.tsx b/packages/staking/src/features/staking/OneTimeModals.tsx index 830ee4c05..5c9c2ad5e 100644 --- a/packages/staking/src/features/staking/OneTimeModals.tsx +++ b/packages/staking/src/features/staking/OneTimeModals.tsx @@ -1,3 +1,4 @@ +import { MultidelegationDAppCompatibilityModal } from 'features/modals/MultidelegationDAppCompatibilityModal'; import { useDelegationPortfolioStore } from 'features/store'; import { isPortfolioSavedOnChain } from 'features/store/delegationPortfolioStore/isPortfolioSavedOnChain'; import { useEffect } from 'react'; @@ -10,6 +11,8 @@ export const OneTimeModals = ({ popupView }: OneTimeModalManagerProps) => { const { multidelegationFirstVisit, triggerMultidelegationFirstVisit, + multidelegationDAppCompatibility, + triggerMultidelegationDAppCompatibility, multidelegationFirstVisitSincePortfolioPersistence, triggerMultidelegationFirstVisitSincePortfolioPersistence, } = useOutsideHandles(); @@ -46,6 +49,16 @@ export const OneTimeModals = ({ popupView }: OneTimeModalManagerProps) => { ); } + if (userAlreadyMultidelegated) { + return ( + + ); + } + if (!portfolioSavedOnChain) { return (