Skip to content

Commit

Permalink
feat(extension): add multi delegation and dapp issues modal
Browse files Browse the repository at this point in the history
  • Loading branch information
vetalcore committed May 7, 2024
1 parent 5a2aa15 commit a4bf009
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand All @@ -109,6 +112,8 @@ export const MultiDelegationStakingPopup = (): JSX.Element => {
setStakingBrowserPreferencesPersistence,
multidelegationFirstVisit,
triggerMultidelegationFirstVisit: () => setMultidelegationFirstVisit(false),
multidelegationDAppCompatibility,
triggerMultidelegationDAppCompatibility: () => setMultidelegationDAppCompatibility(false),
multidelegationFirstVisitSincePortfolioPersistence,
triggerMultidelegationFirstVisitSincePortfolioPersistence: () => {
setMultidelegationFirstVisit(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ describe('Testing useWalletManager hook', () => {
'hideBalance',
'isForgotPasswordFlow',
'multidelegationFirstVisit',
'multidelegationDAppCompatibility',
'multidelegationFirstVisitSincePortfolioPersistence'
]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ export const useWalletManager = (): UseWalletManager => {
'hideBalance',
'isForgotPasswordFlow',
'multidelegationFirstVisit',
'multidelegationDAppCompatibility',
'multidelegationFirstVisitSincePortfolioPersistence'
];

Expand Down
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/src/types/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface ILocalStorage {
analyticsStatus?: EnhancedAnalyticsOptInStatus;
isForgotPasswordFlow?: boolean;
multidelegationFirstVisit?: boolean;
multidelegationDAppCompatibility?: boolean;
multidelegationFirstVisitSincePortfolioPersistence?: boolean;
unconfirmedTransactions: UnconfirmedTransaction[];
stakingBrowserPreferences: StakingBrowserPreferences;
Expand Down
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 'multidelegationDAppCompatibility';
export const STAKING_BROWSER_PREFERENCES_LS_KEY = 'stakingBrowserPreferences';
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand Down Expand Up @@ -125,6 +128,8 @@ export const StakingContainer = (): React.ReactElement => {
compactNumber: compactNumberWithUnit,
multidelegationFirstVisit,
triggerMultidelegationFirstVisit: () => setMultidelegationFirstVisit(false),
multidelegationDAppCompatibility,
triggerMultidelegationDAppCompatibility: () => setMultidelegationDAppCompatibility(false),
multidelegationFirstVisitSincePortfolioPersistence,
triggerMultidelegationFirstVisitSincePortfolioPersistence: () => {
setMultidelegationFirstVisit(false);
Expand Down
2 changes: 2 additions & 0 deletions packages/staking/.storybook/StakingStorybookProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const outsideHandlesMocks: OutsideHandlesContextValue = {
compactNumber: undefined,
multidelegationFirstVisit: undefined,
triggerMultidelegationFirstVisit: undefined,
multidelegationDAppCompatibility: undefined,
triggerMultidelegationDAppCompatibility: undefined,
multidelegationFirstVisitSincePortfolioPersistence: undefined,
triggerMultidelegationFirstVisitSincePortfolioPersistence: undefined,
walletAddress: undefined,
Expand Down
80 changes: 55 additions & 25 deletions packages/staking/src/features/Drawer/StakePoolDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -254,6 +255,7 @@ const makeSelector =
poolInCurrentPortfolio,
poolSelected,
selectionsEmpty: selectedPortfolio.length === 0,
userAlreadyMultidelegated: currentPortfolio.length > 1,
};
};

Expand Down Expand Up @@ -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;

Expand All @@ -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({
Expand All @@ -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);
Expand Down Expand Up @@ -392,18 +413,27 @@ export const StakePoolDetailFooter = ({ popupView }: StakePoolDetailFooterProps)
const [callToActionButton, ...secondaryButtons] = actionButtons;

return (
<Flex flexDirection="column" alignItems="stretch" gap="$16">
{callToActionButton && (
<Button.CallToAction
label={callToActionButton.label}
data-testid={callToActionButton.dataTestId}
onClick={callToActionButton.callback}
w="$fill"
<>
<Flex flexDirection="column" alignItems="stretch" gap="$16">
{callToActionButton && (
<Button.CallToAction
label={callToActionButton.label}
data-testid={callToActionButton.dataTestId}
onClick={callToActionButton.callback}
w="$fill"
/>
)}
{secondaryButtons.map(({ callback, dataTestId, label }) => (
<Button.Secondary key={dataTestId} onClick={callback} data-testid={dataTestId} label={label} w="$fill" />
))}
</Flex>
{showDAppCompatibilityModal && (
<MultidelegationDAppCompatibilityModal
visible={multidelegationDAppCompatibility}
onConfirm={onDAppCompatibilityConfirm}
popupView={popupView}
/>
)}
{secondaryButtons.map(({ callback, dataTestId, label }) => (
<Button.Secondary key={dataTestId} onClick={callback} data-testid={dataTestId} label={label} w="$fill" />
))}
</Flex>
</>
);
};
4 changes: 4 additions & 0 deletions packages/staking/src/features/i18n/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Link>FAQ.</Link> 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':
Expand Down
5 changes: 5 additions & 0 deletions packages/staking/src/features/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ type KeysStructure = {
description: '';
};
};
dapp: {
title: '';
description: '';
button: '';
};
poolsManagement: {
title: '';
buttons: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<StakingModal
announcement
popupView={popupView}
visible={visible}
title={
<Flex alignItems={popupView ? 'flex-start' : 'center'} flexDirection="row" gap="$8">
{t('modals.dapp.title')}
</Flex>
}
description={
<Trans
i18nKey="modals.dapp.description"
t={t}
components={{
Link: <a href={FAQ_URL} rel="noreferrer noopener" target="_blank" />,
}}
/>
}
actions={[
{
body: t('modals.dapp.button'),
dataTestId: 'multidelegation-dapp-modal-button',
disabled: confirmDisabled,
onClick: onConfirm,
},
]}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions packages/staking/src/features/staking/OneTimeModals.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +11,8 @@ export const OneTimeModals = ({ popupView }: OneTimeModalManagerProps) => {
const {
multidelegationFirstVisit,
triggerMultidelegationFirstVisit,
multidelegationDAppCompatibility,
triggerMultidelegationDAppCompatibility,
multidelegationFirstVisitSincePortfolioPersistence,
triggerMultidelegationFirstVisitSincePortfolioPersistence,
} = useOutsideHandles();
Expand Down Expand Up @@ -46,6 +49,16 @@ export const OneTimeModals = ({ popupView }: OneTimeModalManagerProps) => {
);
}

if (!multidelegationFirstVisit) {
return (
<MultidelegationDAppCompatibilityModal
visible={multidelegationDAppCompatibility}
onConfirm={triggerMultidelegationDAppCompatibility}
popupView={popupView}
/>
);
}

if (!portfolioSavedOnChain) {
return (
<PortfolioPersistenceModal
Expand Down

0 comments on commit a4bf009

Please sign in to comment.