From b73f543b237b021aa1f748a3b6600933a559cf2e Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Wed, 28 Apr 2021 14:21:41 -0230 Subject: [PATCH] Whats new popup (#10583) * Add 'What's New' notification popup * Move selectors from shared/notifications into ui/ directory * Use keys for localized message in whats new notifications objects, to ensure notifications will be translated. * Remove unused swaps intro popup locale messages * Fix keys of whats new notification locales * Remove notifications messages and descriptions from comment in shared/notifications * Move notifcationActionFunctions to shared/notifications and make it stateless * Get notification data from constants instead of state in whats-new-popup * Code cleanup * Fix build quote reference to swapsEthToken, broken during rebase * Rename notificationFilters to notificationToExclude to clarify its purpose * Documentation for getSortedNotificationsToShow * Move notification action functions from shared/ to whats-new-popup.js * Stop setting swapsWelcomeMessageHasBeenShown to state in app-state controller * Update e2e tests for whats new popup changes * Updating migration files * Addressing feedback part 1 * Addressing feedback part 2 * Remove unnecessary div in whats-new-popup * Change getNotificationsToExclude to getNotificationsToInclude for use in the getSortedNotificationsToShow selector * Delete intro-popup directory and test files * Lint fix * Add notifiction state to address-entry fixture * Use two separate functions for rendering first and subsequent notifications in the whats-new-popup * Ensure that string literals are passed to t for whats new popup text * Update import-ui fixtures to include notificaiton controller state * Remove unnecessary, accidental change confirm-approve * Remove swaps notification in favour of mobile swaps as first notifcation and TBD 3rd notification * Update whats-new-popup to use intersection observer api to detect if notification has been seen * Add notifications to send-edit and threebox e2e test fixtures * Update ui/app/selectors/selectors.js Co-authored-by: Mark Stacey * Update ui/app/selectors/selectors.js Co-authored-by: Mark Stacey * Clean up locale code for whats-new-popup notifications * Disconnect observers in whats-new-popup when their callback is first called * Add test case for migration 58 for when the AppStateController does not exist * Rename popover components containerRef to popoverWrapRef * Fix messages.json * Update notification messages and images * Rename popoverWrapRef -> popoverRef in whats-new-popup and popover.component * Only create one observer, and only after images have loaded, in whats-new-popup * Set width and height on whats-new-popup image, instead of setting state on img load * Update ui/app/components/app/whats-new-popup/whats-new-popup.js Co-authored-by: Mark Stacey * Code clean up in whats new popup re: notification rendering and action functions * Code cleanup in render notification functions of whats-new-popup * Update ui/app/components/app/whats-new-popup/whats-new-popup.js Co-authored-by: Mark Stacey * lint fix * Update and localize notification dates * Clean up date code in shred/notifications/index.js Co-authored-by: ryanml Co-authored-by: Mark Stacey --- app/_locales/en/messages.json | 57 ++++-- app/_locales/es/messages.json | 21 -- app/_locales/es_419/messages.json | 21 -- app/_locales/hi/messages.json | 21 -- app/_locales/id/messages.json | 21 -- app/_locales/it/messages.json | 21 -- app/_locales/ja/messages.json | 21 -- app/_locales/ko/messages.json | 21 -- app/_locales/ru/messages.json | 21 -- app/_locales/tl/messages.json | 21 -- app/_locales/vi/messages.json | 21 -- app/_locales/zh_CN/messages.json | 21 -- app/images/mobile-link-qr.svg | 1 + app/scripts/controllers/app-state.js | 10 - app/scripts/metamask-controller.js | 20 +- app/scripts/migrations/058.js | 23 +++ app/scripts/migrations/058.test.js | 46 +++++ app/scripts/migrations/index.js | 1 + shared/notifications/index.js | 51 +++++ test/e2e/fixtures/address-entry/state.json | 13 ++ test/e2e/fixtures/connected-state/state.json | 16 +- test/e2e/fixtures/import-ui/state.json | 13 ++ test/e2e/fixtures/imported-account/state.json | 16 +- test/e2e/fixtures/localization/state.json | 16 +- test/e2e/fixtures/metrics-enabled/state.json | 16 +- test/e2e/fixtures/send-edit/state.json | 13 ++ test/e2e/fixtures/threebox-enabled/state.json | 13 ++ test/e2e/metamask-ui.spec.js | 18 ++ test/e2e/tests/from-import-ui.spec.js | 5 + test/e2e/tests/incremental-security.spec.js | 7 + ui/app/components/app/app-components.scss | 1 + .../components/app/whats-new-popup/index.js | 1 + .../components/app/whats-new-popup/index.scss | 51 +++++ .../app/whats-new-popup/whats-new-popup.js | 179 ++++++++++++++++++ ui/app/components/ui/popover/index.scss | 4 + .../ui/popover/popover.component.js | 14 +- ui/app/ducks/app/app.js | 13 ++ ui/app/pages/home/home.component.js | 20 +- ui/app/pages/home/home.container.js | 16 +- ui/app/pages/routes/routes.component.js | 4 +- ui/app/pages/swaps/index.scss | 1 - .../__snapshots__/intro-popup.test.js.snap | 9 - ui/app/pages/swaps/intro-popup/index.js | 1 - ui/app/pages/swaps/intro-popup/index.scss | 71 ------- ui/app/pages/swaps/intro-popup/intro-popup.js | 108 ----------- .../swaps/intro-popup/intro-popup.test.js | 24 --- ui/app/selectors/selectors.js | 33 ++++ ui/app/store/actionConstants.js | 3 + ui/app/store/actions.js | 16 +- 49 files changed, 634 insertions(+), 521 deletions(-) create mode 100644 app/images/mobile-link-qr.svg create mode 100644 app/scripts/migrations/058.js create mode 100644 app/scripts/migrations/058.test.js create mode 100644 shared/notifications/index.js create mode 100644 ui/app/components/app/whats-new-popup/index.js create mode 100644 ui/app/components/app/whats-new-popup/index.scss create mode 100644 ui/app/components/app/whats-new-popup/whats-new-popup.js delete mode 100644 ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap delete mode 100644 ui/app/pages/swaps/intro-popup/index.js delete mode 100644 ui/app/pages/swaps/intro-popup/index.scss delete mode 100644 ui/app/pages/swaps/intro-popup/intro-popup.js delete mode 100644 ui/app/pages/swaps/intro-popup/intro-popup.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 656b950e5..f98bd83a4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1238,6 +1238,38 @@ "notEnoughGas": { "message": "Not Enough Gas" }, + "notifications1Description": { + "message": "MetaMask Mobile users can now swap tokens inside their mobile wallet. Scan the QR code to get the mobile app and start swapping.", + "description": "Description of a notification in the 'See What's New' popup. Describes the swapping on mobile feature." + }, + "notifications1Title": { + "message": "Swapping on mobile is here!", + "description": "Title for a notification in the 'See What's New' popup. Tells users that they can now use MetaMask Swaps on Mobile." + }, + "notifications2ActionText": { + "message": "Start survey", + "description": "The 'call to action' label on the button, or link, of the 'Help improve MetaMask' 'See What's New' notification. Upon clicking, users will be taken to an external page where they can complete a survey." + }, + "notifications2Description": { + "message": "Please share your experience in this 5 minute survey.", + "description": "Description of a notification in the 'See What's New' popup. Further clarifies how the users can help: by completing a 5 minute survey about MetaMask." + }, + "notifications2Title": { + "message": "Help improve MetaMask", + "description": "Title for a notification in the 'See What's New' popup. Asks users to take action to make MetaMask better." + }, + "notifications3ActionText": { + "message": "Read more", + "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website." + }, + "notifications3Description": { + "message": "Stay up to date on MetaMask security best practices and get the latest security tips from official MetaMask support.", + "description": "Description of a notification in the 'See What's New' popup. Describes the information they can get on security from the linked support page." + }, + "notifications3Title": { + "message": "Stay secure", + "description": "Title for a notification in the 'See What's New' popup. Encourages users to consider security." + }, "ofTextNofM": { "message": "of" }, @@ -1817,24 +1849,6 @@ "swapHighSlippageWarning": { "message": "Slippage amount is very high. Make sure you know what you are doing!" }, - "swapIntroLearnMoreHeader": { - "message": "Want to learn more?" - }, - "swapIntroLearnMoreLink": { - "message": "Learn more about MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Liquidity sources include:" - }, - "swapIntroPopupSubTitle": { - "message": "You can now swap tokens directly in your MetaMask wallet. MetaMask Swaps combines multiple decentralized exchange aggregators, professional market makers, and individual DEXs to ensure MetaMask users always get the best price with the lowest network fees." - }, - "swapIntroPopupTitle": { - "message": "Token swapping is here!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Review our official contracts audit" - }, "swapLowSlippageError": { "message": "Transaction may fail, max slippage too low." }, @@ -1961,9 +1975,6 @@ "swapSourceInfo": { "message": "We search multiple liquidity sources (exchanges, aggregators and professional market makers) to find the best rates and lowest network fees." }, - "swapStartSwapping": { - "message": "Start swapping" - }, "swapSwapFrom": { "message": "Swap from" }, @@ -2310,6 +2321,10 @@ "welcomeBack": { "message": "Welcome Back!" }, + "whatsNew": { + "message": "What's new", + "description": "This is the title of a popup that gives users notifications about new features and updates to MetaMask." + }, "whatsThis": { "message": "What's this?" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 1731f13cd..c85673d74 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1579,24 +1579,6 @@ "swapHighSlippageWarning": { "message": "La cantidad de deslizamiento es muy alta. ¡Asegúrate de saber lo que estás haciendo!" }, - "swapIntroLearnMoreHeader": { - "message": "¿Quiere aprender más?" - }, - "swapIntroLearnMoreLink": { - "message": "Más información sobre los Intercambios MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Las fuentes de liquidez incluyen:" - }, - "swapIntroPopupSubTitle": { - "message": "Ahora puede intercambiar tokens directamente en su monedero MetaMask. Intercambios MetaMask combina múltiples agregadores de intercambio descentralizados, creadores de mercado profesionales y DEX individuales para garantizar que los usuarios de MetaMask siempre obtengan el mejor precio con las tarifas de red más bajas." - }, - "swapIntroPopupTitle": { - "message": "¡El intercambio de tokens está aquí!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Revise nuestra auditoría de contratos oficiales" - }, "swapLowSlippageError": { "message": "La transacción puede fallar, el deslizamiento máximo es demasiado bajo." }, @@ -1717,9 +1699,6 @@ "swapSourceInfo": { "message": "Buscamos múltiples fuentes de liquidez (exchanges, agregadores y creadores de mercado profesionales) para encontrar las mejores tarifas y las tarifas de red más bajas." }, - "swapStartSwapping": { - "message": "Comenzar intercambio" - }, "swapSwapFrom": { "message": "Intercambiar desde" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index b9073e5e6..1fe79aad6 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1579,24 +1579,6 @@ "swapHighSlippageWarning": { "message": "La cantidad de deslizamiento es muy alta. ¡Asegúrate de saber lo que estás haciendo!" }, - "swapIntroLearnMoreHeader": { - "message": "¿Quiere aprender más?" - }, - "swapIntroLearnMoreLink": { - "message": "Más información sobre los Intercambios MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Las fuentes de liquidez incluyen:" - }, - "swapIntroPopupSubTitle": { - "message": "Ahora puede intercambiar tokens directamente en su billetera MetaMask. Intercambios MetaMask combina múltiples agregadores de intercambio descentralizados, creadores de mercado profesionales y DEX individuales para garantizar que los usuarios de MetaMask siempre obtengan el mejor precio con las tarifas de red más bajas." - }, - "swapIntroPopupTitle": { - "message": "¡El intercambio de tokens está aquí!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Revise nuestra auditoría de contratos oficiales" - }, "swapLowSlippageError": { "message": "La transacción puede fallar, el deslizamiento máximo es demasiado bajo." }, @@ -1717,9 +1699,6 @@ "swapSourceInfo": { "message": "Buscamos múltiples fuentes de liquidez (exchanges, agregadores y creadores de mercado profesionales) para encontrar las mejores tarifas y las tarifas de red más bajas." }, - "swapStartSwapping": { - "message": "Comenzar intercambio" - }, "swapSwapFrom": { "message": "Intercambiar desde" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 52118ac28..c222c9c13 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1567,24 +1567,6 @@ "swapHighSlippageWarning": { "message": "स्लिपेज राशि बहुत अधिक है। सुनिश्चित करें कि आप जानते हैं कि आप क्या कर रहे हैं!" }, - "swapIntroLearnMoreHeader": { - "message": "अधिक सीखना चाहते हैं?" - }, - "swapIntroLearnMoreLink": { - "message": "MetaMask स्वैप के बारे में अधिक जानें" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "चलनिधि स्रोतों में निम्न शामिल हैं:" - }, - "swapIntroPopupSubTitle": { - "message": "अब आप अपने MetaMask वॉलेट में सीधे टोकन स्वैप कर सकते हैं। MetaMask स्वैप कई विकेंद्रीकृत विनिमय एग्रीगेटर, पेशेवर बाजार निर्माताओं और व्यक्तिगत DEX को जोड़ता है, ताकि MetaMask उपयोगकर्ताओं को हमेशा सबसे कम नेटवर्क शुल्क के साथ सबसे अच्छा मूल्य मिल सके।" - }, - "swapIntroPopupTitle": { - "message": "टोकन स्वैपिंग यहाँ उपलब्ध है!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "हमारे आधिकारिक अनुबंधों के ऑडिट की समीक्षा करें" - }, "swapLowSlippageError": { "message": "लेनदेन विफल हो सकता है, अधिकतम स्लिपेज बहुत कम हो सकता है।" }, @@ -1687,9 +1669,6 @@ "swapSourceInfo": { "message": "हम सर्वोत्तम दरों और न्यूनतम नेटवर्क शुल्क का पता लगाने के लिए कई चलनिधि स्रोतों (एक्सचेंज, एग्रीगेटर और पेशेवर बाजार निर्माताओं) की खोज करते हैं।" }, - "swapStartSwapping": { - "message": "स्वैप करना शुरू करें" - }, "swapSwapFrom": { "message": "इससे स्वैप करें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 915813bc1..e497c2eb0 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1567,24 +1567,6 @@ "swapHighSlippageWarning": { "message": "Jumlah slippage sangat tinggi. Pastikan Anda mengetahui yang Anda kerjakan!" }, - "swapIntroLearnMoreHeader": { - "message": "Ingin mempelajari selengkapnya?" - }, - "swapIntroLearnMoreLink": { - "message": "Pelajari selengkapnya tentang Penukaran MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Sumber likuiditas mencakup:" - }, - "swapIntroPopupSubTitle": { - "message": "Sekarang, Anda bisa menukar token secara langsung di dompet MetaMask Anda. Penukaran MetaMask menggabungkan beberapa agregator penukaran terdesentralisasi, pembuat pasar profesional, dan DEX individu untuk memastikan pengguna MetaMask selalu mendapatkan harga terbaik dengan biaya jaringan terendah." - }, - "swapIntroPopupTitle": { - "message": "Penukaran token ada di sini!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Tinjau audit kontrak resmi kami" - }, "swapLowSlippageError": { "message": "Transaksi bisa gagal, slippage maks. terlalu rendah." }, @@ -1687,9 +1669,6 @@ "swapSourceInfo": { "message": "Kami mencari beberapa sumber likuiditas (penukaran, agregator, dan pembuat pasar profesional) untuk menemukan tarif terbaik dan biaya jaringan terendah." }, - "swapStartSwapping": { - "message": "Mulai menukar" - }, "swapSwapFrom": { "message": "Tukar dari" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 154eddc3e..a312707c4 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1585,24 +1585,6 @@ "swapHighSlippageWarning": { "message": "L'importo di slippage è molto alto. Assicurati di sapere cosa stai facendo!" }, - "swapIntroLearnMoreHeader": { - "message": "Vuoi sapere di più?" - }, - "swapIntroLearnMoreLink": { - "message": "Scopri di più su MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Sorgenti di liquidità incluse:" - }, - "swapIntroPopupSubTitle": { - "message": "Adesso puoi scambiare token direttamente dal tuo portafgolio MetaMask. MetaMask Swaps combina vari siti di scambio decentralizzati, aggregatori e market maker professionisti per assicurare che gli utenti di MetaMask ottengano sempre il miglior prezzo con le tasse di rete minori." - }, - "swapIntroPopupTitle": { - "message": "Lo scambio di token è qui!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Esamina l'audit ufficiale dei nostri smart contracts" - }, "swapLowSlippageError": { "message": "La transazione può fallire, il massimo slippage è troppo basso." }, @@ -1729,9 +1711,6 @@ "swapSourceInfo": { "message": "Cerchiamo sorgenti di liquidità multiple (siti di scambio, aggregatori, market maker professionisti) per trovare le tariffe migliori e le tasse di rete minori." }, - "swapStartSwapping": { - "message": "Inizia a scambiare" - }, "swapSwapFrom": { "message": "Scambia da" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 6410a5565..78d2132a2 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1579,24 +1579,6 @@ "swapHighSlippageWarning": { "message": "非常に大きいスリッページ額です。本当に実行するか確認してください。" }, - "swapIntroLearnMoreHeader": { - "message": "詳細を表示しますか?" - }, - "swapIntroLearnMoreLink": { - "message": "MetaMask Swapsの詳細" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "流動性ソースには以下が含まれます。" - }, - "swapIntroPopupSubTitle": { - "message": "トークンをMetaMaskで直接スワップできるようになりました。MetaMask Swapsは、多数の分散型取引所アグリゲーター、専門のマーケットメーカー、DEX取引所を統合し、ユーザーは常に最低のネットワーク手数料、最適な価格で取引できます。" - }, - "swapIntroPopupTitle": { - "message": "トークンのスワップはこちら!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "MetaSwapのコントラクト監査のレビュー" - }, "swapLowSlippageError": { "message": "トランザクションが失敗する可能性があります。最大スリッページが少なすぎます。" }, @@ -1717,9 +1699,6 @@ "swapSourceInfo": { "message": "最良のレートと最小のネットワーク手数料を探すため、複数の流動性ソース(取引所、アグリゲーター、専門のマーケットメーカー)を検索します。" }, - "swapStartSwapping": { - "message": "スワップの開始" - }, "swapSwapFrom": { "message": "スワップ元" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 1b1e6ce0d..5cd7c37fc 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1567,24 +1567,6 @@ "swapHighSlippageWarning": { "message": "슬리패지 금액이 아주 큽니다. 현재 어떤 작업을 하고 있는지 확인하세요!" }, - "swapIntroLearnMoreHeader": { - "message": "자세한 정보를 확인하고 싶으신가요?" - }, - "swapIntroLearnMoreLink": { - "message": "MetaMask Swaps에 대해 자세히 알아보기" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "다음을 포함한 유동성 소스:" - }, - "swapIntroPopupSubTitle": { - "message": "이제 MetaMask 지갑에서 토큰을 바로 스왑할 수 있습니다. MetaMask Swaps는 다양한 분산형 교환 애그리게이터, 투자전문기관, 개별 DEX를 결합하여 MetaMask 사용자가 언제든 최저 네트워크 요금으로 최상의 가격을 얻을 수 있게 합니다." - }, - "swapIntroPopupTitle": { - "message": "토큰 스왑은 여기서 진행됩니다!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "당사의 공식 계약 감사 검토" - }, "swapLowSlippageError": { "message": "거래가 실패할 수도 있습니다. 최대 슬리패지가 너무 낮습니다." }, @@ -1687,9 +1669,6 @@ "swapSourceInfo": { "message": "당사에서는 여러 유동성 소스(교환, 애그리게이터, 투자전문기관)를 검색하여 최상의 요율과 최저 네트워크 요금을 찾습니다." }, - "swapStartSwapping": { - "message": "스왑 시작" - }, "swapSwapFrom": { "message": "다음에서 스왑" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 91cbce494..7208b08fe 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1567,24 +1567,6 @@ "swapHighSlippageWarning": { "message": "Величина проскальзывания очень велика. Убедитесь, что вы знаете, что делаете!" }, - "swapIntroLearnMoreHeader": { - "message": "Хотите узнать больше?" - }, - "swapIntroLearnMoreLink": { - "message": "Подробнее о свопах MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Источники ликвидности включают:" - }, - "swapIntroPopupSubTitle": { - "message": "Теперь вы можете обменивать токены прямо в кошельке MetaMask. MetaMask Swaps объединяет несколько децентрализованных агрегаторов обменов, профессиональных торговцев и отдельные DEX, чтобы пользователи MetaMask всегда получали лучшую цену с минимальными комиссиями сети." - }, - "swapIntroPopupTitle": { - "message": "Обмен токенов здесь!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Ознакомьтесь с нашим официальным аудитом контрактов" - }, "swapLowSlippageError": { "message": "Транзакции могут завершиться неудачей, максимальное проскальзывание слишком мало." }, @@ -1687,9 +1669,6 @@ "swapSourceInfo": { "message": "Мы ищем несколько источников ликвидности (биржи, агрегаторы и профессиональные продавцы), чтобы найти лучшие цены и самые низкие сетевые комиссии." }, - "swapStartSwapping": { - "message": "Начать обмен" - }, "swapSwapFrom": { "message": "Своп с" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 4868042fa..70b7b8ad8 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1564,24 +1564,6 @@ "swapHighSlippageWarning": { "message": "Sobrang laki ng halaga ng slippage. Tiyaking alam mo ang ginagawa mo!" }, - "swapIntroLearnMoreHeader": { - "message": "Gusto mo bang matuto pa?" - }, - "swapIntroLearnMoreLink": { - "message": "Matuto pa tungkol sa MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Kasama sa mga pinagkunan ng liquidity ang:" - }, - "swapIntroPopupSubTitle": { - "message": "Puwede mo nang direktang i-swap ang mga token sa iyong MetaMask wallet. Pinagsasama-sama ng MetaMask Swaps ang maraming decentralized exchange aggregator, propesyonal na market maker, at indibidwal na DEX para matiyak na makukuha palagi ng mga user ng MetaMask ang pinakasulit na presyo nang may pinakamababang bayarin sa network." - }, - "swapIntroPopupTitle": { - "message": "Ito na ang pag-swap ng token!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Suriin ang aming audit ng mga opisyal na kontrata" - }, "swapLowSlippageError": { "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." }, @@ -1684,9 +1666,6 @@ "swapSourceInfo": { "message": "Naghahanap kami ng maraming pinagkukunan ng liquidity (mga exchange, aggregator at propesyonal na market maker) para mahanap ang mga pinakasulit na rate at pinakamababang bayarin sa network." }, - "swapStartSwapping": { - "message": "Simulang mag-swap" - }, "swapSwapFrom": { "message": "Ipalit mula sa" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index c6930cb5f..883c2f67d 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1567,24 +1567,6 @@ "swapHighSlippageWarning": { "message": "Số tiền trượt giá rất cao. Hãy chắc chắn rằng bạn hiểu những gì mình đang làm!" }, - "swapIntroLearnMoreHeader": { - "message": "Bạn muốn tìm hiểu thêm?" - }, - "swapIntroLearnMoreLink": { - "message": "Tìm hiểu thêm về MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Các nguồn thanh khoản bao gồm:" - }, - "swapIntroPopupSubTitle": { - "message": "Giờ đây bạn có thể hoán đổi token ngay trong ví MetaMask của mình. MetaMask Swaps quy tụ nhiều trình tổng hợp sàn giao dịch phi tập trung, các nhà tạo lập thị trường chuyên nghiệp và các sàn giao dịch phi tập trung dành cho cá nhân nhằm đảm bảo người dùng MetaMask luôn nhận được mức giá tốt nhất với phí mạng thấp nhất." - }, - "swapIntroPopupTitle": { - "message": "Tính năng hoán đổi token đã sẵn sàng!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Xem xét quy trình kiểm tra hợp đồng chính thức của chúng tôi" - }, "swapLowSlippageError": { "message": "Giao dịch có thể không thành công, mức trượt giá tối đa quá thấp." }, @@ -1687,9 +1669,6 @@ "swapSourceInfo": { "message": "Chúng tôi tìm kiếm nhiều nguồn thanh khoản (các sàn giao dịch, trình tổng hợp và nhà tạo lập thị trường) để tìm được mức tỷ lệ tốt nhất và phí mạng thấp nhất." }, - "swapStartSwapping": { - "message": "Bắt đầu hoán đổi" - }, "swapSwapFrom": { "message": "Hoán đổi từ" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 4c23a0bb6..1c0071709 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1579,24 +1579,6 @@ "swapHighSlippageWarning": { "message": "滑点数量非常大。确保您知道您的操作!" }, - "swapIntroLearnMoreHeader": { - "message": "想了解更多信息?" - }, - "swapIntroLearnMoreLink": { - "message": "了解更多关于 MetaMask Swap(兑换)" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "流动资金来源包括:" - }, - "swapIntroPopupSubTitle": { - "message": "现在您可以直接在 MetaMask 钱包中兑换代币。MetaMask Swaps(兑换)结合了多个去中心化交易所聚合商、专业做市商和个人 DEX,确保 MetaMask 用户始终以最低的网络费用获得最佳价格。" - }, - "swapIntroPopupTitle": { - "message": "代币兑换来了!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "查看我们的官方合约审计" - }, "swapLowSlippageError": { "message": "交易可能失败,最大滑点过低。" }, @@ -1717,9 +1699,6 @@ "swapSourceInfo": { "message": "我们搜索多个流动性来源(交易所、聚合商和专业做市商),以找到最好的利率和最低的网络手续费。" }, - "swapStartSwapping": { - "message": "开始兑换" - }, "swapSwapFrom": { "message": "兑换自" }, diff --git a/app/images/mobile-link-qr.svg b/app/images/mobile-link-qr.svg new file mode 100644 index 000000000..3141bbe5f --- /dev/null +++ b/app/images/mobile-link-qr.svg @@ -0,0 +1 @@ + diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 73173bf68..261b735dc 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -22,7 +22,6 @@ export default class AppStateController extends EventEmitter { this.store = new ObservableStore({ timeoutMinutes: 0, connectedStatusPopoverHasBeenShown: true, - swapsWelcomeMessageHasBeenShown: false, defaultHomeActiveTabName: null, ...initState, }); @@ -112,15 +111,6 @@ export default class AppStateController extends EventEmitter { }); } - /** - * Record that the user has seen the swap screen welcome message - */ - setSwapsWelcomeMessageHasBeenShown() { - this.store.updateState({ - swapsWelcomeMessageHasBeenShown: true, - }); - } - /** * Sets the last active time to the current time * @returns {void} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c5c181400..61291e6c4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -22,9 +22,12 @@ import { ApprovalController, CurrencyRateController, PhishingController, + NotificationController, } from '@metamask/controllers'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; +import { UI_NOTIFICATIONS } from '../../shared/notifications'; + import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; @@ -160,6 +163,11 @@ export default class MetamaskController extends EventEmitter { this.phishingController = new PhishingController(); + this.notificationController = new NotificationController( + { allNotifications: UI_NOTIFICATIONS }, + initState.NotificationController, + ); + // now we can initialize the RPC provider, which other controllers require this.initializeProvider(); this.provider = this.networkController.getProviderAndBlockTracker().provider; @@ -426,6 +434,7 @@ export default class MetamaskController extends EventEmitter { PermissionsController: this.permissionsController.permissions, PermissionsMetadata: this.permissionsController.store, ThreeBoxController: this.threeBoxController.store, + NotificationController: this.notificationController, }); this.memStore = new ComposableObservableStore(null, { @@ -454,6 +463,7 @@ export default class MetamaskController extends EventEmitter { SwapsController: this.swapsController.store, EnsController: this.ensController.store, ApprovalController: this.approvalController, + NotificationController: this.notificationController, }); this.memStore.subscribe(this.sendUpdate.bind(this)); @@ -736,10 +746,6 @@ export default class MetamaskController extends EventEmitter { this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController, ), - setSwapsWelcomeMessageHasBeenShown: nodeify( - this.appStateController.setSwapsWelcomeMessageHasBeenShown, - this.appStateController, - ), // EnsController tryReverseResolveAddress: nodeify( @@ -946,6 +952,12 @@ export default class MetamaskController extends EventEmitter { approvalController.reject, approvalController, ), + + // Notifications + updateViewedNotifications: nodeify( + this.notificationController.updateViewed, + this.notificationController, + ), }; } diff --git a/app/scripts/migrations/058.js b/app/scripts/migrations/058.js new file mode 100644 index 000000000..247d11bf7 --- /dev/null +++ b/app/scripts/migrations/058.js @@ -0,0 +1,23 @@ +import { cloneDeep } from 'lodash'; + +const version = 58; + +/** + * Deletes the swapsWelcomeMessageHasBeenShown property from state + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + versionedData.data = transformState(state); + return versionedData; + }, +}; + +function transformState(state) { + delete state.AppStateController?.swapsWelcomeMessageHasBeenShown; + + return state; +} diff --git a/app/scripts/migrations/058.test.js b/app/scripts/migrations/058.test.js new file mode 100644 index 000000000..02efdcdd7 --- /dev/null +++ b/app/scripts/migrations/058.test.js @@ -0,0 +1,46 @@ +import { strict as assert } from 'assert'; +import migration58 from './058'; + +describe('migration #58', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 57, + }, + data: {}, + }; + + const newStorage = await migration58.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 58, + }); + }); + + describe('deleting swapsWelcomeMessageHasBeenShown', function () { + it('should delete the swapsWelcomeMessageHasBeenShown property', async function () { + const oldStorage = { + meta: {}, + data: { + AppStateController: { + swapsWelcomeMessageHasBeenShown: false, + bar: 'baz', + }, + foo: 'bar', + }, + }; + const newStorage = await migration58.migrate(oldStorage); + assert.deepEqual(newStorage.data.AppStateController, { bar: 'baz' }); + }); + + it('should not modify state if the AppStateController does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + foo: 'bar', + }, + }; + const newStorage = await migration58.migrate(oldStorage); + assert.deepEqual(newStorage.data, oldStorage.data); + }); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 8d56dcc85..b0c1716f5 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -62,6 +62,7 @@ const migrations = [ require('./055').default, require('./056').default, require('./057').default, + require('./058').default, ]; export default migrations; diff --git a/shared/notifications/index.js b/shared/notifications/index.js new file mode 100644 index 000000000..a85cb0f4c --- /dev/null +++ b/shared/notifications/index.js @@ -0,0 +1,51 @@ +// Messages and descriptions for these locale keys are in app/_locales/en/messages.json +export const UI_NOTIFICATIONS = { + 1: { + id: 1, + date: '2021-03-17', + image: { + src: 'images/mobile-link-qr.svg', + height: '270px', + width: '270px', + }, + }, + 2: { + id: 2, + date: '2020-08-31', + }, + 3: { + id: 3, + date: '2021-03-8', + }, +}; + +export const getTranslatedUINoficiations = (t, locale) => { + return { + 1: { + ...UI_NOTIFICATIONS[1], + title: t('notifications1Title'), + description: t('notifications1Description'), + date: new Intl.DateTimeFormat(locale).format( + new Date(UI_NOTIFICATIONS[1].date), + ), + }, + 2: { + ...UI_NOTIFICATIONS[2], + title: t('notifications2Title'), + description: t('notifications2Description'), + actionText: t('notifications2ActionText'), + date: new Intl.DateTimeFormat(locale).format( + new Date(UI_NOTIFICATIONS[2].date), + ), + }, + 3: { + ...UI_NOTIFICATIONS[3], + title: t('notifications3Title'), + description: t('notifications3Description'), + actionText: t('notifications3ActionText'), + date: new Intl.DateTimeFormat(locale).format( + new Date(UI_NOTIFICATIONS[3].date), + ), + }, + }; +}; diff --git a/test/e2e/fixtures/address-entry/state.json b/test/e2e/fixtures/address-entry/state.json index 26da39502..e53e3a922 100644 --- a/test/e2e/fixtures/address-entry/state.json +++ b/test/e2e/fixtures/address-entry/state.json @@ -50,6 +50,19 @@ "type": "rpc" } }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/connected-state/state.json b/test/e2e/fixtures/connected-state/state.json index dcf4624c8..a88a31a74 100644 --- a/test/e2e/fixtures/connected-state/state.json +++ b/test/e2e/fixtures/connected-state/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "connectedStatusPopoverHasBeenShown": false, - "swapsWelcomeMessageHasBeenShown": true + "connectedStatusPopoverHasBeenShown": false }, "CachedBalancesController": { "cachedBalances": { @@ -41,6 +40,19 @@ }, "network": "1337" }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/import-ui/state.json b/test/e2e/fixtures/import-ui/state.json index 34f2e1152..350ed8848 100644 --- a/test/e2e/fixtures/import-ui/state.json +++ b/test/e2e/fixtures/import-ui/state.json @@ -91,6 +91,19 @@ }, "network": "1337" }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "CurrencyController": { "conversionDate": 1618940438.187, "conversionRate": 2254.54, diff --git a/test/e2e/fixtures/imported-account/state.json b/test/e2e/fixtures/imported-account/state.json index 1af251b51..a3d51e5d5 100644 --- a/test/e2e/fixtures/imported-account/state.json +++ b/test/e2e/fixtures/imported-account/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "mkrMigrationReminderTimestamp": null, - "swapsWelcomeMessageHasBeenShown": true + "mkrMigrationReminderTimestamp": null }, "CachedBalancesController": { "cachedBalances": { @@ -37,6 +36,19 @@ "type": "rpc" } }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/localization/state.json b/test/e2e/fixtures/localization/state.json index 8b60ffc9d..22d76151b 100644 --- a/test/e2e/fixtures/localization/state.json +++ b/test/e2e/fixtures/localization/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "mkrMigrationReminderTimestamp": null, - "swapsWelcomeMessageHasBeenShown": true + "mkrMigrationReminderTimestamp": null }, "CachedBalancesController": { "cachedBalances": { @@ -37,6 +36,19 @@ "type": "rpc" } }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/metrics-enabled/state.json b/test/e2e/fixtures/metrics-enabled/state.json index 4b0e52e70..2fa9931c5 100644 --- a/test/e2e/fixtures/metrics-enabled/state.json +++ b/test/e2e/fixtures/metrics-enabled/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "connectedStatusPopoverHasBeenShown": false, - "swapsWelcomeMessageHasBeenShown": true + "connectedStatusPopoverHasBeenShown": false }, "CachedBalancesController": { "cachedBalances": { @@ -41,6 +40,19 @@ }, "network": "1337" }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/send-edit/state.json b/test/e2e/fixtures/send-edit/state.json index 77fd20720..c5c548f4c 100644 --- a/test/e2e/fixtures/send-edit/state.json +++ b/test/e2e/fixtures/send-edit/state.json @@ -37,6 +37,19 @@ "type": "rpc" } }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/threebox-enabled/state.json b/test/e2e/fixtures/threebox-enabled/state.json index 602b8d94c..8bac5f889 100644 --- a/test/e2e/fixtures/threebox-enabled/state.json +++ b/test/e2e/fixtures/threebox-enabled/state.json @@ -47,6 +47,19 @@ }, "network": "1337" }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": true diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 9b6fc5fc3..f970ebedf 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -137,6 +137,24 @@ describe('MetaMask', function () { }); }); + describe("Close the what's new popup", function () { + it("should show the what's new popover", async function () { + const popoverTitle = await driver.findElement( + '.popover-header__title h2', + ); + + assert.equal(await popoverTitle.getText(), "What's new"); + }); + + it("should close the what's new popup", async function () { + const popover = await driver.findElement('.popover-container'); + + await driver.clickElement('[data-testid="popover-close"]'); + + await popover.waitForElementState('hidden'); + }); + }); + describe('Show account information', function () { it('shows the QR code for the account', async function () { await driver.clickElement('[data-testid="account-options-menu-button"]'); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index 4c973454a..7bef78294 100644 --- a/test/e2e/tests/from-import-ui.spec.js +++ b/test/e2e/tests/from-import-ui.spec.js @@ -60,6 +60,11 @@ describe('Metamask Import UI', function () { tag: 'button', }); + // close the what's new popup + const popover = await driver.findElement('.popover-container'); + await driver.clickElement('[data-testid="popover-close"]'); + await popover.waitForElementState('hidden'); + // Show account information await driver.clickElement( '[data-testid="account-options-menu-button"]', diff --git a/test/e2e/tests/incremental-security.spec.js b/test/e2e/tests/incremental-security.spec.js index 573330319..1705c2731 100644 --- a/test/e2e/tests/incremental-security.spec.js +++ b/test/e2e/tests/incremental-security.spec.js @@ -65,6 +65,13 @@ describe('Incremental Security', function () { tag: 'button', }); + // closes the what's new popup + const popover = await driver.findElement('.popover-container'); + + await driver.clickElement('[data-testid="popover-close"]'); + + await popover.waitForElementState('hidden'); + await driver.clickElement( '[data-testid="account-options-menu-button"]', ); diff --git a/ui/app/components/app/app-components.scss b/ui/app/components/app/app-components.scss index 07eac6ce1..4f407569d 100644 --- a/ui/app/components/app/app-components.scss +++ b/ui/app/components/app/app-components.scss @@ -36,3 +36,4 @@ @import 'transaction-list/index'; @import 'transaction-status/index'; @import 'wallet-overview/index'; +@import 'whats-new-popup/index'; diff --git a/ui/app/components/app/whats-new-popup/index.js b/ui/app/components/app/whats-new-popup/index.js new file mode 100644 index 000000000..2f20e4f4f --- /dev/null +++ b/ui/app/components/app/whats-new-popup/index.js @@ -0,0 +1 @@ +export { default } from './whats-new-popup'; diff --git a/ui/app/components/app/whats-new-popup/index.scss b/ui/app/components/app/whats-new-popup/index.scss new file mode 100644 index 000000000..277f1e44b --- /dev/null +++ b/ui/app/components/app/whats-new-popup/index.scss @@ -0,0 +1,51 @@ +.whats-new-popup { + &__notifications { + display: flex; + flex-direction: column; + align-items: center; + } + + &__notification, + &__first-notification { + display: flex; + flex-direction: column; + align-items: left; + margin: 0 24px 24px 24px; + border-bottom: 1px solid $Grey-100; + } + + &__notification-image { + margin-bottom: 16px; + } + + &__description-and-date { + margin-bottom: 16px; + } + + &__notification-date { + color: $Grey-500; + } + + &__button { + margin-right: auto; + } + + &__button, + &__link { + margin-bottom: 24px; + } + + &__link { + @include H6; + + color: $Blue-500; + cursor: pointer; + } + + &__notification-title { + @include H4; + + font-weight: bold; + margin-bottom: 8px; + } +} diff --git a/ui/app/components/app/whats-new-popup/whats-new-popup.js b/ui/app/components/app/whats-new-popup/whats-new-popup.js new file mode 100644 index 000000000..4849be291 --- /dev/null +++ b/ui/app/components/app/whats-new-popup/whats-new-popup.js @@ -0,0 +1,179 @@ +import React, { useContext, useMemo, useRef, useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { getCurrentLocale } from '../../../ducks/metamask/metamask'; +import { I18nContext } from '../../../contexts/i18n'; +import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; +import Button from '../../ui/button'; +import Popover from '../../ui/popover'; +import { updateViewedNotifications } from '../../../store/actions'; +import { getTranslatedUINoficiations } from '../../../../../shared/notifications'; +import { getSortedNotificationsToShow } from '../../../selectors'; + +function getActionFunctionById(id) { + const actionFunctions = { + 2: () => { + global.platform.openTab({ + url: + 'https://survey.alchemer.com/s3/6173069/MetaMask-Extension-NPS-January-2021', + }); + }, + 3: () => { + global.platform.openTab({ + url: 'https://community.metamask.io/t/about-the-security-category/72', + }); + }, + }; + + return actionFunctions[id]; +} + +const renderFirstNotification = (notification, idRefMap) => { + const { id, date, title, description, image, actionText } = notification; + const actionFunction = getActionFunctionById(id); + return ( +
+ {image && ( + + )} +
{title}
+
+
+ {description} +
+
{date}
+
+ {actionText && ( + + )} +
+ ); +}; + +const renderSubsequentNotification = (notification, idRefMap) => { + const { id, date, title, description, actionText } = notification; + + const actionFunction = getActionFunctionById(id); + return ( +
+
{title}
+
+
+ {description} +
+
{date}
+
+ {actionText && ( +
+ {actionText} +
+ )} +
+ ); +}; + +export default function WhatsNewPopup({ onClose }) { + const t = useContext(I18nContext); + + const notifications = useSelector(getSortedNotificationsToShow); + const locale = useSelector(getCurrentLocale); + + const [seenNotifications, setSeenNotifications] = useState({}); + + const popoverRef = useRef(); + + const memoizedNotifications = useEqualityCheck(notifications); + const idRefMap = useMemo( + () => + memoizedNotifications.reduce( + (_idRefMap, notification) => ({ + ..._idRefMap, + [notification.id]: React.createRef(), + }), + {}, + ), + [memoizedNotifications], + ); + + useEffect(() => { + const observer = new window.IntersectionObserver( + (entries, _observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const [id, ref] = Object.entries(idRefMap).find(([_, _ref]) => + _ref.current.isSameNode(entry.target), + ); + + setSeenNotifications((_seenNotifications) => ({ + ..._seenNotifications, + [id]: true, + })); + + _observer.unobserve(ref.current); + } + }); + }, + { + root: popoverRef.current, + threshold: 1.0, + }, + ); + + Object.values(idRefMap).forEach((ref) => { + observer.observe(ref.current); + }); + + return () => { + observer.disconnect(); + }; + }, [idRefMap, setSeenNotifications]); + + return ( + { + updateViewedNotifications(seenNotifications); + onClose(); + }} + popoverRef={popoverRef} + mediumHeight + > +
+ {notifications.map(({ id }, index) => { + const notification = getTranslatedUINoficiations(t, locale)[id]; + return index === 0 + ? renderFirstNotification(notification, idRefMap) + : renderSubsequentNotification(notification, idRefMap); + })} +
+
+ ); +} + +WhatsNewPopup.propTypes = { + onClose: PropTypes.func.isRequired, +}; diff --git a/ui/app/components/ui/popover/index.scss b/ui/app/components/ui/popover/index.scss index d2869b484..2ab7743e5 100644 --- a/ui/app/components/ui/popover/index.scss +++ b/ui/app/components/ui/popover/index.scss @@ -18,6 +18,10 @@ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.25); border-radius: 10px; background: white; + + &--medium-height { + max-height: 600px; + } } &-header { diff --git a/ui/app/components/ui/popover/popover.component.js b/ui/app/components/ui/popover/popover.component.js index 0f93c7183..1c5c0dd1a 100644 --- a/ui/app/components/ui/popover/popover.component.js +++ b/ui/app/components/ui/popover/popover.component.js @@ -13,9 +13,11 @@ const Popover = ({ onBack, onClose, className, + mediumHeight, contentClassName, showArrow, CustomBackground, + popoverRef, }) => { const t = useI18nContext(); return ( @@ -25,7 +27,12 @@ const Popover = ({ ) : (
)} -
+
{showArrow ?
: null}
@@ -42,6 +49,7 @@ const Popover = ({
@@ -76,6 +84,10 @@ Popover.propTypes = { contentClassName: PropTypes.string, className: PropTypes.string, showArrow: PropTypes.bool, + mediumHeight: PropTypes.bool, + popoverRef: PropTypes.shape({ + current: PropTypes.instanceOf(window.Element), + }), }; export default class PopoverPortal extends PureComponent { diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js index c927e70c9..20bab88f7 100644 --- a/ui/app/ducks/app/app.js +++ b/ui/app/ducks/app/app.js @@ -50,6 +50,7 @@ export default function reduceApp(state = {}, action) { requestAccountTabs: {}, openMetaMaskTabs: {}, currentWindowTab: {}, + showWhatsNewPopup: true, ...state, }; @@ -352,6 +353,12 @@ export default function reduceApp(state = {}, action) { currentWindowTab: action.value, }; + case actionConstants.HIDE_WHATS_NEW_POPUP: + return { + ...appState, + showWhatsNewPopup: false, + }; + default: return appState; } @@ -364,3 +371,9 @@ export function setThreeBoxLastUpdated(lastUpdated) { value: lastUpdated, }; } + +export function hideWhatsNewPopup() { + return { + type: actionConstants.HIDE_WHATS_NEW_POPUP, + }; +} diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 427cd46b3..faf86eb91 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -13,7 +13,7 @@ import ConnectedSites from '../connected-sites'; import ConnectedAccounts from '../connected-accounts'; import { Tabs, Tab } from '../../components/ui/tabs'; import { EthOverview } from '../../components/app/wallet-overview'; -import SwapsIntroPopup from '../swaps/intro-popup'; +import WhatsNewPopup from '../../components/app/whats-new-popup'; import { ASSET_ROUTE, @@ -64,19 +64,18 @@ export default class Home extends PureComponent { connectedStatusPopoverHasBeenShown: PropTypes.bool, defaultHomeActiveTabName: PropTypes.string, onTabClick: PropTypes.func.isRequired, - setSwapsWelcomeMessageHasBeenShown: PropTypes.func.isRequired, - swapsWelcomeMessageHasBeenShown: PropTypes.bool.isRequired, haveSwapsQuotes: PropTypes.bool.isRequired, showAwaitingSwapScreen: PropTypes.bool.isRequired, swapsFetchParams: PropTypes.object, - swapsEnabled: PropTypes.bool, - isMainnet: PropTypes.bool, shouldShowWeb3ShimUsageNotification: PropTypes.bool.isRequired, setWeb3ShimUsageAlertDismissed: PropTypes.func.isRequired, originOfCurrentTab: PropTypes.string, disableWeb3ShimUsageAlert: PropTypes.func.isRequired, pendingConfirmations: PropTypes.arrayOf(PropTypes.object).isRequired, infuraBlocked: PropTypes.bool.isRequired, + showWhatsNewPopup: PropTypes.bool.isRequired, + hideWhatsNewPopup: PropTypes.func.isRequired, + notificationsToShow: PropTypes.bool.isRequired, }; state = { @@ -323,10 +322,9 @@ export default class Home extends PureComponent { history, connectedStatusPopoverHasBeenShown, isPopup, - swapsWelcomeMessageHasBeenShown, - setSwapsWelcomeMessageHasBeenShown, - swapsEnabled, - isMainnet, + notificationsToShow, + showWhatsNewPopup, + hideWhatsNewPopup, } = this.props; if (forgottenPassword) { @@ -344,8 +342,8 @@ export default class Home extends PureComponent { exact />
- {!swapsWelcomeMessageHasBeenShown && swapsEnabled && isMainnet ? ( - + {notificationsToShow && showWhatsNewPopup ? ( + ) : null} {isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index 721c7a15e..6020c1510 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -12,6 +12,8 @@ import { getWeb3ShimUsageStateForOrigin, unconfirmedTransactionsCountSelector, getInfuraBlocked, + getShowWhatsNewPopup, + getSortedNotificationsToShow, } from '../../selectors'; import { @@ -21,16 +23,12 @@ import { setShowRestorePromptToFalse, setConnectedStatusPopoverHasBeenShown, setDefaultHomeActiveTabName, - setSwapsWelcomeMessageHasBeenShown, setWeb3ShimUsageAlertDismissed, setAlertEnabledness, } from '../../store/actions'; -import { setThreeBoxLastUpdated } from '../../ducks/app/app'; +import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; -import { - getSwapsWelcomeMessageSeenStatus, - getSwapsFeatureLiveness, -} from '../../ducks/swaps/swaps'; +import { getSwapsFeatureLiveness } from '../../ducks/swaps/swaps'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_NOTIFICATION, @@ -97,7 +95,6 @@ const mapStateToProps = (state) => { totalUnapprovedCount, connectedStatusPopoverHasBeenShown, defaultHomeActiveTabName, - swapsWelcomeMessageHasBeenShown: getSwapsWelcomeMessageSeenStatus(state), haveSwapsQuotes: Boolean(Object.values(swapsState.quotes || {}).length), swapsFetchParams: swapsState.fetchParams, showAwaitingSwapScreen: swapsState.routeState === 'awaiting', @@ -106,6 +103,8 @@ const mapStateToProps = (state) => { shouldShowWeb3ShimUsageNotification, pendingConfirmations, infuraBlocked: getInfuraBlocked(state), + notificationsToShow: getSortedNotificationsToShow(state).length > 0, + showWhatsNewPopup: getShowWhatsNewPopup(state), }; }; @@ -126,12 +125,11 @@ const mapDispatchToProps = (dispatch) => ({ setConnectedStatusPopoverHasBeenShown: () => dispatch(setConnectedStatusPopoverHasBeenShown()), onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)), - setSwapsWelcomeMessageHasBeenShown: () => - dispatch(setSwapsWelcomeMessageHasBeenShown()), setWeb3ShimUsageAlertDismissed: (origin) => setWeb3ShimUsageAlertDismissed(origin), disableWeb3ShimUsageAlert: () => setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false), + hideWhatsNewPopup: () => dispatch(hideWhatsNewPopup()), }); export default compose( diff --git a/ui/app/pages/routes/routes.component.js b/ui/app/pages/routes/routes.component.js index 09b5c2fca..5648e4e20 100644 --- a/ui/app/pages/routes/routes.component.js +++ b/ui/app/pages/routes/routes.component.js @@ -102,13 +102,13 @@ export default class Routes extends Component { currentCurrency, pageChanged, setCurrentCurrencyToUSD, + history, } = this.props; - if (!currentCurrency) { setCurrentCurrencyToUSD(); } - this.props.history.listen((locationObj, action) => { + history.listen((locationObj, action) => { if (action === 'PUSH') { pageChanged(locationObj.pathname); } diff --git a/ui/app/pages/swaps/index.scss b/ui/app/pages/swaps/index.scss index cb08d457b..7f1fe894a 100644 --- a/ui/app/pages/swaps/index.scss +++ b/ui/app/pages/swaps/index.scss @@ -6,7 +6,6 @@ @import 'dropdown-search-list/index'; @import 'exchange-rate-display/index'; @import 'fee-card/index'; -@import 'intro-popup/index'; @import 'loading-swaps-quotes/index'; @import 'main-quote-summary/index'; @import 'searchable-item-list/index'; diff --git a/ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap b/ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap deleted file mode 100644 index d9d0324df..000000000 --- a/ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IntroPopup renders the component with initial props 1`] = ` -
-
-
-`; diff --git a/ui/app/pages/swaps/intro-popup/index.js b/ui/app/pages/swaps/intro-popup/index.js deleted file mode 100644 index 6460538b9..000000000 --- a/ui/app/pages/swaps/intro-popup/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './intro-popup'; diff --git a/ui/app/pages/swaps/intro-popup/index.scss b/ui/app/pages/swaps/intro-popup/index.scss deleted file mode 100644 index 48d79c5f9..000000000 --- a/ui/app/pages/swaps/intro-popup/index.scss +++ /dev/null @@ -1,71 +0,0 @@ -.intro-popup { - &__liquidity-sources-label { - @include H7; - - font-weight: bold; - margin-bottom: 6px; - color: $Black-100; - - @media screen and (min-width: 576px) { - @include H6; - } - } - - &__learn-more-header { - @include H4; - - font-weight: bold; - margin-bottom: 12px; - margin-top: 16px; - } - - &__learn-more-link { - @include H6; - - color: $Blue-500; - margin-bottom: 8px; - cursor: pointer; - } - - &__content { - margin-left: 24px; - - > img { - width: 96%; - margin-left: -9px; - } - } - - &__footer { - border-top: none; - } - - &__button { - border-radius: 100px; - height: 44px; - } - - &__source-logo-container { - width: 276px; - display: flex; - justify-content: center; - align-items: center; - padding: 20px 16px; - background: $Grey-000; - border-radius: 8px; - - @media screen and (min-width: 576px) { - width: 412px; - - img { - width: 364px; - } - } - } - - &__popover { - @media screen and (min-width: 576px) { - width: 460px; - } - } -} diff --git a/ui/app/pages/swaps/intro-popup/intro-popup.js b/ui/app/pages/swaps/intro-popup/intro-popup.js deleted file mode 100644 index 658c84bb9..000000000 --- a/ui/app/pages/swaps/intro-popup/intro-popup.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import PropTypes from 'prop-types'; -import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; -import { I18nContext } from '../../../contexts/i18n'; -import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'; -import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; -import { getSwapsDefaultToken } from '../../../selectors'; -import Button from '../../../components/ui/button'; -import Popover from '../../../components/ui/popover'; - -export default function IntroPopup({ onClose }) { - const dispatch = useDispatch(useDispatch); - const history = useHistory(); - const t = useContext(I18nContext); - - const swapsDefaultToken = useSelector(getSwapsDefaultToken); - const enteredSwapsEvent = useNewMetricEvent({ - event: 'Swaps Opened', - properties: { - source: 'Intro popup', - active_currency: swapsDefaultToken.symbol, - }, - category: 'swaps', - }); - const blogPostVisitedEvent = useNewMetricEvent({ - event: 'Blog Post Visited ', - category: 'swaps', - }); - const contractAuditVisitedEvent = useNewMetricEvent({ - event: 'Contract Audit Visited', - category: 'swaps', - }); - const productOverviewDismissedEvent = useNewMetricEvent({ - event: 'Product Overview Dismissed', - category: 'swaps', - }); - - return ( -
- { - productOverviewDismissedEvent(); - onClose(); - }} - footerClassName="intro-popup__footer" - footer={ - - } - > -
-
- {t('swapIntroLiquiditySourcesLabel')} -
-
- -
-
- {t('swapIntroLearnMoreHeader')} -
-
{ - global.platform.openTab({ - url: - 'https://medium.com/metamask/introducing-metamask-swaps-84318c643785', - }); - blogPostVisitedEvent(); - }} - > - {t('swapIntroLearnMoreLink')} -
-
{ - global.platform.openTab({ - url: - 'https://diligence.consensys.net/audits/private/lsjipyllnw2/', - }); - contractAuditVisitedEvent(); - }} - > - {t('swapLearnMoreContractsAuditReview')} -
-
-
-
- ); -} - -IntroPopup.propTypes = { - onClose: PropTypes.func.isRequired, -}; diff --git a/ui/app/pages/swaps/intro-popup/intro-popup.test.js b/ui/app/pages/swaps/intro-popup/intro-popup.test.js deleted file mode 100644 index 2ceec7e0e..000000000 --- a/ui/app/pages/swaps/intro-popup/intro-popup.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, -} from '../../../../../test/jest'; -import IntroPopup from '.'; - -const createProps = (customProps = {}) => { - return { - onClose: jest.fn(), - ...customProps, - }; -}; - -describe('IntroPopup', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { container } = renderWithProvider(, store); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 6b26d18f9..73cebf8db 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -494,3 +494,36 @@ export function getNativeCurrencyImage(state) { export function getNextSuggestedNonce(state) { return Number(state.metamask.nextNonce); } + +export function getShowWhatsNewPopup(state) { + return state.appState.showWhatsNewPopup; +} + +/** + * @typedef {Object} Notification + * @property {number} id - A unique identifier for the notification + * @property {string} date - A date in YYYY-MM-DD format, identifying when the notification was first committed + */ + +/** + * Notifications are managed by the notification controller and referenced by + * `state.metamask.notifications`. This function returns a list of notifications + * the can be shown to the user. This list includes all notifications that do not + * have a truthy `isShown` property. + * + * The returned notifications are sorted by date. + * + * @param {Object} state - the redux state object + * @returns {Notification[]} An array of notifications that can be shown to the user + */ + +export function getSortedNotificationsToShow(state) { + const notifications = Object.values(state.metamask.notifications); + const notificationsToShow = notifications.filter( + (notification) => !notification.isShown, + ); + const notificationsSortedByDate = notificationsToShow.sort( + (a, b) => new Date(b.date) - new Date(a.date), + ); + return notificationsSortedByDate; +} diff --git a/ui/app/store/actionConstants.js b/ui/app/store/actionConstants.js index 2fe280251..8613318c8 100644 --- a/ui/app/store/actionConstants.js +++ b/ui/app/store/actionConstants.js @@ -112,3 +112,6 @@ export const LOADING_TOKEN_PARAMS_FINISHED = 'LOADING_TOKEN_PARAMS_FINISHED'; export const SET_REQUEST_ACCOUNT_TABS = 'SET_REQUEST_ACCOUNT_TABS'; export const SET_CURRENT_WINDOW_TAB = 'SET_CURRENT_WINDOW_TAB'; export const SET_OPEN_METAMASK_TAB_IDS = 'SET_OPEN_METAMASK_TAB_IDS'; + +// Home Screen +export const HIDE_WHATS_NEW_POPUP = 'HIDE_WHATS_NEW_POPUP'; diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 8d193afa6..7d73228f5 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -2573,16 +2573,6 @@ export function setConnectedStatusPopoverHasBeenShown() { }; } -export function setSwapsWelcomeMessageHasBeenShown() { - return () => { - background.setSwapsWelcomeMessageHasBeenShown((err) => { - if (err) { - throw new Error(err.message); - } - }); - }; -} - export async function setAlertEnabledness(alertId, enabledness) { await promisifiedBackground.setAlertEnabledness(alertId, enabledness); } @@ -2888,3 +2878,9 @@ export function trackMetaMetricsEvent(payload, options) { export function trackMetaMetricsPage(payload, options) { return promisifiedBackground.trackMetaMetricsPage(payload, options); } + +export function updateViewedNotifications(notificationIdViewedStatusMap) { + return promisifiedBackground.updateViewedNotifications( + notificationIdViewedStatusMap, + ); +}