diff --git a/app/components/UI/Carousel/__snapshots__/index.test.tsx.snap b/app/components/UI/Carousel/__snapshots__/index.test.tsx.snap index 87ea76f6bb50..5980eef69de2 100644 --- a/app/components/UI/Carousel/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Carousel/__snapshots__/index.test.tsx.snap @@ -23,6 +23,19 @@ exports[`Carousel should only render fund banner when all banners are dismissed + + + + + + + + + + banner.solana.title + + + banner.solana.subtitle + + + + + +  + + + + + + `; @@ -484,6 +699,19 @@ exports[`Carousel should render correctly 1`] = ` + + + + + + + + + + banner.solana.title + + + banner.solana.subtitle + + + + + +  + + + + + + `; diff --git a/app/components/UI/Carousel/constants.ts b/app/components/UI/Carousel/constants.ts index 8808cc6146c7..a7e5086383d9 100644 --- a/app/components/UI/Carousel/constants.ts +++ b/app/components/UI/Carousel/constants.ts @@ -14,8 +14,37 @@ import aggregatedImage from '../../../images/banners/banner_image_aggregated.png ///: BEGIN:ONLY_INCLUDE_IF(multi-srp) import multiSrpImage from '../../../images/banners/banner_image_multisrp.png'; ///: END:ONLY_INCLUDE_IF +///: BEGIN:ONLY_INCLUDE_IF(solana) +import solanaImage from '../../../images/banners/banner_image_solana.png'; +import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; +import { SolScope } from '@metamask/keyring-api'; +///: END:ONLY_INCLUDE_IF export const PREDEFINED_SLIDES: CarouselSlide[] = [ + ///: BEGIN:ONLY_INCLUDE_IF(solana) + { + id: 'solana', + title: strings('banner.solana.title'), + description: strings('banner.solana.subtitle'), + undismissable: false, + navigation: { + type: 'function', + navigate: () => [ + Routes.MODAL.ROOT_MODAL_FLOW, + { + screen: Routes.SHEET.ADD_ACCOUNT, + params: { + clientType: WalletClientType.Solana, + scope: SolScope.Mainnet, + }, + }, + ], + }, + testID: WalletViewSelectorsIDs.CAROUSEL_SIXTH_SLIDE, + testIDTitle: WalletViewSelectorsIDs.CAROUSEL_SIXTH_SLIDE_TITLE, + testIDCloseButton: WalletViewSelectorsIDs.CAROUSEL_SIXTH_SLIDE_CLOSE_BUTTON, + }, + ///: END:ONLY_INCLUDE_IF { id: 'card', title: strings('banner.card.title'), @@ -94,4 +123,7 @@ export const BANNER_IMAGES: Record = { ///: BEGIN:ONLY_INCLUDE_IF(multi-srp) multisrp: multiSrpImage, ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(solana) + solana: solanaImage, + ///: END:ONLY_INCLUDE_IF }; diff --git a/app/components/UI/Carousel/index.test.tsx b/app/components/UI/Carousel/index.test.tsx index abc5e12302cc..ded8ca62e7cc 100644 --- a/app/components/UI/Carousel/index.test.tsx +++ b/app/components/UI/Carousel/index.test.tsx @@ -5,6 +5,8 @@ import { Linking } from 'react-native'; import Carousel from './'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; import { backgroundState } from '../../../util/test/initial-root-state'; +import { SolAccountType } from '@metamask/keyring-api'; +import Engine from '../../../core/Engine'; jest.mock('../../../core/Engine', () => ({ getTotalEvmFiatAccountBalance: jest.fn(), @@ -47,6 +49,7 @@ jest.mock('@react-navigation/native', () => ({ jest.mock('../../../core/Engine', () => ({ getTotalEvmFiatAccountBalance: jest.fn(), + setSelectedAddress: jest.fn(), })); const selectShowFiatInTestnets = jest.fn(); @@ -255,4 +258,73 @@ describe('Carousel', () => { expect(flatList).toBeTruthy(); }); + + it('does not render solana banner if user has a solana account', () => { + (useSelector as jest.Mock).mockImplementation((selector) => + selector({ + banners: { + dismissedBanners: [], + }, + engine: { + backgroundState: { + ...backgroundState, + AccountsController: { + internalAccounts: { + selectedAccount: '1', + accounts: { + '1': { + address: '0xSomeAddress', + type: SolAccountType.DataAccount, + }, + }, + }, + }, + }, + }, + }), + ); + + const { queryByTestId } = render(); + const solanaBanner = queryByTestId( + WalletViewSelectorsIDs.CAROUSEL_SIXTH_SLIDE, + ); + expect(solanaBanner).toBeNull(); + }); + + it('changes to a solana address if user has a solana account', async () => { + (useSelector as jest.Mock).mockImplementation((selector) => + selector({ + banners: { + dismissedBanners: [], + }, + engine: { + backgroundState: { + ...backgroundState, + AccountsController: { + internalAccounts: { + selectedAccount: '1', + accounts: { + '1': { + address: '0xSomeAddress', + }, + '2': { + address: 'SomeSolanaAddress', + type: SolAccountType.DataAccount, + }, + }, + }, + }, + }, + }, + }), + ); + + const { getByTestId } = render(); + const solanaBanner = getByTestId( + WalletViewSelectorsIDs.CAROUSEL_SIXTH_SLIDE, + ); + fireEvent.press(solanaBanner); + + expect(Engine.setSelectedAddress).toHaveBeenCalledWith('SomeSolanaAddress'); + }); }); diff --git a/app/components/UI/Carousel/index.tsx b/app/components/UI/Carousel/index.tsx index 9b6e3c659bb8..3ef0c4ee9528 100644 --- a/app/components/UI/Carousel/index.tsx +++ b/app/components/UI/Carousel/index.tsx @@ -23,6 +23,14 @@ import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletV import { PREDEFINED_SLIDES, BANNER_IMAGES } from './constants'; import { useStyles } from '../../../component-library/hooks'; import { selectDismissedBanners } from '../../../selectors/banner'; +///: BEGIN:ONLY_INCLUDE_IF(solana) +import { + selectSelectedInternalAccount, + selectLastSelectedSolanaAccount, +} from '../../../selectors/accountsController'; +import { SolAccountType } from '@metamask/keyring-api'; +import Engine from '../../../core/Engine'; +///: END:ONLY_INCLUDE_IF export const Carousel: FC = ({ style }) => { const [selectedIndex, setSelectedIndex] = useState(0); @@ -34,6 +42,12 @@ export const Carousel: FC = ({ style }) => { const { navigate } = useNavigation(); const { styles } = useStyles(styleSheet, { style }); const dismissedBanners = useSelector(selectDismissedBanners); + ///: BEGIN:ONLY_INCLUDE_IF(solana) + const selectedAccount = useSelector(selectSelectedInternalAccount); + const lastSelectedSolanaAccount = useSelector( + selectLastSelectedSolanaAccount, + ); + ///: END:ONLY_INCLUDE_IF const isZeroBalance = selectedAccountMultichainBalance?.totalFiatBalance === 0; @@ -57,14 +71,29 @@ export const Carousel: FC = ({ style }) => { const visibleSlides = useMemo( () => slidesConfig.filter((slide) => { + ///: BEGIN:ONLY_INCLUDE_IF(solana) + if ( + slide.id === 'solana' && + selectedAccount?.type === SolAccountType.DataAccount + ) { + return false; + } + ///: END:ONLY_INCLUDE_IF + if (slide.id === 'fund' && isZeroBalance) { return true; } return !dismissedBanners.includes(slide.id); }), - [slidesConfig, isZeroBalance, dismissedBanners], + [ + slidesConfig, + isZeroBalance, + dismissedBanners, + ///: BEGIN:ONLY_INCLUDE_IF(solana) + selectedAccount, + ///: END:ONLY_INCLUDE_IF + ], ); - const isSingleSlide = visibleSlides.length === 1; const openUrl = @@ -76,15 +105,33 @@ export const Carousel: FC = ({ style }) => { const handleSlideClick = useCallback( (slideId: string, navigation: NavigationAction) => { + const extraProperties: Record = {}; + + ///: BEGIN:ONLY_INCLUDE_IF(solana) + const isSolanaBanner = slideId === 'solana'; + if (isSolanaBanner && lastSelectedSolanaAccount) { + extraProperties.action = 'redirect-solana-account'; + } else if (isSolanaBanner && !lastSelectedSolanaAccount) { + extraProperties.action = 'create-solana-account'; + } + ///: END:ONLY_INCLUDE_IF + trackEvent( createEventBuilder({ category: 'Banner Select', properties: { name: slideId, + ...extraProperties, }, }).build(), ); + ///: BEGIN:ONLY_INCLUDE_IF(solana) + if (isSolanaBanner && lastSelectedSolanaAccount) { + return Engine.setSelectedAddress(lastSelectedSolanaAccount.address); + } + ///: END:ONLY_INCLUDE_IF + if (navigation.type === 'url') { return openUrl(navigation.href)(); } @@ -97,7 +144,14 @@ export const Carousel: FC = ({ style }) => { return navigate(navigation.route); } }, - [navigate, trackEvent, createEventBuilder], + [ + trackEvent, + createEventBuilder, + navigate, + ///: BEGIN:ONLY_INCLUDE_IF(solana) + lastSelectedSolanaAccount, + ///: END:ONLY_INCLUDE_IF + ], ); const handleClose = useCallback( diff --git a/app/components/UI/Carousel/types.ts b/app/components/UI/Carousel/types.ts index 3ea1a399f6ad..427f2cf9ffa0 100644 --- a/app/components/UI/Carousel/types.ts +++ b/app/components/UI/Carousel/types.ts @@ -1,12 +1,21 @@ import { ViewStyle } from 'react-native'; - -export type SlideId = 'card' | 'fund' | 'cashout' | 'aggregated' | 'multisrp'; +import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; +import { CaipChainId } from '@metamask/utils'; +export type SlideId = + | 'card' + | 'fund' + | 'cashout' + | 'aggregated' + | 'multisrp' + | 'solana'; interface NavigationParams { address?: string; chainId?: string; amount?: string; currency?: string; + clientType?: WalletClientType; + scope?: CaipChainId; } interface NavigationScreen { diff --git a/app/images/banners/banner_image_solana.png b/app/images/banners/banner_image_solana.png new file mode 100644 index 000000000000..b7381e3593f6 Binary files /dev/null and b/app/images/banners/banner_image_solana.png differ diff --git a/app/lib/snaps/preinstalled-snaps.ts b/app/lib/snaps/preinstalled-snaps.ts index 452d61bf75e7..cb75ba148b5a 100644 --- a/app/lib/snaps/preinstalled-snaps.ts +++ b/app/lib/snaps/preinstalled-snaps.ts @@ -1,6 +1,6 @@ import type { PreinstalledSnap } from '@metamask/snaps-controllers'; import MessageSigningSnap from '@metamask/message-signing-snap/dist/preinstalled-snap.json'; -///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +///: BEGIN:ONLY_INCLUDE_IF(solana) import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(bitcoin) @@ -9,7 +9,7 @@ import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-s const PREINSTALLED_SNAPS: readonly PreinstalledSnap[] = Object.freeze([ MessageSigningSnap as unknown as PreinstalledSnap, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + ///: BEGIN:ONLY_INCLUDE_IF(solana) SolanaWalletSnap as unknown as PreinstalledSnap, ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(bitcoin) diff --git a/e2e/selectors/wallet/WalletView.selectors.js b/e2e/selectors/wallet/WalletView.selectors.js index c6ee77f5838c..94d8eae65af0 100644 --- a/e2e/selectors/wallet/WalletView.selectors.js +++ b/e2e/selectors/wallet/WalletView.selectors.js @@ -79,6 +79,9 @@ export const WalletViewSelectorsIDs = { CAROUSEL_FIFTH_SLIDE: 'carousel-fifth-slide', CAROUSEL_FIFTH_SLIDE_TITLE: 'carousel-fifth-slide-title', CAROUSEL_FIFTH_SLIDE_CLOSE_BUTTON: 'carousel-fifth-slide-close-button', + CAROUSEL_SIXTH_SLIDE: 'carousel-sixth-slide', + CAROUSEL_SIXTH_SLIDE_TITLE: 'carousel-sixth-slide-title', + CAROUSEL_SIXTH_SLIDE_CLOSE_BUTTON: 'carousel-sixth-slide-close-button', CAROUSEL_PROGRESS_DOTS: 'progress-dots', CAROUSEL_SLIDE: 'carousel-slide', }; diff --git a/locales/languages/en.json b/locales/languages/en.json index e316b8856a55..17c97b926a16 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -446,6 +446,10 @@ "multisrp": { "title": "Add multiple Secret Recovery Phrases", "subtitle": "Import and use multiple wallets in MetaMask" + }, + "solana": { + "title": "Solana is now supported", + "subtitle": "Create a Solana account to get started" } }, "wallet": { diff --git a/metro.transform.js b/metro.transform.js index 3d01299fa2e0..4d755ddff7b2 100644 --- a/metro.transform.js +++ b/metro.transform.js @@ -20,10 +20,17 @@ const availableFeatures = new Set([ 'keyring-snaps', 'multi-srp', 'bitcoin', + 'solana', ]); const mainFeatureSet = new Set(['preinstalled-snaps', 'multi-srp']); -const betaFeatureSet = new Set(['beta', 'preinstalled-snaps', 'keyring-snaps', 'multi-srp']); +const betaFeatureSet = new Set([ + 'beta', + 'preinstalled-snaps', + 'keyring-snaps', + 'multi-srp', + 'solana', +]); const flaskFeatureSet = new Set([ 'flask', 'preinstalled-snaps', @@ -31,6 +38,7 @@ const flaskFeatureSet = new Set([ 'keyring-snaps', 'multi-srp', 'bitcoin', + 'solana', ]); /**