Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cbfdbbd
add reward banner
SorinC6 Dec 17, 2025
a900ed5
add staking on gov banner
SorinC6 Dec 18, 2025
6497fa3
update condition
SorinC6 Dec 18, 2025
1e40e68
fix flow and tsc
SorinC6 Dec 18, 2025
d10c515
Merge branch 'chore/YW-174/staking-revamp' into feat/YW-224/new-banners
SorinC6 Dec 18, 2025
9a65f3f
fix format
SorinC6 Dec 18, 2025
54756ee
fix comment
SorinC6 Dec 18, 2025
dda0000
Merge branch 'develop' into feat/YW-224/new-banners
Nebyt Dec 18, 2025
3272b4b
add poolId from config
SorinC6 Dec 19, 2025
dd8607e
add location
SorinC6 Dec 19, 2025
ada4b10
add undelegate condition
SorinC6 Dec 19, 2025
feb243e
fixes
SorinC6 Dec 19, 2025
55b81de
add optional props
SorinC6 Dec 19, 2025
961ff9f
add new prop and observer
SorinC6 Dec 19, 2025
2c4e08a
combined delegation tx
zuzunker Dec 19, 2025
1de4289
lint fix
zuzunker Dec 19, 2025
38f5952
wip
SorinC6 Dec 22, 2025
7159c2f
fix banner display
SorinC6 Dec 22, 2025
c4e4c35
add drep
SorinC6 Dec 22, 2025
c95ec08
fix format
SorinC6 Dec 22, 2025
44443ef
fix flow
SorinC6 Dec 22, 2025
e10521c
remove console.log
SorinC6 Dec 22, 2025
8510b8a
add drep id
SorinC6 Dec 22, 2025
921df40
Merge branch 'develop' into feat/YW-224/new-banners
Nebyt Dec 22, 2025
0ccf51a
remove unused
SorinC6 Dec 22, 2025
984d35c
remove unused
Nebyt Dec 22, 2025
6efe696
fix conditions
SorinC6 Dec 22, 2025
38948a6
fix delegate
SorinC6 Dec 22, 2025
85719bf
fix order
SorinC6 Dec 22, 2025
bc9c079
fix else statement
SorinC6 Dec 22, 2025
3c505cb
fix lint error
Nebyt Dec 22, 2025
4f0bf46
update string
Nebyt Dec 22, 2025
0c3a933
fix format
Nebyt Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions packages/yoroi-extension/app/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ import { StakingContextProvider } from './UI/features/staking/module/StakingCont
import BuySellDialog from './components/buySell/BuySellDialog';
// $FlowIgnore: suppressing this error
import TransactionReviewFailedPage from './UI/pages/TransactionReview/TransactionReviewFailedPage';
import { ModalProvider } from './UI/components/modals/ModalContext';
import { ReviewTxProvider } from './UI/features/transaction-review/module/ReviewTxProvider';

// PAGES
const LanguageSelectionPagePromise = () => import('./containers/profile/LanguageSelectionPage');
Expand Down Expand Up @@ -271,11 +273,32 @@ export const YoroiRoutes = (stores: StoresMap): Node => {
);
};

const WalletsSubpages = ({ stores }) => (
<Wallet stores={stores}>
<Outlet />
</Wallet>
);
const WalletsSubpages = ({ stores }) => {
const currentWalletInfo = createCurrrentWalletInfo(stores);

const { delegationTransaction } = stores.substores.ada;
const delegationTxResult = delegationTransaction.createDelegationTx.result;
const delegationTxError = delegationTransaction.createDelegationTx.error;

return (
<ReviewTxProvider stores={stores}>
<Wallet stores={stores}>
<GovernanceContextProvider
currentWallet={currentWalletInfo}
createDrepDelegationTransaction={request => stores.delegation.createDrepDelegationTransaction(request)}
signDelegationTransaction={request => stores.substores.ada.delegationTransaction.signTransaction(request)}
txDelegationResult={delegationTxResult}
txDelegationError={delegationTxError}
tokenInfo={stores.tokenInfoStore.tokenInfo}
triggerBuySellAdaDialog={() => stores.uiDialogs.open({ dialog: BuySellDialog })}
getCurrentPrice={stores.coinPriceStore.getCurrentPrice}
>
<Outlet />
</GovernanceContextProvider>
</Wallet>
</ReviewTxProvider>
);
};

const NftGallerySubPages = ({ stores }) => (
<NftGalleryContextProvider stores={stores}>
Expand Down Expand Up @@ -367,14 +390,6 @@ const DappCenterSubpages = ({ stores }) => (
</DappCenterContextProvider>
);

const StakingSubpages = ({ stores }) => (
<StakingContextProvider stores={stores}>
<Suspense fallback={null}>
<Outlet />
</Suspense>
</StakingContextProvider>
);

const CatalystRegistrationSubpages = ({ stores }) => (
<CatalystRegistrationContextProvider stores={stores}>
<Suspense fallback={null}>
Expand All @@ -383,13 +398,29 @@ const CatalystRegistrationSubpages = ({ stores }) => (
</CatalystRegistrationContextProvider>
);

const GovernanceSubpages = ({ stores }) => {
const StakingSubpages = ({ stores }) => (
<StakingAndGovernancePagesWrapper
stores={stores}
wrapOutlet={node => <StakingContextProvider stores={stores}>{node}</StakingContextProvider>}
/>
);

const GovernanceSubpages = ({ stores }) => <StakingAndGovernancePagesWrapper stores={stores} />;

const StakingAndGovernancePagesWrapper = ({ stores, wrapOutlet }: any) => {
const { unitOfAccount } = stores.profile;
const currentWalletInfo = createCurrrentWalletInfo(stores);

const { delegationTransaction } = stores.substores.ada;
const delegationTxResult = delegationTransaction.createDelegationTx.result;
const delegationTxError = delegationTransaction.createDelegationTx.error;

const outlet = (
<Suspense fallback={null}>
<Outlet />
</Suspense>
);

return (
<CurrencyProvider currency={unitOfAccount.currency || 'USD'}>
<GovernanceContextProvider
Expand All @@ -402,9 +433,7 @@ const GovernanceSubpages = ({ stores }) => {
triggerBuySellAdaDialog={() => stores.uiDialogs.open({ dialog: BuySellDialog })}
getCurrentPrice={stores.coinPriceStore.getCurrentPrice}
>
<Suspense fallback={null}>
<Outlet />
</Suspense>
{wrapOutlet ? wrapOutlet(outlet) : outlet}
</GovernanceContextProvider>
</CurrencyProvider>
);
Expand Down
1 change: 1 addition & 0 deletions packages/yoroi-extension/app/UI/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum BannerType {
BuyAda = 'buyAdaBanner',
Bring = 'bringBanner',
Usda = 'usdaBanner',
Rewards = 'rewardsBanner',
}

export const SUPPORT_CRISP_CHATBOX_URL = 'https://emurgo.github.io/yoroi-crisp-support/';
Expand Down
23 changes: 20 additions & 3 deletions packages/yoroi-extension/app/UI/common/hooks/useBannersQueue.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { useState, useEffect, useCallback } from 'react';
import LocalStorageApi from '../../../api/localStorage';
import { BannerType, DREP_BANNER_MIN_ADA } from '../constants';
import { useLocation } from 'react-router';

export function useBannerQueue({ bannersRemoteConfig, walletBalance }) {
export function useBannerQueue({ bannersRemoteConfig, walletBalance, walletId, currentlyDelegating, governanceStatus }) {
const localStorage = new LocalStorageApi();
const [visible, setVisible] = useState<BannerType | null>(null);
const [evaluationKey, setEvaluationKey] = useState(0);
const location = useLocation();

const resolveBanner = useCallback(async () => {
if (governanceStatus.status === 'none' && walletBalance > 5 && bannersRemoteConfig?.earnRewardsWithYoroi?.display === true) {
return BannerType.Rewards;
}
if (
(await localStorage.getMidnightBannerPhase2Closed()) === undefined &&
bannersRemoteConfig?.midnightPhase2Announcement.display === true
Expand All @@ -22,14 +27,26 @@ export function useBannerQueue({ bannersRemoteConfig, walletBalance }) {
}

return null;
}, [bannersRemoteConfig, walletBalance]);
}, [bannersRemoteConfig, governanceStatus.status, walletBalance, currentlyDelegating, location.search]);

useEffect(() => {
resolveBanner().then(setVisible);
}, [bannersRemoteConfig, walletBalance, evaluationKey, resolveBanner]);
}, [
bannersRemoteConfig,
walletId,
walletBalance,
evaluationKey,
resolveBanner,
currentlyDelegating,
location.search,
governanceStatus.status,
]);

const dismiss = async type => {
switch (type) {
case BannerType.Rewards:
setVisible(null);
break;
case BannerType.MidnightPhase2:
setVisible(null);
await localStorage.setMidnightBannerPhase2Closed('true');
Expand Down
16 changes: 16 additions & 0 deletions packages/yoroi-extension/app/UI/common/hooks/useStrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ export const messages = Object.freeze(
id: 'staking.dialog.updateDetails',
defaultMessage: '!!!EMURGO is updating the margin fee on its stakepools as a part of a broader 2026 modernization effort.',
},
earnRewards: {
id: 'banners.rewards.earn',
defaultMessage: '!!! Earn Rewards with Yoroi',
},
delegateRewards: {
id: 'banners.rewards.delegate',
defaultMessage:
'!!! Delegate your ADA to our stake pool and DRep in one step. Support Cardano governance, strengthen the network, and earn rewards along the way.',
},
rewardsButton: {
id: 'banners.rewards.button',
defaultMessage: '!!! Earn ADA',
},
})
);

Expand Down Expand Up @@ -177,5 +190,8 @@ export const useStrings = () => {
stakingUpdates: intl.formatMessage(messages.stakingUpdates),
upcomingUpdate: intl.formatMessage(messages.upcomingUpdate),
updateDetails: intl.formatMessage(messages.updateDetails),
earnRewards: intl.formatMessage(messages.earnRewards),
delegateRewards: intl.formatMessage(messages.delegateRewards),
rewardsButton: intl.formatMessage(messages.rewardsButton),
}).current;
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,30 @@ import { BannerType } from '../../common/constants';
import { BringBanner } from './BringBanner';
import { UsdaBanner } from './UsdaBanner';
import { MidnightPhase2Banner } from './MidnightPhase2Banner';
import { RewardsBanner } from './RewardsBanner';
import { observer } from 'mobx-react';

export const BannerVisibilityManager = ({ stores, intl }) => {
import { useGovernanceStatusState } from '../../features/governace/common/hooks/useGovernanceStatusState';

export const BannerVisibilityManager = observer(({ stores, intl }) => {
const selectedWallet = stores.wallets.selectedOrFail;
const currentlyDelegating = stores.delegation.isCurrentlyDelegating(selectedWallet.publicDeriverId);
const { governanceStatus } = useGovernanceStatusState();

const { data } = useYoroiRemoteConfig();
const { visible, dismiss } = useBannerQueue({
walletBalance: Number(
selectedWallet.balance.getDefaultEntry().amount.shiftedBy(-primaryTokenInfoMainnet.decimals).toString()
),
bannersRemoteConfig: data?.banners,
currentlyDelegating,
walletId: selectedWallet.publicDeriverId,
governanceStatus,
});

return (
<>
{visible === BannerType.Rewards && <RewardsBanner stores={stores} onClose={() => dismiss(BannerType.Rewards)} />}
{visible === BannerType.MidnightPhase2 && <MidnightPhase2Banner onClose={() => dismiss(BannerType.MidnightPhase2)} />}
{visible === BannerType.DRep && (
<DrepPromotionBanner onClose={() => dismiss(BannerType.DRep)} stores={stores} intl={intl} />
Expand All @@ -35,4 +46,4 @@ export const BannerVisibilityManager = ({ stores, intl }) => {
{visible === BannerType.Usda && <UsdaBanner onClose={() => dismiss(BannerType.DRep)} />}
</>
);
};
});
28 changes: 17 additions & 11 deletions packages/yoroi-extension/app/UI/components/Banners/BaseBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface BaseBannerProps {
illustration?: React.ReactNode;
illustrationProps?: StackProps;
displayIllustration?: boolean;
noClose?: boolean;
customButton?: React.ReactNode;
}

export const BaseBanner = ({
Expand All @@ -30,6 +32,8 @@ export const BaseBanner = ({
illustration,
illustrationProps,
displayIllustration = true,
noClose = false,
customButton,
}: BaseBannerProps) => {
const theme: any = useTheme();

Expand All @@ -39,16 +43,18 @@ export const BaseBanner = ({

return (
<Container direction="row" justifyContent="space-between" sx={{ position: 'relative', flex: 1 }}>
<Stack sx={{ position: 'absolute', zIndex: 20, right: 10, top: 10 }}>
<IconWrapper
buttonProps={{ onClick: handleClose }}
icon={Icons.CloseCircleIcon}
iconProps={{ fill: theme.palette.ds.el_gray_max }}
color="ds.el_gray_max"
borderColor="ds.el_gray_max"
asButton
/>
</Stack>
{!noClose && (
<Stack sx={{ position: 'absolute', zIndex: 20, right: 10, top: 10 }}>
<IconWrapper
buttonProps={{ onClick: handleClose }}
icon={Icons.CloseCircleIcon}
iconProps={{ fill: theme.palette.ds.el_gray_max }}
color="ds.el_gray_max"
borderColor="ds.el_gray_max"
asButton
/>
</Stack>
)}
<Stack direction="column" p="16px" alignItems="flex-start">
{typeof title === 'string' ? (
<Typography fontSize="16px" fontWeight={500} color="ds.gray_max">
Expand All @@ -60,7 +66,7 @@ export const BaseBanner = ({
<Typography variant="body1" mt="8px" mb={buttonText ? '24px' : '0px'} color="ds.gray_max">
{description}
</Typography>
{buttonText && <Button {...buttonProps}>{buttonText}</Button>}
{customButton ? customButton : buttonText && <Button {...buttonProps}>{buttonText}</Button>}
</Stack>
{displayIllustration && (
<Stack height={125} {...illustrationProps}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useStrings } from '../../common/hooks/useStrings';
import { DelegateButton } from '../../features/staking/useCases/DelegatedStakePoolInfo/DelegateButton';
import { BaseBanner } from './BaseBanner';
import type { StoresMap } from '../../../stores';
import { useYoroiRemoteConfig } from '../../common/hooks/useYoroiRemoteConfig';

interface UsdaBannerProps {
onClose?: () => void;
onClick?: () => void;
displayIllustration?: boolean;
stores: StoresMap;
}

export const RewardsBanner = ({ onClose, stores, displayIllustration = false }: UsdaBannerProps) => {
const { rewardsButton, earnRewards, delegateRewards } = useStrings();
const { data } = useYoroiRemoteConfig();
const yoroiPoolID = data?.banners?.earnRewardsWithYoroi?.poolId;
const yoroiPoolName = data?.banners?.earnRewardsWithYoroi?.poolName;
// TODO: read from config when merged
const yoroiDrepID = '220655f3a1c76788d839212adc459b188b84e680f30ae944c593fa18ae';

const handleClose = () => {
onClose && onClose();
};

return (
<BaseBanner
noClose
onClose={handleClose}
title={earnRewards}
description={delegateRewards}
buttonText={rewardsButton}
buttonProps={{
onClick: () => {},
// @ts-ignore
variant: 'secondary',
sx: {
width: 'fit-content',
height: '40px',
'&.MuiButton-sizeMedium': {
p: '9px 20px',
},
},
}}
displayIllustration={displayIllustration}
customButton={
<DelegateButton
btnVariant="secondary"
stores={stores}
label={rewardsButton}
disabled={false}
poolID={yoroiPoolID || ''}
poolName={yoroiPoolName || ''}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty pool ID passed to delegation on mainnet

RewardsBanner passes poolID={yoroiPoolID || ''} which converts undefined remote config values to empty strings. In DelegateButton, the expression isTestnet ? (poolID ?? fallback) : poolID only uses nullish coalescing (??) for testnet, which doesn't catch empty strings. On mainnet, if the remote config lacks poolId, an empty string is passed to createDelegationTransaction, which will likely fail. The || operator in RewardsBanner and ?? operator in DelegateButton handle edge cases inconsistently.

Additional Locations (1)

Fix in Cursor Fix in Web

dRepID={yoroiDrepID || ''}
socialMediaInfo={undefined}
delegateAndStake
/>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useNavigate } from 'react-router';
import React from 'react';
import { ROUTES } from '../../../../../routes-config';

export const useNavigateTo = () => {
const navigate = useNavigate();

return React.useRef({
selectRevampStatus: () => navigate(ROUTES.GOVERNANCE.ROOT),
}).current;
};
Loading
Loading