From d6a2ce60c33112c1fb30bc49033ad5a93213045a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 20:51:57 -0700 Subject: [PATCH 01/44] feat: add backgroundUrl and isTrending to DiscoverData type and API mapping Co-Authored-By: Claude Sonnet 4.6 --- @shared/api/internal.ts | 4 + @shared/api/types/types.ts | 2 + .../plans/2026-03-30-discover-v2.md | 2171 +++++++++++++++++ .../specs/2026-03-30-discover-v2-design.md | 340 +++ .../popup/views/__tests__/Discover.test.tsx | 4 + 5 files changed, 2521 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-30-discover-v2.md create mode 100644 docs/superpowers/specs/2026-03-30-discover-v2-design.md diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 981b71eb82..9fc4daf98d 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -633,6 +633,8 @@ export const getDiscoverData = async () => { website_url: string; tags: string[]; is_blacklisted: boolean; + background_url?: string; + is_trending: boolean; }[]; }; }; @@ -652,6 +654,8 @@ export const getDiscoverData = async () => { websiteUrl: entry.website_url, tags: entry.tags, isBlacklisted: entry.is_blacklisted, + backgroundUrl: entry.background_url, + isTrending: entry.is_trending, })); }; diff --git a/@shared/api/types/types.ts b/@shared/api/types/types.ts index ca3fbb376c..8b73652eef 100644 --- a/@shared/api/types/types.ts +++ b/@shared/api/types/types.ts @@ -403,6 +403,8 @@ export type DiscoverData = { websiteUrl: string; tags: string[]; isBlacklisted: boolean; + backgroundUrl?: string; + isTrending: boolean; }[]; export interface LedgerKeyAccount { diff --git a/docs/superpowers/plans/2026-03-30-discover-v2.md b/docs/superpowers/plans/2026-03-30-discover-v2.md new file mode 100644 index 0000000000..036d560f33 --- /dev/null +++ b/docs/superpowers/plans/2026-03-30-discover-v2.md @@ -0,0 +1,2171 @@ +# Discover V2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port the Discover V2 refactoring (Trending carousel, Recent protocols, +dApps sections, protocol details modal, welcome modal) from freighter-mobile to +the freighter browser extension. + +**Architecture:** The Discover screen renders inside a Radix Sheet overlay +(slide-from-bottom, same as AssetDetail). Sub-views (expanded Recent/dApps) swap +via local state inside the Sheet. Protocol details use the existing +SlideupModal. Recent protocols are stored in `browser.storage.local` via a +standalone helper. No new routes — the existing `/discover` route is removed. + +**Tech Stack:** React 19, TypeScript, SCSS (BEM), @stellar/design-system, Radix +UI Sheet (shadcn wrapper), webextension-polyfill, browser.storage.local + +**Spec:** `docs/superpowers/specs/2026-03-30-discover-v2-design.md` + +--- + +### Task 1: Update DiscoverData type and getDiscoverData API mapping + +**Files:** + +- Modify: `@shared/api/types/types.ts:399-406` +- Modify: `@shared/api/internal.ts:624-656` + +- [ ] **Step 1: Update DiscoverData type** + +In `@shared/api/types/types.ts`, add `backgroundUrl` and `isTrending` to the +`DiscoverData` type: + +```typescript +export type DiscoverData = { + description: string; + iconUrl: string; + name: string; + websiteUrl: string; + tags: string[]; + isBlacklisted: boolean; + backgroundUrl?: string; + isTrending: boolean; +}[]; +``` + +- [ ] **Step 2: Update getDiscoverData mapping** + +In `@shared/api/internal.ts`, update the response type and mapping in +`getDiscoverData()`: + +Add to the `parsedResponse` type assertion inside the function: + +```typescript +background_url?: string; +is_trending: boolean; +``` + +Add to the return mapping: + +```typescript +backgroundUrl: entry.background_url, +isTrending: entry.is_trending, +``` + +The full function should look like: + +```typescript +export const getDiscoverData = async () => { + const url = new URL(`${INDEXER_V2_URL}/protocols`); + const response = await fetch(url.href); + const parsedResponse = (await response.json()) as { + data: { + protocols: { + description: string; + icon_url: string; + name: string; + website_url: string; + tags: string[]; + is_blacklisted: boolean; + background_url?: string; + is_trending: boolean; + }[]; + }; + }; + + if (!response.ok || !parsedResponse.data) { + const _err = JSON.stringify(parsedResponse); + captureException( + `Failed to fetch discover entries - ${response.status}: ${response.statusText}`, + ); + throw new Error(_err); + } + + return parsedResponse.data.protocols.map((entry) => ({ + description: entry.description, + iconUrl: entry.icon_url, + name: entry.name, + websiteUrl: entry.website_url, + tags: entry.tags, + isBlacklisted: entry.is_blacklisted, + backgroundUrl: entry.background_url, + isTrending: entry.is_trending, + })); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add @shared/api/types/types.ts @shared/api/internal.ts +git commit -m "feat: add backgroundUrl and isTrending to DiscoverData type and API mapping" +``` + +--- + +### Task 2: Create recentProtocols helper + +**Files:** + +- Create: `extension/src/popup/helpers/recentProtocols.ts` + +- [ ] **Step 1: Create the recentProtocols helper** + +Create `extension/src/popup/helpers/recentProtocols.ts`: + +```typescript +import browser from "webextension-polyfill"; + +const STORAGE_KEY = "recentProtocols"; +const MAX_RECENT = 5; + +export interface RecentProtocolEntry { + websiteUrl: string; + lastAccessed: number; +} + +export const getRecentProtocols = async (): Promise => { + const result = await browser.storage.local.get(STORAGE_KEY); + return (result[STORAGE_KEY] as RecentProtocolEntry[]) || []; +}; + +export const addRecentProtocol = async (websiteUrl: string): Promise => { + const existing = await getRecentProtocols(); + const filtered = existing.filter((entry) => entry.websiteUrl !== websiteUrl); + const updated = [{ websiteUrl, lastAccessed: Date.now() }, ...filtered].slice( + 0, + MAX_RECENT, + ); + await browser.storage.local.set({ [STORAGE_KEY]: updated }); +}; + +export const clearRecentProtocols = async (): Promise => { + await browser.storage.local.remove(STORAGE_KEY); +}; +``` + +- [ ] **Step 2: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 3: Commit** + +```bash +git add extension/src/popup/helpers/recentProtocols.ts +git commit -m "feat: add recentProtocols helper for browser.storage.local" +``` + +--- + +### Task 3: Create useDiscoverData hook + +**Files:** + +- Create: `extension/src/popup/views/Discover/hooks/useDiscoverData.ts` + +- [ ] **Step 1: Create the hook** + +Create `extension/src/popup/views/Discover/hooks/useDiscoverData.ts`: + +```typescript +import { useCallback, useEffect, useReducer } from "react"; +import { captureException } from "@sentry/browser"; + +import { getDiscoverData } from "@shared/api/internal"; +import { DiscoverData } from "@shared/api/types"; +import { + getRecentProtocols, + RecentProtocolEntry, +} from "popup/helpers/recentProtocols"; + +interface DiscoverDataState { + isLoading: boolean; + error: unknown | null; + allProtocols: DiscoverData; + recentEntries: RecentProtocolEntry[]; +} + +type Action = + | { type: "FETCH_START" } + | { + type: "FETCH_SUCCESS"; + payload: { + protocols: DiscoverData; + recentEntries: RecentProtocolEntry[]; + }; + } + | { type: "FETCH_ERROR"; payload: unknown } + | { type: "REFRESH_RECENT"; payload: RecentProtocolEntry[] }; + +const initialState: DiscoverDataState = { + isLoading: true, + error: null, + allProtocols: [], + recentEntries: [], +}; + +const reducer = ( + state: DiscoverDataState, + action: Action, +): DiscoverDataState => { + switch (action.type) { + case "FETCH_START": + return { ...state, isLoading: true, error: null }; + case "FETCH_SUCCESS": + return { + isLoading: false, + error: null, + allProtocols: action.payload.protocols, + recentEntries: action.payload.recentEntries, + }; + case "FETCH_ERROR": + return { ...state, isLoading: false, error: action.payload }; + case "REFRESH_RECENT": + return { ...state, recentEntries: action.payload }; + default: + return state; + } +}; + +export const useDiscoverData = () => { + const [state, dispatch] = useReducer(reducer, initialState); + + const fetchData = useCallback(async () => { + dispatch({ type: "FETCH_START" }); + try { + const [protocols, recentEntries] = await Promise.all([ + getDiscoverData(), + getRecentProtocols(), + ]); + dispatch({ + type: "FETCH_SUCCESS", + payload: { protocols, recentEntries }, + }); + } catch (error) { + dispatch({ type: "FETCH_ERROR", payload: error }); + captureException(`Error loading discover data - ${error}`); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const refreshRecent = useCallback(async () => { + const recentEntries = await getRecentProtocols(); + dispatch({ type: "REFRESH_RECENT", payload: recentEntries }); + }, []); + + const allowedProtocols = state.allProtocols.filter((p) => !p.isBlacklisted); + + const trendingItems = allowedProtocols.filter((p) => p.isTrending); + + const recentItems = state.recentEntries + .map((entry) => + allowedProtocols.find((p) => p.websiteUrl === entry.websiteUrl), + ) + .filter(Boolean) as DiscoverData; + + const dappsItems = allowedProtocols; + + return { + isLoading: state.isLoading, + error: state.error, + trendingItems, + recentItems, + dappsItems, + allProtocols: allowedProtocols, + refreshRecent, + }; +}; +``` + +- [ ] **Step 2: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 3: Commit** + +```bash +git add extension/src/popup/views/Discover/hooks/useDiscoverData.ts +git commit -m "feat: add useDiscoverData hook with trending, recent, and dapps lists" +``` + +--- + +### Task 4: Create useDiscoverWelcome hook + +**Files:** + +- Create: `extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts` + +- [ ] **Step 1: Create the hook** + +Create `extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts`: + +```typescript +import { useCallback, useEffect, useState } from "react"; +import browser from "webextension-polyfill"; + +const STORAGE_KEY = "hasSeenDiscoverWelcome"; + +export const useDiscoverWelcome = () => { + const [showWelcome, setShowWelcome] = useState(false); + + useEffect(() => { + const checkWelcome = async () => { + const result = await browser.storage.local.get(STORAGE_KEY); + if (!result[STORAGE_KEY]) { + setShowWelcome(true); + } + }; + checkWelcome(); + }, []); + + const dismissWelcome = useCallback(async () => { + setShowWelcome(false); + await browser.storage.local.set({ [STORAGE_KEY]: true }); + }, []); + + return { showWelcome, dismissWelcome }; +}; +``` + +- [ ] **Step 2: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 3: Commit** + +```bash +git add extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts +git commit -m "feat: add useDiscoverWelcome hook for first-visit modal" +``` + +--- + +### Task 5: Create ProtocolRow component + +**Files:** + +- Create: `extension/src/popup/views/Discover/components/ProtocolRow/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/ProtocolRow/styles.scss` + +- [ ] **Step 1: Create ProtocolRow styles** + +Create `extension/src/popup/views/Discover/components/ProtocolRow/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.ProtocolRow { + display: flex; + gap: pxToRem(12); + align-items: center; + cursor: pointer; + + &__icon { + height: pxToRem(32); + width: pxToRem(32); + border-radius: pxToRem(10); + object-fit: cover; + flex-shrink: 0; + } + + &__label { + display: flex; + flex-direction: column; + flex: 1 0 0; + min-width: 0; + + &__subtitle { + color: var(--sds-clr-gray-11); + } + } + + &__open-button { + border-radius: 100px; + border: 1px solid var(--sds-clr-gray-06); + color: var(--sds-clr-gray-12); + display: flex; + gap: pxToRem(4); + padding: pxToRem(6) pxToRem(10); + justify-content: center; + align-items: center; + cursor: pointer; + background: transparent; + text-decoration: none; + flex-shrink: 0; + + &__icon { + height: pxToRem(14); + width: pxToRem(14); + color: var(--sds-clr-gray-09); + } + } +} +``` + +- [ ] **Step 2: Create ProtocolRow component** + +Create `extension/src/popup/views/Discover/components/ProtocolRow/index.tsx`: + +```tsx +import React from "react"; +import { Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ProtocolRowProps { + protocol: Protocol; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const ProtocolRow = ({ + protocol, + onRowClick, + onOpenClick, +}: ProtocolRowProps) => { + const { t } = useTranslation(); + + return ( +
onRowClick(protocol)} + > + {protocol.name} +
+ + {protocol.name} + +
+ + {protocol.tags[0]} + +
+
+ +
+ ); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/popup/views/Discover/components/ProtocolRow/ +git commit -m "feat: add ProtocolRow component for Discover sections" +``` + +--- + +### Task 6: Create TrendingCarousel component + +**Files:** + +- Create: + `extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss` + +- [ ] **Step 1: Create TrendingCarousel styles** + +Create +`extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.TrendingCarousel { + &__label { + margin-bottom: pxToRem(12); + } + + &__scroll-container { + display: flex; + gap: pxToRem(16); + overflow-x: auto; + scroll-snap-type: x mandatory; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } + + &__card { + min-width: pxToRem(264); + height: pxToRem(148); + border-radius: pxToRem(12); + background-color: var(--sds-clr-gray-03); + background-size: cover; + background-position: center; + position: relative; + overflow: hidden; + cursor: pointer; + scroll-snap-align: start; + flex-shrink: 0; + + &__gradient { + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + transparent 40%, + var(--sds-clr-gray-03) 100% + ); + } + + &__content { + position: absolute; + bottom: pxToRem(12); + left: pxToRem(12); + right: pxToRem(12); + display: flex; + justify-content: space-between; + align-items: flex-end; + } + + &__tag { + color: var(--sds-clr-gray-11); + } + } +} +``` + +- [ ] **Step 2: Create TrendingCarousel component** + +Create +`extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx`: + +```tsx +import React from "react"; +import { Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface TrendingCarouselProps { + items: DiscoverData; + onCardClick: (protocol: Protocol) => void; +} + +export const TrendingCarousel = ({ + items, + onCardClick, +}: TrendingCarouselProps) => { + const { t } = useTranslation(); + + if (items.length === 0) { + return null; + } + + return ( +
+
+ + {t("Trending")} + +
+
+ {items.map((protocol) => ( +
onCardClick(protocol)} + > +
+
+ + {protocol.name} + +
+ + {protocol.tags[0]} + +
+
+
+ ))} +
+
+ ); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/popup/views/Discover/components/TrendingCarousel/ +git commit -m "feat: add TrendingCarousel component for Discover" +``` + +--- + +### Task 7: Create DiscoverSection component + +**Files:** + +- Create: + `extension/src/popup/views/Discover/components/DiscoverSection/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/DiscoverSection/styles.scss` + +- [ ] **Step 1: Create DiscoverSection styles** + +Create +`extension/src/popup/views/Discover/components/DiscoverSection/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.DiscoverSection { + &__header { + display: flex; + align-items: center; + gap: pxToRem(4); + cursor: pointer; + margin-bottom: pxToRem(24); + color: var(--sds-clr-gray-12); + + svg { + width: pxToRem(16); + height: pxToRem(16); + } + } + + &__list { + display: flex; + flex-direction: column; + gap: pxToRem(24); + } +} +``` + +- [ ] **Step 2: Create DiscoverSection component** + +Create +`extension/src/popup/views/Discover/components/DiscoverSection/index.tsx`: + +```tsx +import React from "react"; +import { Icon, Text } from "@stellar/design-system"; + +import { DiscoverData } from "@shared/api/types"; +import { ProtocolRow } from "../ProtocolRow"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +const MAX_VISIBLE = 5; + +interface DiscoverSectionProps { + title: string; + items: DiscoverData; + onExpand: () => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const DiscoverSection = ({ + title, + items, + onExpand, + onRowClick, + onOpenClick, +}: DiscoverSectionProps) => { + if (items.length === 0) { + return null; + } + + const visibleItems = items.slice(0, MAX_VISIBLE); + + return ( +
+
+ + {title} + + +
+
+ {visibleItems.map((protocol) => ( + + ))} +
+
+ ); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/popup/views/Discover/components/DiscoverSection/ +git commit -m "feat: add DiscoverSection component with title, chevron, and protocol list" +``` + +--- + +### Task 8: Create ProtocolDetailsPanel component + +**Files:** + +- Create: + `extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss` + +- [ ] **Step 1: Create ProtocolDetailsPanel styles** + +Create +`extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.ProtocolDetailsPanel { + padding: pxToRem(32) pxToRem(16); + + &__header { + display: flex; + align-items: center; + gap: pxToRem(12); + margin-bottom: pxToRem(24); + } + + &__icon { + width: pxToRem(40); + height: pxToRem(40); + border-radius: pxToRem(10); + object-fit: cover; + flex-shrink: 0; + } + + &__name { + flex: 1 0 0; + min-width: 0; + } + + &__open-button { + background: var(--sds-clr-gray-12); + color: var(--sds-clr-gray-01); + border: none; + border-radius: 100px; + display: flex; + gap: pxToRem(4); + padding: pxToRem(6) pxToRem(10); + justify-content: center; + align-items: center; + cursor: pointer; + flex-shrink: 0; + + &__icon { + height: pxToRem(14); + width: pxToRem(14); + } + } + + &__section { + margin-bottom: pxToRem(24); + + &__label { + color: var(--sds-clr-gray-11); + margin-bottom: pxToRem(8); + } + } + + &__domain { + display: flex; + align-items: center; + gap: pxToRem(4); + + svg { + width: pxToRem(16); + height: pxToRem(16); + color: var(--sds-clr-gray-11); + } + } + + &__tags { + display: flex; + gap: pxToRem(8); + flex-wrap: wrap; + } + + &__tag { + border-radius: 100px; + background: var(--sds-clr-lime-02); + border: 1px solid var(--sds-clr-lime-06); + padding: pxToRem(2) pxToRem(8); + color: var(--sds-clr-lime-11); + } + + &__description { + color: var(--sds-clr-gray-12); + } +} +``` + +- [ ] **Step 2: Create ProtocolDetailsPanel component** + +Create +`extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx`: + +```tsx +import React from "react"; +import { Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ProtocolDetailsPanelProps { + protocol: Protocol; + onOpen: (protocol: Protocol) => void; +} + +const getHostname = (url: string): string => { + try { + return new URL(url).hostname; + } catch { + return url; + } +}; + +export const ProtocolDetailsPanel = ({ + protocol, + onOpen, +}: ProtocolDetailsPanelProps) => { + const { t } = useTranslation(); + + return ( +
+
+ {protocol.name} +
+ + {protocol.name} + +
+ +
+ +
+
+ + {t("Domain")} + +
+
+ + + {getHostname(protocol.websiteUrl)} + +
+
+ +
+
+ + {t("Tags")} + +
+
+ {protocol.tags.map((tag) => ( +
+ + {tag} + +
+ ))} +
+
+ +
+
+ + {t("Overview")} + +
+
+ + {protocol.description} + +
+
+
+ ); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/popup/views/Discover/components/ProtocolDetailsPanel/ +git commit -m "feat: add ProtocolDetailsPanel component for SlideupModal content" +``` + +--- + +### Task 9: Create ExpandedRecent and ExpandedDapps components + +**Files:** + +- Create: + `extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss` +- Create: + `extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss` + +- [ ] **Step 1: Create ExpandedRecent styles** + +Create +`extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.ExpandedRecent { + &__list { + display: flex; + flex-direction: column; + gap: pxToRem(24); + padding: 0 pxToRem(24); + } + + &__menu { + position: relative; + } + + &__menu-trigger { + background: transparent; + border: none; + cursor: pointer; + color: var(--sds-clr-gray-12); + padding: pxToRem(4); + + svg { + width: pxToRem(20); + height: pxToRem(20); + } + } + + &__dropdown { + position: absolute; + right: 0; + top: pxToRem(32); + width: pxToRem(180); + background: var(--sds-clr-gray-01); + border: 1px solid var(--sds-clr-gray-06); + border-radius: pxToRem(6); + box-shadow: 0 pxToRem(10) pxToRem(20) pxToRem(-10) rgba(0, 0, 0, 0.1); + padding: pxToRem(4); + z-index: 10; + } + + &__dropdown-item { + display: flex; + align-items: center; + gap: pxToRem(8); + padding: pxToRem(8); + cursor: pointer; + border-radius: pxToRem(4); + color: var(--sds-clr-red-09); + + svg { + width: pxToRem(16); + height: pxToRem(16); + } + + &:hover { + background: var(--sds-clr-gray-03); + } + } +} +``` + +- [ ] **Step 2: Create ExpandedRecent component** + +Create `extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx`: + +```tsx +import React, { useState } from "react"; +import { Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import { ProtocolRow } from "../ProtocolRow"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ExpandedRecentProps { + items: DiscoverData; + onBack: () => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; + onClearRecent: () => void; +} + +export const ExpandedRecent = ({ + items, + onBack, + onRowClick, + onOpenClick, + onClearRecent, +}: ExpandedRecentProps) => { + const { t } = useTranslation(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + return ( + <> + + + {isMenuOpen && ( +
+
{ + setIsMenuOpen(false); + onClearRecent(); + }} + data-testid="clear-recents-button" + > + + + {t("Clear recents")} + +
+
+ )} +
+ } + /> + +
+ {items.map((protocol) => ( + + ))} +
+
+ + ); +}; +``` + +- [ ] **Step 3: Create ExpandedDapps styles** + +Create +`extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.ExpandedDapps { + &__list { + display: flex; + flex-direction: column; + gap: pxToRem(24); + padding: 0 pxToRem(24); + } +} +``` + +- [ ] **Step 4: Create ExpandedDapps component** + +Create `extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx`: + +```tsx +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import { ProtocolRow } from "../ProtocolRow"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ExpandedDappsProps { + items: DiscoverData; + onBack: () => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const ExpandedDapps = ({ + items, + onBack, + onRowClick, + onOpenClick, +}: ExpandedDappsProps) => { + const { t } = useTranslation(); + + return ( + <> + + +
+ {items.map((protocol) => ( + + ))} +
+
+ + ); +}; +``` + +- [ ] **Step 5: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 6: Commit** + +```bash +git add extension/src/popup/views/Discover/components/ExpandedRecent/ extension/src/popup/views/Discover/components/ExpandedDapps/ +git commit -m "feat: add ExpandedRecent and ExpandedDapps sub-view components" +``` + +--- + +### Task 10: Create DiscoverWelcomeModal component + +**Files:** + +- Create: + `extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss` + +- [ ] **Step 1: Create DiscoverWelcomeModal styles** + +Create +`extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.DiscoverWelcomeModal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-index-modal); + padding: pxToRem(24); + + &__card { + background: var(--sds-clr-gray-01); + border: 1px solid var(--sds-clr-gray-06); + border-radius: pxToRem(16); + padding: pxToRem(24); + position: relative; + z-index: 2; + max-width: pxToRem(312); + width: 100%; + } + + &__icon { + width: pxToRem(40); + height: pxToRem(40); + border-radius: pxToRem(10); + background: var(--sds-clr-lilac-03, var(--sds-clr-gray-03)); + border: 1px solid var(--sds-clr-lilac-06, var(--sds-clr-gray-06)); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: pxToRem(16); + color: var(--sds-clr-gray-12); + + svg { + width: pxToRem(20); + height: pxToRem(20); + } + } + + &__title { + margin-bottom: pxToRem(12); + } + + &__body { + color: var(--sds-clr-gray-11); + margin-bottom: pxToRem(24); + + p { + margin-bottom: pxToRem(12); + + &:last-child { + margin-bottom: 0; + } + } + } +} +``` + +- [ ] **Step 2: Create DiscoverWelcomeModal component** + +Create +`extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx`: + +```tsx +import React from "react"; +import { Button, Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { LoadingBackground } from "popup/basics/LoadingBackground"; + +import "./styles.scss"; + +interface DiscoverWelcomeModalProps { + onDismiss: () => void; +} + +export const DiscoverWelcomeModal = ({ + onDismiss, +}: DiscoverWelcomeModalProps) => { + const { t } = useTranslation(); + + return ( + <> +
+
+
+ +
+
+ + {t("Welcome to Discover!")} + +
+
+ + {t( + "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.", + )} + + + {t( + "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", + )} + +
+ +
+
+ + + ); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/popup/views/Discover/components/DiscoverWelcomeModal/ +git commit -m "feat: add DiscoverWelcomeModal component for first-visit onboarding" +``` + +--- + +### Task 11: Create DiscoverHome component + +**Files:** + +- Create: `extension/src/popup/views/Discover/components/DiscoverHome/index.tsx` +- Create: + `extension/src/popup/views/Discover/components/DiscoverHome/styles.scss` + +- [ ] **Step 1: Create DiscoverHome styles** + +Create `extension/src/popup/views/Discover/components/DiscoverHome/styles.scss`: + +```scss +@use "../../../../styles/utils.scss" as *; + +.DiscoverHome { + &__sections { + display: flex; + flex-direction: column; + gap: pxToRem(32); + } + + &__footer { + display: flex; + flex-direction: column; + gap: pxToRem(8); + border-radius: pxToRem(16); + background-color: var(--sds-clr-gray-02); + padding: pxToRem(16); + margin-top: pxToRem(32); + + &__copy { + color: var(--sds-clr-gray-11); + font-size: pxToRem(12); + line-height: pxToRem(14); + } + } +} +``` + +- [ ] **Step 2: Create DiscoverHome component** + +Create `extension/src/popup/views/Discover/components/DiscoverHome/index.tsx`: + +```tsx +import React from "react"; +import { Icon } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import { TrendingCarousel } from "../TrendingCarousel"; +import { DiscoverSection } from "../DiscoverSection"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface DiscoverHomeProps { + trendingItems: DiscoverData; + recentItems: DiscoverData; + dappsItems: DiscoverData; + onClose: () => void; + onExpandRecent: () => void; + onExpandDapps: () => void; + onCardClick: (protocol: Protocol) => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const DiscoverHome = ({ + trendingItems, + recentItems, + dappsItems, + onClose, + onExpandRecent, + onExpandDapps, + onCardClick, + onRowClick, + onOpenClick, +}: DiscoverHomeProps) => { + const { t } = useTranslation(); + + return ( + <> + } + customBackAction={onClose} + /> + +
+ + + +
+ {dappsItems.length > 0 && ( +
+
+ {`${t( + "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.", + )} ${t("Freighter does not endorse any listed items.")}`} +
+
+ {t( + "By using these services, you act at your own risk, and Freighter or Stellar Development Foundation (SDF) bears no liability for any resulting losses or damages.", + )} +
+
+ )} +
+ + ); +}; +``` + +- [ ] **Step 3: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/popup/views/Discover/components/DiscoverHome/ +git commit -m "feat: add DiscoverHome main view with trending, sections, and footer" +``` + +--- + +### Task 12: Rewrite Discover orchestrator + +**Files:** + +- Modify: `extension/src/popup/views/Discover/index.tsx` +- Modify: `extension/src/popup/views/Discover/styles.scss` + +- [ ] **Step 1: Rewrite Discover styles** + +Replace the contents of `extension/src/popup/views/Discover/styles.scss` with: + +```scss +.Discover { + height: 100%; + display: flex; + flex-direction: column; + background: var(--sds-clr-gray-01); +} +``` + +- [ ] **Step 2: Rewrite Discover orchestrator** + +Replace the contents of `extension/src/popup/views/Discover/index.tsx` with: + +```tsx +import React, { useCallback, useState } from "react"; + +import { DiscoverData } from "@shared/api/types"; +import { openTab } from "popup/helpers/navigate"; +import { + addRecentProtocol, + clearRecentProtocols, +} from "popup/helpers/recentProtocols"; +import { SlideupModal } from "popup/components/SlideupModal"; +import { Loading } from "popup/components/Loading"; + +import { useDiscoverData } from "./hooks/useDiscoverData"; +import { useDiscoverWelcome } from "./hooks/useDiscoverWelcome"; +import { DiscoverHome } from "./components/DiscoverHome"; +import { ExpandedRecent } from "./components/ExpandedRecent"; +import { ExpandedDapps } from "./components/ExpandedDapps"; +import { ProtocolDetailsPanel } from "./components/ProtocolDetailsPanel"; +import { DiscoverWelcomeModal } from "./components/DiscoverWelcomeModal"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; +type DiscoverView = "main" | "recent" | "dapps"; + +interface DiscoverProps { + onClose: () => void; +} + +export const Discover = ({ onClose }: DiscoverProps) => { + const [activeView, setActiveView] = useState("main"); + const [selectedProtocol, setSelectedProtocol] = useState( + null, + ); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); + + const { isLoading, trendingItems, recentItems, dappsItems, refreshRecent } = + useDiscoverData(); + const { showWelcome, dismissWelcome } = useDiscoverWelcome(); + + const handleOpenProtocol = useCallback( + async (protocol: Protocol) => { + openTab(protocol.websiteUrl); + await addRecentProtocol(protocol.websiteUrl); + await refreshRecent(); + }, + [refreshRecent], + ); + + const handleRowClick = useCallback((protocol: Protocol) => { + setSelectedProtocol(protocol); + setIsDetailsOpen(true); + }, []); + + const handleDetailsOpen = useCallback( + async (protocol: Protocol) => { + setIsDetailsOpen(false); + setTimeout(async () => { + setSelectedProtocol(null); + await handleOpenProtocol(protocol); + }, 200); + }, + [handleOpenProtocol], + ); + + const handleClearRecent = useCallback(async () => { + await clearRecentProtocols(); + await refreshRecent(); + setActiveView("main"); + }, [refreshRecent]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {activeView === "main" && ( + setActiveView("recent")} + onExpandDapps={() => setActiveView("dapps")} + onCardClick={handleRowClick} + onRowClick={handleRowClick} + onOpenClick={handleOpenProtocol} + /> + )} + {activeView === "recent" && ( + setActiveView("main")} + onRowClick={handleRowClick} + onOpenClick={handleOpenProtocol} + onClearRecent={handleClearRecent} + /> + )} + {activeView === "dapps" && ( + setActiveView("main")} + onRowClick={handleRowClick} + onOpenClick={handleOpenProtocol} + /> + )} + + { + setIsDetailsOpen(open); + if (!open) { + setSelectedProtocol(null); + } + }} + > + {selectedProtocol ? ( + + ) : ( +
+ )} + + + {showWelcome && } +
+ ); +}; +``` + +- [ ] **Step 3: Delete old useGetDiscoverData hook** + +Delete `extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts` — it is +replaced by `useDiscoverData.ts`. + +- [ ] **Step 4: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 5: Commit** + +```bash +git add extension/src/popup/views/Discover/index.tsx extension/src/popup/views/Discover/styles.scss +git rm extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts +git commit -m "feat: rewrite Discover orchestrator with sub-views, modals, and data hooks" +``` + +--- + +### Task 13: Wire up the Sheet in Account view and update AccountHeader + +**Files:** + +- Modify: `extension/src/popup/views/Account/index.tsx` +- Modify: `extension/src/popup/components/account/AccountHeader/index.tsx` + +- [ ] **Step 1: Add isDiscoverOpen state and Sheet to Account view** + +In `extension/src/popup/views/Account/index.tsx`: + +Add imports at the top (with the existing imports): + +```typescript +import { useState } from "react"; +import { + Sheet, + SheetContent, + ScreenReaderOnly, + SheetTitle, +} from "popup/basics/shadcn/Sheet"; +import { Discover } from "popup/views/Discover"; +``` + +Update the `useEffect, useRef, useContext` import from React to also include +`useState`: + +```typescript +import React, { useEffect, useRef, useContext, useState } from "react"; +``` + +Inside the `Account` component, add state after the existing hooks (around line +53): + +```typescript +const [isDiscoverOpen, setIsDiscoverOpen] = useState(false); +``` + +Add `onDiscoverClick` prop to `AccountHeader` (around line 188): + +```tsx + setIsDiscoverOpen(true)} +/> +``` + +Add the Discover Sheet after the closing `` of the existing return but before +the final fragment close. Place it right before the final `` at the end of +the return (after the `NotFundedMessage` conditional block, around line 302): + +```tsx + + e.preventDefault()} + aria-describedby={undefined} + side="bottom" + className="Discover__sheet-wrapper" + > + + Discover + + setIsDiscoverOpen(false)} /> + + +``` + +- [ ] **Step 2: Update AccountHeader to accept onDiscoverClick prop** + +In `extension/src/popup/components/account/AccountHeader/index.tsx`: + +Add `onDiscoverClick` to the `AccountHeaderProps` interface: + +```typescript +interface AccountHeaderProps { + allowList: string[]; + currentAccountName: string; + isFunded: boolean; + onAllowListRemove: () => void; + onClickRow: (updatedValues: { + publicKey?: string; + network?: NetworkDetails; + }) => Promise; + publicKey: string; + roundedTotalBalanceUsd: string; + refreshHiddenCollectibles: () => Promise; + isCollectibleHidden: (collectionAddress: string, tokenId: string) => boolean; + onDiscoverClick: () => void; +} +``` + +Add `onDiscoverClick` to the destructured props: + +```typescript +export const AccountHeader = ({ + allowList, + currentAccountName, + isFunded, + onAllowListRemove, + onClickRow, + publicKey, + roundedTotalBalanceUsd, + refreshHiddenCollectibles, + isCollectibleHidden, + onDiscoverClick, +}: AccountHeaderProps) => { +``` + +Update the Discover button's `onClick` handler (around line 310). Change: + +```tsx +onClick={() => navigateTo(ROUTES.discover, navigate)} +``` + +to: + +```tsx +onClick = { onDiscoverClick }; +``` + +Remove the `ROUTES` import for `discover` if it's no longer used elsewhere in +this file (it is still used for other routes like `ROUTES.manageAssets`, +`ROUTES.settings`, etc., so keep the import but the `discover` route reference +is gone from this file). + +- [ ] **Step 3: Add Sheet wrapper styles** + +Add to `extension/src/popup/views/Discover/styles.scss`: + +```scss +.Discover { + height: 100%; + display: flex; + flex-direction: column; + background: var(--sds-clr-gray-01); + + &__sheet-wrapper { + height: var(--popup--height); + } +} +``` + +Note: the `&__sheet-wrapper` class is used on the `SheetContent` to constrain it +to the popup height. + +- [ ] **Step 4: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 5: Commit** + +```bash +git add extension/src/popup/views/Account/index.tsx extension/src/popup/components/account/AccountHeader/index.tsx extension/src/popup/views/Discover/styles.scss +git commit -m "feat: wire up Discover Sheet in Account view and update AccountHeader" +``` + +--- + +### Task 14: Remove the /discover route + +**Files:** + +- Modify: `extension/src/popup/constants/routes.ts` +- Modify: `extension/src/popup/Router.tsx` +- Modify: `extension/src/popup/constants/metricsNames.ts` +- Modify: `extension/src/popup/metrics/views.ts` + +- [ ] **Step 1: Remove discover from routes enum** + +In `extension/src/popup/constants/routes.ts`, remove: + +```typescript +discover = "/discover", +``` + +- [ ] **Step 2: Remove Discover route and import from Router** + +In `extension/src/popup/Router.tsx`: + +Remove the import (line 63): + +```typescript +import { Discover } from "popup/views/Discover"; +``` + +Remove the route (line 278): + +```tsx +} /> +``` + +- [ ] **Step 3: Remove discover metric name** + +In `extension/src/popup/constants/metricsNames.ts`, remove: + +```typescript +discover: "loaded screen: discover", +``` + +- [ ] **Step 4: Remove discover metric route mapping** + +In `extension/src/popup/metrics/views.ts`, remove: + +```typescript +[ROUTES.discover]: METRIC_NAMES.discover, +``` + +- [ ] **Step 5: Check for any remaining ROUTES.discover references** + +Run: `cd extension && grep -r "ROUTES.discover" src/` Expected: No matches +(AccountHeader was already updated in Task 13) + +- [ ] **Step 6: Verify build** + +Run: `cd extension && npx tsc --noEmit` Expected: No type errors + +- [ ] **Step 7: Commit** + +```bash +git add extension/src/popup/constants/routes.ts extension/src/popup/Router.tsx extension/src/popup/constants/metricsNames.ts extension/src/popup/metrics/views.ts +git commit -m "refactor: remove /discover route, now handled via Sheet overlay" +``` + +--- + +### Task 15: Update existing tests + +**Files:** + +- Modify: `extension/src/popup/views/__tests__/Discover.test.tsx` + +- [ ] **Step 1: Rewrite the Discover test** + +Replace the contents of `extension/src/popup/views/__tests__/Discover.test.tsx` +with: + +```tsx +import React from "react"; +import { render, waitFor, screen } from "@testing-library/react"; +import { + DEFAULT_NETWORKS, + MAINNET_NETWORK_DETAILS, +} from "@shared/constants/stellar"; +import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState"; +import * as ApiInternal from "@shared/api/internal"; +import * as RecentProtocols from "popup/helpers/recentProtocols"; +import { Wrapper, mockAccounts } from "../../__testHelpers__"; +import { Discover } from "../Discover"; + +// Mock browser.storage.local +jest.mock("webextension-polyfill", () => ({ + storage: { + local: { + get: jest.fn().mockResolvedValue({}), + set: jest.fn().mockResolvedValue(undefined), + remove: jest.fn().mockResolvedValue(undefined), + }, + }, + tabs: { + create: jest.fn(), + }, +})); + +const mockProtocols = [ + { + description: "A lending protocol", + name: "Blend", + iconUrl: "https://example.com/blend.png", + websiteUrl: "https://blend.capital", + tags: ["Lending", "DeFi"], + isBlacklisted: false, + backgroundUrl: "https://example.com/blend-bg.png", + isTrending: true, + }, + { + description: "An exchange", + name: "Soroswap", + iconUrl: "https://example.com/soroswap.png", + websiteUrl: "https://soroswap.finance", + tags: ["Exchange"], + isBlacklisted: false, + isTrending: false, + }, + { + description: "Blacklisted protocol", + name: "BadProtocol", + iconUrl: "https://example.com/bad.png", + websiteUrl: "https://bad.com", + tags: ["Scam"], + isBlacklisted: true, + isTrending: false, + }, +]; + +describe("Discover", () => { + beforeEach(() => { + jest.spyOn(ApiInternal, "getDiscoverData").mockResolvedValue(mockProtocols); + jest.spyOn(RecentProtocols, "getRecentProtocols").mockResolvedValue([]); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const renderDiscover = () => + render( + + + , + ); + + it("displays trending protocols in the carousel", async () => { + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("trending-carousel")).toBeInTheDocument(); + }); + + const trendingCards = screen.getAllByTestId("trending-card"); + expect(trendingCards).toHaveLength(1); + expect(trendingCards[0]).toHaveTextContent("Blend"); + }); + + it("displays dApps section with non-blacklisted protocols", async () => { + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-section-dapps")).toBeInTheDocument(); + }); + + const protocolRows = screen.getAllByTestId("protocol-row"); + // Blend + Soroswap (BadProtocol is blacklisted) + expect(protocolRows).toHaveLength(2); + expect(protocolRows[0]).toHaveTextContent("Blend"); + expect(protocolRows[1]).toHaveTextContent("Soroswap"); + }); + + it("hides recent section when no recent protocols exist", async () => { + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-section-dapps")).toBeInTheDocument(); + }); + + expect( + screen.queryByTestId("discover-section-recent"), + ).not.toBeInTheDocument(); + }); + + it("shows recent section when recent protocols exist", async () => { + jest + .spyOn(RecentProtocols, "getRecentProtocols") + .mockResolvedValue([ + { websiteUrl: "https://blend.capital", lastAccessed: Date.now() }, + ]); + + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-section-recent")).toBeInTheDocument(); + }); + }); +}); +``` + +- [ ] **Step 2: Run the tests** + +Run: +`cd extension && npx jest src/popup/views/__tests__/Discover.test.tsx --no-coverage` +Expected: All tests pass + +- [ ] **Step 3: Commit** + +```bash +git add extension/src/popup/views/__tests__/Discover.test.tsx +git commit -m "test: rewrite Discover tests for new Sheet-based component structure" +``` + +--- + +### Task 16: Manual visual verification + +- [ ] **Step 1: Build the extension** + +Run: `cd extension && yarn build:extension` Expected: Build succeeds with no +errors + +- [ ] **Step 2: Load in browser and verify** + +Load the built extension in Chrome as an unpacked extension from +`extension/build/`. + +Verify the following flows: + +1. Home screen shows the "Discover" button (compass icon + label) in the header +2. Clicking "Discover" slides the Discover Sheet up from the bottom, revealing + Home underneath +3. Trending carousel shows cards with background images, horizontally scrollable +4. Recent section is hidden (no recent visits yet) +5. dApps section shows all non-blacklisted protocols +6. Clicking a protocol row opens the protocol details SlideupModal (dark theme) +7. Clicking "Open" in the details modal opens the protocol in a new browser tab +8. After opening a protocol, the Recent section appears on return +9. Clicking "Recent >" expands to the full recent list with back arrow and + three-dot menu +10. "Clear recents" removes all recent entries and returns to main view +11. Clicking "dApps >" expands to the full dApps list +12. Back arrow in expanded views returns to the main Discover view +13. "X" button closes the Sheet with a smooth slide-down animation revealing the + Home screen +14. Welcome modal appears on first visit, "Let's go" dismisses it, does not + appear again + +- [ ] **Step 3: Fix any visual issues found during verification** + +Address spacing, colors, or layout issues comparing against the Figma designs. + +- [ ] **Step 4: Final commit** + +```bash +git add -A +git commit -m "fix: visual polish for Discover V2 after manual verification" +``` diff --git a/docs/superpowers/specs/2026-03-30-discover-v2-design.md b/docs/superpowers/specs/2026-03-30-discover-v2-design.md new file mode 100644 index 0000000000..2ae03267e3 --- /dev/null +++ b/docs/superpowers/specs/2026-03-30-discover-v2-design.md @@ -0,0 +1,340 @@ +# Discover V2 — Extension Design Spec + +Port the Discover tab refactoring from freighter-mobile (PR #780) to the +freighter browser extension. The mobile version has a bottom navigation bar, +in-app WebView browser, and tab management — none of which apply to the +extension. The extension version is simpler: a Sheet overlay with scrollable +sections, protocol details modal, and new-tab navigation. + +## Figma References + +- **Extension Discover tab**: Figma file `C3G0a4Gd6RQyplRBppGDsL`, nodes + `7839-5623`, `7839-5921` +- **Extension Recent expanded**: node `7840-31089` +- **Extension dApps expanded**: node `7839-5513` +- **Extension Protocol details**: node `7376-5216` +- **Extension Home with Discover button**: node `7301-18521` +- **Mobile Discover (reference)**: Figma file `KwkHXQxbNmDllwermJtnRu`, nodes + `11115-14760`, `9892-93605`, `10233-25250`, `10052-14241`, `10852-35337` + +## Decisions + +| Decision | Choice | Rationale | +| --------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------- | +| Main Discover entry | Sheet overlay (like AssetDetail) | Keeps Home mounted underneath for smooth slide-down reveal. Matches existing pattern. | +| Sub-views (Recent/dApps expanded) | Local state inside Sheet | Sheet stays open, content swaps. Instant transition. Same approach as mobile. | +| Protocol details | SlideupModal (like TransactionDetail) | Dark theme. Existing component, well-tested. | +| Recent protocols storage | Standalone helper + `browser.storage.local` | Self-contained, no Redux overhead. Custom hook for React integration. | +| Welcome modal storage | `browser.storage.local` flag | Consistent with recent protocols. Async read on mount is trivial. | +| Opening a protocol | `openTab(url)` — new browser tab | Extension is web-based, no need for in-app WebView. | +| Trending card size | 264x148px (near full-width) | ~85% of content width. One card visible + peek of next. Matches mobile proportions. | +| Route changes | Remove `/discover` route | Everything lives inside the Sheet overlay now. | +| Analytics/metrics | Deferred | Implement after initial feature is stable. | +| E2E tests | Deferred | Implement after initial feature is stable. | + +## Data Layer + +### DiscoverData type update + +`@shared/api/types/types.ts` — add two fields to the existing `DiscoverData` +type: + +```typescript +export type DiscoverData = { + description: string; + iconUrl: string; + name: string; + websiteUrl: string; + tags: string[]; + isBlacklisted: boolean; + backgroundUrl?: string; // NEW — trending card background image + isTrending: boolean; // NEW — whether protocol appears in Trending +}[]; +``` + +### getDiscoverData() update + +`@shared/api/internal.ts` — map `background_url` and `is_trending` from the +backend response (already returned by the API, just not mapped): + +```typescript +return parsedResponse.data.protocols.map((entry) => ({ + // ...existing fields... + backgroundUrl: entry.background_url, + isTrending: entry.is_trending, +})); +``` + +### Recent protocols helper + +`popup/helpers/recentProtocols.ts` — standalone module using +`browser.storage.local`: + +- `getRecentProtocols(): Promise` — reads stored entries +- `addRecentProtocol(url: string): Promise` — adds or moves URL to front, + caps at 5 entries +- `clearRecentProtocols(): Promise` — removes all entries + +Storage shape: + +```typescript +interface RecentProtocolEntry { + websiteUrl: string; + lastAccessed: number; // timestamp +} +// Stored under key "recentProtocols" in browser.storage.local +``` + +### useDiscoverData hook + +`popup/views/Discover/hooks/useDiscoverData.ts` — replaces the existing +`useGetDiscoverData`: + +- Fetches protocols via `getDiscoverData()` +- Reads recent protocols via `getRecentProtocols()` +- Returns computed lists: + - `trendingItems` — protocols where `isTrending === true` + - `recentItems` — recent URLs matched against fetched protocols (max 5) + - `dappsItems` — all non-blacklisted protocols + - `allProtocols` — full list for lookups + - `isLoading`, `error` — request state +- Exposes `refreshRecent()` to re-read recent protocols after a visit + +### useDiscoverWelcome hook + +`popup/views/Discover/hooks/useDiscoverWelcome.ts`: + +- Reads `hasSeenDiscoverWelcome` from `browser.storage.local` on mount +- Returns `{ showWelcome: boolean, dismissWelcome: () => void }` +- `dismissWelcome()` writes flag to storage and sets local state to false + +## Navigation & Transitions + +### Main Discover screen — Sheet overlay + +The `Account` view (`popup/views/Account/index.tsx`) is the common parent of +`AccountHeader` and `AccountAssets`. It owns the `isDiscoverOpen` state and +passes it down: + +- `Account` holds `const [isDiscoverOpen, setIsDiscoverOpen] = useState(false)` +- `AccountHeader` receives `onDiscoverClick` prop — the Discover button calls + `onDiscoverClick()` instead of `navigateTo` +- The `Sheet` + `SheetContent side="bottom"` is rendered in the `Account` view + itself (same level as the existing layout), controlled by `isDiscoverOpen` +- Home screen stays mounted underneath — smooth slide-down reveal on close +- The Discover content renders inside the Sheet +- Header uses `SubviewHeader` with `customBackIcon` (X icon) and + `customBackAction` calling `setIsDiscoverOpen(false)` + +### Sub-views inside the Sheet + +Local state variable controls which content is rendered: + +```typescript +type DiscoverView = "main" | "recent" | "dapps"; +const [activeView, setActiveView] = useState("main"); +``` + +- `'main'` — DiscoverHome (trending + sections + footer) +- `'recent'` — ExpandedRecent (full list with clear menu) +- `'dapps'` — ExpandedDapps (full list) + +Instant content swap between sub-views (no transition animation). Back buttons +in expanded views call `setActiveView('main')`. + +### Protocol details — SlideupModal + +- Rendered within the Sheet, layered on top of the current sub-view +- Managed by `selectedProtocol` local state (null = hidden, protocol object = + shown) +- Uses the existing `SlideupModal` component with dark theme + (`--sds-clr-gray-01` background) +- Dismissed by clicking the `LoadingBackground` overlay + +### Opening a protocol + +- Calls `openTab(protocol.websiteUrl)` to open a new browser tab +- Calls `addRecentProtocol(protocol.websiteUrl)` to record the visit +- Calls `refreshRecent()` to update the Recent section + +### Welcome modal + +- Rendered within the Sheet on first visit (when `showWelcome` is true) +- Overlay modal on top of the Discover content +- "Let's go" button calls `dismissWelcome()` + +### Route cleanup + +- Remove the `/discover` route from `routes.ts` and `Router.tsx` +- Remove the old `Discover` view import from `Router.tsx` +- Update `AccountHeader` to toggle Sheet state instead of calling `navigateTo` + +## Components + +### Discover (index.tsx) — orchestrator + +Main component rendered inside the Sheet. Manages: + +- `activeView` state (`'main' | 'recent' | 'dapps'`) +- `selectedProtocol` state (for SlideupModal) +- Welcome modal visibility (via `useDiscoverWelcome`) +- Data fetching (via `useDiscoverData`) +- Renders the active sub-view + ProtocolDetailsPanel in SlideupModal + + DiscoverWelcomeModal + +### DiscoverHome — main scrollable view + +Content of the `'main'` sub-view: + +- `SubviewHeader` with "Discover" title, X close icon via `customBackIcon` / + `customBackAction` +- `TrendingCarousel` section with "Trending" label +- `DiscoverSection` for Recent (hidden if no recent items) — chevron navigates + to `'recent'` view +- `DiscoverSection` for dApps — chevron navigates to `'dapps'` view +- Legal disclaimer footer (same text as current implementation) + +### TrendingCarousel + +Horizontal scrollable container: + +- Cards: 264px wide x 148px tall, 12px border-radius +- Background image with gradient overlay (bottom-to-top, from + `--sds-clr-gray-03` to transparent) +- Protocol name (bottom-left, semi-bold 14px) + primary tag (bottom-right, + medium 14px, secondary color) +- 16px gap between cards +- Container has horizontal overflow scroll, no scrollbar +- Tapping a card sets `selectedProtocol` (opens details modal) + +### DiscoverSection + +Reusable section wrapper (used for both Recent and dApps on the main view): + +- Header row: section title (Text, sm, semi-bold) + `Icon.ChevronRight` — + clickable, triggers expand callback +- Vertical list of up to 5 `ProtocolRow` items +- 24px gap between rows + +### ProtocolRow + +Reusable row component (used in sections and expanded views): + +- 32px x 32px rounded (10px radius) protocol icon +- 12px gap to text column +- Protocol name (Text, sm, medium) +- Primary tag as subtitle (Text, xs, medium, `--sds-clr-gray-11`) +- "Open" pill button on far right: border `--sds-clr-gray-06`, text + + `Icon.LinkExternal01` (14px) +- Tapping the row (not the button) opens protocol details modal +- Tapping "Open" button calls `openTab(url)` + `addRecentProtocol(url)` + +### ExpandedRecent + +Full-page sub-view for recent protocols: + +- `SubviewHeader` with back arrow (default), "Recent" title, `rightButton` with + three-dot menu icon +- Three-dot menu: dropdown with "Clear recents" option (red text + `--sds-clr-red-09`, trash icon) +- Full scrollable list of recent `ProtocolRow` items +- "Clear recents" calls `clearRecentProtocols()` + `refreshRecent()` + returns + to main view + +### ExpandedDapps + +Full-page sub-view for all dApps: + +- `SubviewHeader` with back arrow, "dApps" title, no right button +- Full scrollable list of all `ProtocolRow` items + +### ProtocolDetailsPanel + +Content rendered inside `SlideupModal` (dark theme): + +- Header row: 40px icon (10px radius) + protocol name (Text, lg, medium) + + filled "Open" button (dark bg, white text, pill shape) +- Domain section: "Domain" label (Text, xs, medium, `--sds-clr-gray-11`) + globe + icon + hostname (Text, sm, medium) +- Tags section: "Tags" label + pill badges (success variant, lime colors) +- Overview section: "Overview" label + description text (Text, sm, regular) +- 24px vertical spacing between sections +- "Open" button calls `openTab(url)` + `addRecentProtocol(url)` + +### DiscoverWelcomeModal + +First-time onboarding modal: + +- Rendered as an overlay on top of Discover content (using `LoadingBackground` + + positioned card) +- Compass icon in a rounded container +- "Welcome to Discover!" title +- Two paragraphs: ecosystem intro + third-party disclaimer +- "Let's go" CTA button — dismisses the modal and writes flag to + `browser.storage.local` + +## File Structure + +``` +@shared/api/ + types/types.ts # UPDATE: add backgroundUrl, isTrending to DiscoverData + internal.ts # UPDATE: map background_url, is_trending + +extension/src/popup/ + views/Discover/ + index.tsx # REWRITE: orchestrator (sub-views, modals, data) + styles.scss # REWRITE: top-level styles + hooks/ + useDiscoverData.ts # NEW: replaces useGetDiscoverData + useDiscoverWelcome.ts # NEW: browser.storage.local welcome flag + components/ + DiscoverHome/ + index.tsx # NEW: main scrollable view + styles.scss + TrendingCarousel/ + index.tsx # NEW: horizontal card carousel + styles.scss + DiscoverSection/ + index.tsx # NEW: section wrapper (title + chevron + rows) + styles.scss + ProtocolRow/ + index.tsx # NEW: reusable protocol row + styles.scss + ExpandedRecent/ + index.tsx # NEW: full recent list with clear menu + styles.scss + ExpandedDapps/ + index.tsx # NEW: full dApps list + styles.scss + ProtocolDetailsPanel/ + index.tsx # NEW: SlideupModal content + styles.scss + DiscoverWelcomeModal/ + index.tsx # NEW: first-time modal + styles.scss + helpers/ + recentProtocols.ts # NEW: browser.storage.local CRUD + views/Account/ + index.tsx # UPDATE: add isDiscoverOpen state + Discover Sheet + components/account/ + AccountHeader/index.tsx # UPDATE: Discover button calls onDiscoverClick prop + +MODIFY: + popup/constants/routes.ts # remove discover route + popup/Router.tsx # remove Discover route + import + +REMOVE: + popup/views/Discover/hooks/useGetDiscoverData.ts # replaced by useDiscoverData + +REWRITE: + popup/views/__tests__/Discover.test.tsx # rewrite for new Sheet-based structure +``` + +## Out of Scope + +- Analytics/metrics events — deferred to follow-up +- E2E tests — deferred to follow-up +- Bottom navigation bar (mobile-only concept) +- In-app WebView / tab management (mobile-only concept) +- Search/URL bar (mobile-only concept) +- Tab overview grid (mobile-only concept) diff --git a/extension/src/popup/views/__tests__/Discover.test.tsx b/extension/src/popup/views/__tests__/Discover.test.tsx index 471ac10a09..b35cf675df 100644 --- a/extension/src/popup/views/__tests__/Discover.test.tsx +++ b/extension/src/popup/views/__tests__/Discover.test.tsx @@ -21,6 +21,8 @@ describe("Discover view", () => { websiteUrl: "https://foo.com", tags: ["tag1", "tag2"], isBlacklisted: false, + backgroundUrl: undefined, + isTrending: false, }, { description: "description text", @@ -29,6 +31,8 @@ describe("Discover view", () => { websiteUrl: "https://baz.com", tags: ["tag1", "tag2"], isBlacklisted: true, + backgroundUrl: undefined, + isTrending: false, }, ]), ); From 3b805d1d0cd8bb2e8b66b407647ed871bfd94ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 20:53:26 -0700 Subject: [PATCH 02/44] feat: add recentProtocols helper for browser.storage.local --- .../src/popup/helpers/recentProtocols.ts | 28 +++++++++++++++++++ .../Discover/hooks/useDiscoverWelcome.ts | 25 +++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 extension/src/popup/helpers/recentProtocols.ts create mode 100644 extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts diff --git a/extension/src/popup/helpers/recentProtocols.ts b/extension/src/popup/helpers/recentProtocols.ts new file mode 100644 index 0000000000..2cb70b3d64 --- /dev/null +++ b/extension/src/popup/helpers/recentProtocols.ts @@ -0,0 +1,28 @@ +import browser from "webextension-polyfill"; + +const STORAGE_KEY = "recentProtocols"; +const MAX_RECENT = 5; + +export interface RecentProtocolEntry { + websiteUrl: string; + lastAccessed: number; +} + +export const getRecentProtocols = async (): Promise => { + const result = await browser.storage.local.get(STORAGE_KEY); + return (result[STORAGE_KEY] as RecentProtocolEntry[]) || []; +}; + +export const addRecentProtocol = async (websiteUrl: string): Promise => { + const existing = await getRecentProtocols(); + const filtered = existing.filter((entry) => entry.websiteUrl !== websiteUrl); + const updated = [{ websiteUrl, lastAccessed: Date.now() }, ...filtered].slice( + 0, + MAX_RECENT, + ); + await browser.storage.local.set({ [STORAGE_KEY]: updated }); +}; + +export const clearRecentProtocols = async (): Promise => { + await browser.storage.local.remove(STORAGE_KEY); +}; diff --git a/extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts b/extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts new file mode 100644 index 0000000000..300577f288 --- /dev/null +++ b/extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts @@ -0,0 +1,25 @@ +import { useCallback, useEffect, useState } from "react"; +import browser from "webextension-polyfill"; + +const STORAGE_KEY = "hasSeenDiscoverWelcome"; + +export const useDiscoverWelcome = () => { + const [showWelcome, setShowWelcome] = useState(false); + + useEffect(() => { + const checkWelcome = async () => { + const result = await browser.storage.local.get(STORAGE_KEY); + if (!result[STORAGE_KEY]) { + setShowWelcome(true); + } + }; + checkWelcome(); + }, []); + + const dismissWelcome = useCallback(async () => { + setShowWelcome(false); + await browser.storage.local.set({ [STORAGE_KEY]: true }); + }, []); + + return { showWelcome, dismissWelcome }; +}; From 5746f87ebdc30980615e1736c308203bd3dda1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:00:53 -0700 Subject: [PATCH 03/44] feat: add useDiscoverData hook with trending, recent, and dapps lists Co-Authored-By: Claude Sonnet 4.6 --- .../views/Discover/hooks/useDiscoverData.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 extension/src/popup/views/Discover/hooks/useDiscoverData.ts diff --git a/extension/src/popup/views/Discover/hooks/useDiscoverData.ts b/extension/src/popup/views/Discover/hooks/useDiscoverData.ts new file mode 100644 index 0000000000..9db30a6f25 --- /dev/null +++ b/extension/src/popup/views/Discover/hooks/useDiscoverData.ts @@ -0,0 +1,110 @@ +import { useCallback, useEffect, useReducer } from "react"; +import { captureException } from "@sentry/browser"; + +import { getDiscoverData } from "@shared/api/internal"; +import { DiscoverData } from "@shared/api/types"; +import { + getRecentProtocols, + RecentProtocolEntry, +} from "popup/helpers/recentProtocols"; + +interface DiscoverDataState { + isLoading: boolean; + error: unknown | null; + allProtocols: DiscoverData; + recentEntries: RecentProtocolEntry[]; +} + +type Action = + | { type: "FETCH_START" } + | { + type: "FETCH_SUCCESS"; + payload: { + protocols: DiscoverData; + recentEntries: RecentProtocolEntry[]; + }; + } + | { type: "FETCH_ERROR"; payload: unknown } + | { type: "REFRESH_RECENT"; payload: RecentProtocolEntry[] }; + +const initialState: DiscoverDataState = { + isLoading: true, + error: null, + allProtocols: [], + recentEntries: [], +}; + +const reducer = ( + state: DiscoverDataState, + action: Action, +): DiscoverDataState => { + switch (action.type) { + case "FETCH_START": + return { ...state, isLoading: true, error: null }; + case "FETCH_SUCCESS": + return { + isLoading: false, + error: null, + allProtocols: action.payload.protocols, + recentEntries: action.payload.recentEntries, + }; + case "FETCH_ERROR": + return { ...state, isLoading: false, error: action.payload }; + case "REFRESH_RECENT": + return { ...state, recentEntries: action.payload }; + default: + return state; + } +}; + +export const useDiscoverData = () => { + const [state, dispatch] = useReducer(reducer, initialState); + + const fetchData = useCallback(async () => { + dispatch({ type: "FETCH_START" }); + try { + const [protocols, recentEntries] = await Promise.all([ + getDiscoverData(), + getRecentProtocols(), + ]); + dispatch({ + type: "FETCH_SUCCESS", + payload: { protocols, recentEntries }, + }); + } catch (error) { + dispatch({ type: "FETCH_ERROR", payload: error }); + captureException(`Error loading discover data - ${error}`); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const refreshRecent = useCallback(async () => { + const recentEntries = await getRecentProtocols(); + dispatch({ type: "REFRESH_RECENT", payload: recentEntries }); + }, []); + + const allowedProtocols = state.allProtocols.filter((p) => !p.isBlacklisted); + + const trendingItems = allowedProtocols.filter((p) => p.isTrending); + + const recentItems = state.recentEntries + .map((entry) => + allowedProtocols.find((p) => p.websiteUrl === entry.websiteUrl), + ) + .filter(Boolean) as DiscoverData; + + const dappsItems = allowedProtocols; + + return { + isLoading: state.isLoading, + error: state.error, + trendingItems, + recentItems, + dappsItems, + allProtocols: allowedProtocols, + refreshRecent, + }; +}; From a8c48667848115942d2c7acbfadd7ae80a1f4afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:02:43 -0700 Subject: [PATCH 04/44] feat: add ProtocolRow component for Discover sections --- .../src/popup/locales/en/translation.json | 4 ++ .../src/popup/locales/pt/translation.json | 4 ++ .../components/DiscoverSection/index.tsx | 61 ++++++++++++++++++ .../components/DiscoverSection/styles.scss | 23 +++++++ .../Discover/components/ProtocolRow/index.tsx | 62 +++++++++++++++++++ .../components/ProtocolRow/styles.scss | 48 ++++++++++++++ .../components/TrendingCarousel/index.tsx | 62 +++++++++++++++++++ .../components/TrendingCarousel/styles.scss | 58 +++++++++++++++++ 8 files changed, 322 insertions(+) create mode 100644 extension/src/popup/views/Discover/components/DiscoverSection/index.tsx create mode 100644 extension/src/popup/views/Discover/components/DiscoverSection/styles.scss create mode 100644 extension/src/popup/views/Discover/components/ProtocolRow/index.tsx create mode 100644 extension/src/popup/views/Discover/components/ProtocolRow/styles.scss create mode 100644 extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx create mode 100644 extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 2f86fb4414..957869a9fc 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -163,6 +163,7 @@ "Discover": "Discover", "Do not proceed": "Do not proceed", "Do this later": "Do this later", + "Domain": "Domain", "Don’t share this phrase with anyone": "Don’t share this phrase with anyone", "Done": "Done", "Download on iOS or Android today": "Download on iOS or Android today", @@ -390,6 +391,7 @@ "Order is incorrect, try again": "Order is incorrect, try again", "Overridden response": "Overridden response", "Override Blockaid security responses for testing different security states (DEV only)": "Override Blockaid security responses for testing different security states (DEV only)", + "Overview": "Overview", "Parameters": "Parameters", "Passphrase": "Passphrase", "Password": "Password", @@ -526,6 +528,7 @@ "Swapped!": "Swapped!", "Swapping": "Swapping", "Switch to this network": "Switch to this network", + "Tags": "Tags", "Terms of Service": "Terms of Service", "Terms of Use": "Terms of Use", "The authorization entry is for": "The authorization entry is for", @@ -601,6 +604,7 @@ "Transaction Timeout": "Transaction Timeout", "Transfer from another account": "Transfer from another account", "Transfer from Coinbase, buy with debit and credit cards or bank transfer *": "Transfer from Coinbase, buy with debit and credit cards or bank transfer *", + "Trending": "Trending", "trustlines": "trustlines", "Trustor": "Trustor", "Type": "Type", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index acf030f7ff..132a347388 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -163,6 +163,7 @@ "Discover": "Descobrir", "Do not proceed": "Não prosseguir", "Do this later": "Fazer isso depois", + "Domain": "Domain", "Don’t share this phrase with anyone": "Não compartilhe esta frase com ninguém", "Done": "Concluído", "Download on iOS or Android today": "Baixe no iOS ou Android hoje", @@ -393,6 +394,7 @@ "Order is incorrect, try again": "A ordem está incorreta, tente novamente", "Overridden response": "Overridden response", "Override Blockaid security responses for testing different security states (DEV only)": "Override Blockaid security responses for testing different security states (DEV only)", + "Overview": "Overview", "Parameters": "Parâmetros", "Passphrase": "Frase secreta", "Password": "Senha", @@ -529,6 +531,7 @@ "Swapped!": "Trocado!", "Swapping": "Trocando", "Switch to this network": "Alternar para esta rede", + "Tags": "Tags", "Terms of Service": "Termos de Serviço", "Terms of Use": "Termos de Uso", "The authorization entry is for": "A entrada de autorização é para", @@ -604,6 +607,7 @@ "Transaction Timeout": "Tempo Limite da Transação", "Transfer from another account": "Transferir de outra conta", "Transfer from Coinbase, buy with debit and credit cards or bank transfer *": "Transferir do Coinbase, comprar com cartões de débito e crédito ou transferência bancária *", + "Trending": "Trending", "trustlines": "linhas de confiança", "Trustor": "Fidedigno", "Type": "Tipo", diff --git a/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx b/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx new file mode 100644 index 0000000000..8192d487ed --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { Icon, Text } from "@stellar/design-system"; + +import { DiscoverData } from "@shared/api/types"; +import { ProtocolRow } from "../ProtocolRow"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +const MAX_VISIBLE = 5; + +interface DiscoverSectionProps { + title: string; + items: DiscoverData; + onExpand: () => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const DiscoverSection = ({ + title, + items, + onExpand, + onRowClick, + onOpenClick, +}: DiscoverSectionProps) => { + if (items.length === 0) { + return null; + } + + const visibleItems = items.slice(0, MAX_VISIBLE); + + return ( +
+
+ + {title} + + +
+
+ {visibleItems.map((protocol) => ( + + ))} +
+
+ ); +}; diff --git a/extension/src/popup/views/Discover/components/DiscoverSection/styles.scss b/extension/src/popup/views/Discover/components/DiscoverSection/styles.scss new file mode 100644 index 0000000000..2b5096cb29 --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverSection/styles.scss @@ -0,0 +1,23 @@ +@use "../../../../styles/utils.scss" as *; + +.DiscoverSection { + &__header { + display: flex; + align-items: center; + gap: pxToRem(4); + cursor: pointer; + margin-bottom: pxToRem(24); + color: var(--sds-clr-gray-12); + + svg { + width: pxToRem(16); + height: pxToRem(16); + } + } + + &__list { + display: flex; + flex-direction: column; + gap: pxToRem(24); + } +} diff --git a/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx b/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx new file mode 100644 index 0000000000..d45d33d63c --- /dev/null +++ b/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ProtocolRowProps { + protocol: Protocol; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const ProtocolRow = ({ + protocol, + onRowClick, + onOpenClick, +}: ProtocolRowProps) => { + const { t } = useTranslation(); + + return ( +
onRowClick(protocol)} + > + {protocol.name} +
+ + {protocol.name} + +
+ + {protocol.tags[0]} + +
+
+ +
+ ); +}; diff --git a/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss b/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss new file mode 100644 index 0000000000..76c15688c9 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss @@ -0,0 +1,48 @@ +@use "../../../../styles/utils.scss" as *; + +.ProtocolRow { + display: flex; + gap: pxToRem(12); + align-items: center; + cursor: pointer; + + &__icon { + height: pxToRem(32); + width: pxToRem(32); + border-radius: pxToRem(10); + object-fit: cover; + flex-shrink: 0; + } + + &__label { + display: flex; + flex-direction: column; + flex: 1 0 0; + min-width: 0; + + &__subtitle { + color: var(--sds-clr-gray-11); + } + } + + &__open-button { + border-radius: 100px; + border: 1px solid var(--sds-clr-gray-06); + color: var(--sds-clr-gray-12); + display: flex; + gap: pxToRem(4); + padding: pxToRem(6) pxToRem(10); + justify-content: center; + align-items: center; + cursor: pointer; + background: transparent; + text-decoration: none; + flex-shrink: 0; + + &__icon { + height: pxToRem(14); + width: pxToRem(14); + color: var(--sds-clr-gray-09); + } + } +} diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx new file mode 100644 index 0000000000..57faea459a --- /dev/null +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface TrendingCarouselProps { + items: DiscoverData; + onCardClick: (protocol: Protocol) => void; +} + +export const TrendingCarousel = ({ + items, + onCardClick, +}: TrendingCarouselProps) => { + const { t } = useTranslation(); + + if (items.length === 0) { + return null; + } + + return ( +
+
+ + {t("Trending")} + +
+
+ {items.map((protocol) => ( +
onCardClick(protocol)} + > +
+
+ + {protocol.name} + +
+ + {protocol.tags[0]} + +
+
+
+ ))} +
+
+ ); +}; diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss new file mode 100644 index 0000000000..f4fc2ca1c6 --- /dev/null +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss @@ -0,0 +1,58 @@ +@use "../../../../styles/utils.scss" as *; + +.TrendingCarousel { + &__label { + margin-bottom: pxToRem(12); + } + + &__scroll-container { + display: flex; + gap: pxToRem(16); + overflow-x: auto; + scroll-snap-type: x mandatory; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } + + &__card { + min-width: pxToRem(264); + height: pxToRem(148); + border-radius: pxToRem(12); + background-color: var(--sds-clr-gray-03); + background-size: cover; + background-position: center; + position: relative; + overflow: hidden; + cursor: pointer; + scroll-snap-align: start; + flex-shrink: 0; + + &__gradient { + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + transparent 40%, + var(--sds-clr-gray-03) 100% + ); + } + + &__content { + position: absolute; + bottom: pxToRem(12); + left: pxToRem(12); + right: pxToRem(12); + display: flex; + justify-content: space-between; + align-items: flex-end; + } + + &__tag { + color: var(--sds-clr-gray-11); + } + } +} From 95ea090834c6639ebb6a3102df968a766468e593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:03:25 -0700 Subject: [PATCH 05/44] feat: add ProtocolDetailsPanel component for SlideupModal content --- .../components/ProtocolDetailsPanel/index.tsx | 102 ++++++++++++++++++ .../ProtocolDetailsPanel/styles.scss | 83 ++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx create mode 100644 extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss diff --git a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx new file mode 100644 index 0000000000..4e2098dda8 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ProtocolDetailsPanelProps { + protocol: Protocol; + onOpen: (protocol: Protocol) => void; +} + +const getHostname = (url: string): string => { + try { + return new URL(url).hostname; + } catch { + return url; + } +}; + +export const ProtocolDetailsPanel = ({ + protocol, + onOpen, +}: ProtocolDetailsPanelProps) => { + const { t } = useTranslation(); + + return ( +
+
+ {protocol.name} +
+ + {protocol.name} + +
+ +
+ +
+
+ + {t("Domain")} + +
+
+ + + {getHostname(protocol.websiteUrl)} + +
+
+ +
+
+ + {t("Tags")} + +
+
+ {protocol.tags.map((tag) => ( +
+ + {tag} + +
+ ))} +
+
+ +
+
+ + {t("Overview")} + +
+
+ + {protocol.description} + +
+
+
+ ); +}; diff --git a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss new file mode 100644 index 0000000000..a22a7a6af6 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss @@ -0,0 +1,83 @@ +@use "../../../../styles/utils.scss" as *; + +.ProtocolDetailsPanel { + padding: pxToRem(32) pxToRem(16); + + &__header { + display: flex; + align-items: center; + gap: pxToRem(12); + margin-bottom: pxToRem(24); + } + + &__icon { + width: pxToRem(40); + height: pxToRem(40); + border-radius: pxToRem(10); + object-fit: cover; + flex-shrink: 0; + } + + &__name { + flex: 1 0 0; + min-width: 0; + } + + &__open-button { + background: var(--sds-clr-gray-12); + color: var(--sds-clr-gray-01); + border: none; + border-radius: 100px; + display: flex; + gap: pxToRem(4); + padding: pxToRem(6) pxToRem(10); + justify-content: center; + align-items: center; + cursor: pointer; + flex-shrink: 0; + + &__icon { + height: pxToRem(14); + width: pxToRem(14); + } + } + + &__section { + margin-bottom: pxToRem(24); + + &__label { + color: var(--sds-clr-gray-11); + margin-bottom: pxToRem(8); + } + } + + &__domain { + display: flex; + align-items: center; + gap: pxToRem(4); + + svg { + width: pxToRem(16); + height: pxToRem(16); + color: var(--sds-clr-gray-11); + } + } + + &__tags { + display: flex; + gap: pxToRem(8); + flex-wrap: wrap; + } + + &__tag { + border-radius: 100px; + background: var(--sds-clr-lime-02); + border: 1px solid var(--sds-clr-lime-06); + padding: pxToRem(2) pxToRem(8); + color: var(--sds-clr-lime-11); + } + + &__description { + color: var(--sds-clr-gray-12); + } +} From 9890836d4bbb51f762bf9b3036993109d4995ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:12:57 -0700 Subject: [PATCH 06/44] feat: add ExpandedRecent and ExpandedDapps sub-view components --- .../src/popup/locales/en/translation.json | 7 ++ .../src/popup/locales/pt/translation.json | 7 ++ .../components/DiscoverHome/index.tsx | 82 +++++++++++++++++++ .../components/DiscoverHome/styles.scss | 25 ++++++ .../components/DiscoverWelcomeModal/index.tsx | 56 +++++++++++++ .../DiscoverWelcomeModal/styles.scss | 57 +++++++++++++ .../components/ExpandedDapps/index.tsx | 45 ++++++++++ .../components/ExpandedDapps/styles.scss | 10 +++ .../components/ExpandedRecent/index.tsx | 80 ++++++++++++++++++ .../components/ExpandedRecent/styles.scss | 59 +++++++++++++ 10 files changed, 428 insertions(+) create mode 100644 extension/src/popup/views/Discover/components/DiscoverHome/index.tsx create mode 100644 extension/src/popup/views/Discover/components/DiscoverHome/styles.scss create mode 100644 extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx create mode 100644 extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss create mode 100644 extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx create mode 100644 extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss create mode 100644 extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx create mode 100644 extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 957869a9fc..8a2209883d 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -95,6 +95,7 @@ "Choose your method": "Choose your method", "Clear Flags": "Clear Flags", "Clear Override": "Clear Override", + "Clear recents": "Clear recents", "Close": "Close", "Coinbase Logo": "Coinbase Logo", "Collectible": "Collectible", @@ -144,6 +145,7 @@ "Create new wallet": "Create new wallet", "Current Network": "Current Network", "Custom": "Custom", + "dApps": "dApps", "Dapps": "Dapps", "data entries": "data entries", "Debug": "Debug", @@ -310,6 +312,7 @@ "Leave Feedback": "Leave Feedback", "Leave feedback about Blockaid warnings and messages": "Leave feedback about Blockaid warnings and messages", "Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust.": "Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust.", + "Let's go": "Let's go", "Limit": "Limit", "Links": "Links", "Liquidity Pool ID": "Liquidity Pool ID", @@ -424,6 +427,7 @@ "Ready to migrate": "Ready to migrate", "Receive": "Receive", "Received": "Received", + "Recent": "Recent", "Recents": "Recents", "Recovery Phrase": "Recovery Phrase", "Refresh metadata": "Refresh metadata", @@ -546,6 +550,7 @@ "The website <1>{url} does not use an SSL certificate.": "The website <1>{url} does not use an SSL certificate.", "There are no sites to display at this moment.": "There are no sites to display at this moment.", "These assets are not on any of your lists. Proceed with caution before adding.": "These assets are not on any of your lists. Proceed with caution before adding.", + "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.": "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", "These words are your wallet’s keys—store them securely to keep your funds safe.": "These words are your wallet’s keys—store them securely to keep your funds safe.", "This asset does not appear safe for the following reasons.": "This asset does not appear safe for the following reasons.", "This asset has a balance": "This asset has a balance", @@ -662,6 +667,7 @@ "We were unable to scan this transaction for security threats": "We were unable to scan this transaction for security threats", "WEBSITE CONNECTION IS NOT SECURE": "WEBSITE CONNECTION IS NOT SECURE", "Welcome back": "Welcome back", + "Welcome to Discover!": "Welcome to Discover!", "What is this transaction for? (optional)": "What is this transaction for? (optional)", "What’s new": "What’s new", "Wrong simulation result": "Wrong simulation result", @@ -697,6 +703,7 @@ "Your account data could not be fetched at this time.": "Your account data could not be fetched at this time.", "Your assets": "Your assets", "Your available XLM balance is not enough to pay for the transaction fee.": "Your available XLM balance is not enough to pay for the transaction fee.", + "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.": "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.", "Your recovery phrase": "Your recovery phrase", "Your Recovery Phrase": "Your Recovery Phrase", "Your recovery phrase gives you access to your account and is the only way to access it in a new browser.": "Your recovery phrase gives you access to your account and is the only way to access it in a new browser.", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 132a347388..d6790ea429 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -95,6 +95,7 @@ "Choose your method": "Escolha seu método", "Clear Flags": "Limpar Flags", "Clear Override": "Clear Override", + "Clear recents": "Clear recents", "Close": "Fechar", "Coinbase Logo": "Logo Coinbase", "Collectible": "Colecionável", @@ -144,6 +145,7 @@ "Create new wallet": "Criar nova carteira", "Current Network": "Rede Atual", "Custom": "Personalizado", + "dApps": "dApps", "Dapps": "Dapps", "data entries": "entradas de dados", "Debug": "Depuração", @@ -310,6 +312,7 @@ "Leave Feedback": "Deixar Feedback", "Leave feedback about Blockaid warnings and messages": "Deixar feedback sobre avisos e mensagens do Blockaid", "Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust.": "O Ledger não exibirá os detalhes da transação na tela do dispositivo antes de assinar, então certifique-se de interagir apenas com aplicativos que você conhece e confia.", + "Let's go": "Let's go", "Limit": "Limite", "Links": "Links", "Liquidity Pool ID": "ID do Pool de Liquidez", @@ -427,6 +430,7 @@ "Ready to migrate": "Pronto para migrar", "Receive": "Receber", "Received": "Recebido", + "Recent": "Recent", "Recents": "Recentes", "Recovery Phrase": "Frase de Recuperação", "Refresh metadata": "Atualizar metadados", @@ -549,6 +553,7 @@ "The website <1>{url} does not use an SSL certificate.": "O site <1>{url} não usa um certificado SSL.", "There are no sites to display at this moment.": "Não há sites para exibir no momento.", "These assets are not on any of your lists. Proceed with caution before adding.": "Esses ativos não estão em nenhuma de suas listas. Prossiga com cautela antes de adicionar.", + "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.": "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", "These words are your wallet’s keys—store them securely to keep your funds safe.": "Essas palavras são as chaves da sua carteira—guarde-as com segurança para manter seus fundos seguros.", "This asset does not appear safe for the following reasons.": "This asset does not appear safe for the following reasons.", "This asset has a balance": "Este ativo tem um saldo", @@ -660,6 +665,7 @@ "We were unable to scan this transaction for security issues": "We were unable to scan this transaction for security issues", "WEBSITE CONNECTION IS NOT SECURE": "CONEXÃO DO WEBSITE NÃO É SEGURA", "Welcome back": "Bem-vindo de volta", + "Welcome to Discover!": "Welcome to Discover!", "What is this transaction for? (optional)": "Para que é esta transação? (opcional)", "What’s new": "O que há de novo", "Wrong simulation result": "Resultado de simulação incorreto", @@ -695,6 +701,7 @@ "Your account data could not be fetched at this time.": "Os dados da sua conta não puderam ser buscados neste momento.", "Your assets": "Seus ativos", "Your available XLM balance is not enough to pay for the transaction fee.": "Seu saldo XLM disponível não é suficiente para pagar a taxa de transação.", + "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.": "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.", "Your recovery phrase": "Sua frase de recuperação", "Your Recovery Phrase": "Sua Frase de Recuperação", "Your recovery phrase gives you access to your account and is the only way to access it in a new browser.": "Sua frase de recuperação lhe dá acesso à sua conta e é a única maneira de acessá-la em um novo navegador.", diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx new file mode 100644 index 0000000000..0631f61d63 --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { Icon } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import { TrendingCarousel } from "../TrendingCarousel"; +import { DiscoverSection } from "../DiscoverSection"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface DiscoverHomeProps { + trendingItems: DiscoverData; + recentItems: DiscoverData; + dappsItems: DiscoverData; + onClose: () => void; + onExpandRecent: () => void; + onExpandDapps: () => void; + onCardClick: (protocol: Protocol) => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const DiscoverHome = ({ + trendingItems, + recentItems, + dappsItems, + onClose, + onExpandRecent, + onExpandDapps, + onCardClick, + onRowClick, + onOpenClick, +}: DiscoverHomeProps) => { + const { t } = useTranslation(); + + return ( + <> + } + customBackAction={onClose} + /> + +
+ + + +
+ {dappsItems.length > 0 && ( +
+
+ {`${t( + "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.", + )} ${t("Freighter does not endorse any listed items.")}`} +
+
+ {t( + "By using these services, you act at your own risk, and Freighter or Stellar Development Foundation (SDF) bears no liability for any resulting losses or damages.", + )} +
+
+ )} +
+ + ); +}; diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss b/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss new file mode 100644 index 0000000000..2b1da6851d --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss @@ -0,0 +1,25 @@ +@use "../../../../styles/utils.scss" as *; + +.DiscoverHome { + &__sections { + display: flex; + flex-direction: column; + gap: pxToRem(32); + } + + &__footer { + display: flex; + flex-direction: column; + gap: pxToRem(8); + border-radius: pxToRem(16); + background-color: var(--sds-clr-gray-02); + padding: pxToRem(16); + margin-top: pxToRem(32); + + &__copy { + color: var(--sds-clr-gray-11); + font-size: pxToRem(12); + line-height: pxToRem(14); + } + } +} diff --git a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx new file mode 100644 index 0000000000..a42ad5cc2f --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Button, Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { LoadingBackground } from "popup/basics/LoadingBackground"; + +import "./styles.scss"; + +interface DiscoverWelcomeModalProps { + onDismiss: () => void; +} + +export const DiscoverWelcomeModal = ({ + onDismiss, +}: DiscoverWelcomeModalProps) => { + const { t } = useTranslation(); + + return ( + <> +
+
+
+ +
+
+ + {t("Welcome to Discover!")} + +
+
+ + {t( + "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.", + )} + + + {t( + "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", + )} + +
+ +
+
+ + + ); +}; diff --git a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss new file mode 100644 index 0000000000..ac60c9613e --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss @@ -0,0 +1,57 @@ +@use "../../../../styles/utils.scss" as *; + +.DiscoverWelcomeModal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-index-modal); + padding: pxToRem(24); + + &__card { + background: var(--sds-clr-gray-01); + border: 1px solid var(--sds-clr-gray-06); + border-radius: pxToRem(16); + padding: pxToRem(24); + position: relative; + z-index: 2; + max-width: pxToRem(312); + width: 100%; + } + + &__icon { + width: pxToRem(40); + height: pxToRem(40); + border-radius: pxToRem(10); + background: var(--sds-clr-lilac-03, var(--sds-clr-gray-03)); + border: 1px solid var(--sds-clr-lilac-06, var(--sds-clr-gray-06)); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: pxToRem(16); + color: var(--sds-clr-gray-12); + + svg { + width: pxToRem(20); + height: pxToRem(20); + } + } + + &__title { + margin-bottom: pxToRem(12); + } + + &__body { + color: var(--sds-clr-gray-11); + margin-bottom: pxToRem(24); + + p { + margin-bottom: pxToRem(12); + + &:last-child { + margin-bottom: 0; + } + } + } +} diff --git a/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx b/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx new file mode 100644 index 0000000000..8008433a64 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import { ProtocolRow } from "../ProtocolRow"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ExpandedDappsProps { + items: DiscoverData; + onBack: () => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; +} + +export const ExpandedDapps = ({ + items, + onBack, + onRowClick, + onOpenClick, +}: ExpandedDappsProps) => { + const { t } = useTranslation(); + + return ( + <> + + +
+ {items.map((protocol) => ( + + ))} +
+
+ + ); +}; diff --git a/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss b/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss new file mode 100644 index 0000000000..4ddebd3e52 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss @@ -0,0 +1,10 @@ +@use "../../../../styles/utils.scss" as *; + +.ExpandedDapps { + &__list { + display: flex; + flex-direction: column; + gap: pxToRem(24); + padding: 0 pxToRem(24); + } +} diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx new file mode 100644 index 0000000000..eb4f09c062 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx @@ -0,0 +1,80 @@ +import React, { useState } from "react"; +import { Icon, Text } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; + +import { DiscoverData } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import { ProtocolRow } from "../ProtocolRow"; + +import "./styles.scss"; + +type Protocol = DiscoverData[number]; + +interface ExpandedRecentProps { + items: DiscoverData; + onBack: () => void; + onRowClick: (protocol: Protocol) => void; + onOpenClick: (protocol: Protocol) => void; + onClearRecent: () => void; +} + +export const ExpandedRecent = ({ + items, + onBack, + onRowClick, + onOpenClick, + onClearRecent, +}: ExpandedRecentProps) => { + const { t } = useTranslation(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + return ( + <> + + + {isMenuOpen && ( +
+
{ + setIsMenuOpen(false); + onClearRecent(); + }} + data-testid="clear-recents-button" + > + + + {t("Clear recents")} + +
+
+ )} +
+ } + /> + +
+ {items.map((protocol) => ( + + ))} +
+
+ + ); +}; diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss new file mode 100644 index 0000000000..d50f634ed4 --- /dev/null +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss @@ -0,0 +1,59 @@ +@use "../../../../styles/utils.scss" as *; + +.ExpandedRecent { + &__list { + display: flex; + flex-direction: column; + gap: pxToRem(24); + padding: 0 pxToRem(24); + } + + &__menu { + position: relative; + } + + &__menu-trigger { + background: transparent; + border: none; + cursor: pointer; + color: var(--sds-clr-gray-12); + padding: pxToRem(4); + + svg { + width: pxToRem(20); + height: pxToRem(20); + } + } + + &__dropdown { + position: absolute; + right: 0; + top: pxToRem(32); + width: pxToRem(180); + background: var(--sds-clr-gray-01); + border: 1px solid var(--sds-clr-gray-06); + border-radius: pxToRem(6); + box-shadow: 0 pxToRem(10) pxToRem(20) pxToRem(-10) rgba(0, 0, 0, 0.1); + padding: pxToRem(4); + z-index: 10; + } + + &__dropdown-item { + display: flex; + align-items: center; + gap: pxToRem(8); + padding: pxToRem(8); + cursor: pointer; + border-radius: pxToRem(4); + color: var(--sds-clr-red-09); + + svg { + width: pxToRem(16); + height: pxToRem(16); + } + + &:hover { + background: var(--sds-clr-gray-03); + } + } +} From c180149849bc5c019764d458d3acf19dd3a92bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:22:43 -0700 Subject: [PATCH 07/44] feat: rewrite Discover orchestrator with sub-views, modals, and data hooks Co-Authored-By: Claude Sonnet 4.6 --- .../Discover/hooks/useGetDiscoverData.ts | 42 ---- extension/src/popup/views/Discover/index.tsx | 217 ++++++++++-------- .../src/popup/views/Discover/styles.scss | 72 +----- 3 files changed, 126 insertions(+), 205 deletions(-) delete mode 100644 extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts diff --git a/extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts b/extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts deleted file mode 100644 index 9b1e936976..0000000000 --- a/extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useReducer } from "react"; -import { captureException } from "@sentry/browser"; - -import { RequestState } from "constants/request"; -import { initialState, reducer } from "helpers/request"; -import { getDiscoverData } from "@shared/api/internal"; -import { DiscoverData } from "@shared/api/types"; - -interface DiscoverDataPayload { - discoverData: DiscoverData; -} - -const useGetDiscoverData = () => { - const [state, dispatch] = useReducer( - reducer, - initialState, - ); - - const fetchData = async () => { - dispatch({ type: "FETCH_DATA_START" }); - try { - const discoverData = await getDiscoverData(); - - const payload = { - discoverData, - }; - dispatch({ type: "FETCH_DATA_SUCCESS", payload }); - return payload; - } catch (error) { - dispatch({ type: "FETCH_DATA_ERROR", payload: error }); - captureException(`Error loading discover protocols - ${error}`); - return error; - } - }; - - return { - state, - fetchData, - }; -}; - -export { useGetDiscoverData, RequestState }; diff --git a/extension/src/popup/views/Discover/index.tsx b/extension/src/popup/views/Discover/index.tsx index 5b8a3bc1b3..cb25e14a6a 100644 --- a/extension/src/popup/views/Discover/index.tsx +++ b/extension/src/popup/views/Discover/index.tsx @@ -1,111 +1,134 @@ -import React, { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Icon, Text } from "@stellar/design-system"; +import React, { useCallback, useState } from "react"; -import { View } from "popup/basics/layout/View"; -import { SubviewHeader } from "popup/components/SubviewHeader"; -import { Loading } from "popup/components/Loading"; import { DiscoverData } from "@shared/api/types"; +import { openTab } from "popup/helpers/navigate"; +import { + addRecentProtocol, + clearRecentProtocols, +} from "popup/helpers/recentProtocols"; +import { SlideupModal } from "popup/components/SlideupModal"; +import { Loading } from "popup/components/Loading"; + +import { useDiscoverData } from "./hooks/useDiscoverData"; +import { useDiscoverWelcome } from "./hooks/useDiscoverWelcome"; +import { DiscoverHome } from "./components/DiscoverHome"; +import { ExpandedRecent } from "./components/ExpandedRecent"; +import { ExpandedDapps } from "./components/ExpandedDapps"; +import { ProtocolDetailsPanel } from "./components/ProtocolDetailsPanel"; +import { DiscoverWelcomeModal } from "./components/DiscoverWelcomeModal"; -import { RequestState, useGetDiscoverData } from "./hooks/useGetDiscoverData"; import "./styles.scss"; -export const Discover = () => { - const { t } = useTranslation(); - const { state: discoverData, fetchData } = useGetDiscoverData(); +type Protocol = DiscoverData[number]; +type DiscoverView = "main" | "recent" | "dapps"; + +interface DiscoverProps { + onClose?: () => void; +} + +export const Discover = ({ onClose = () => {} }: DiscoverProps) => { + const [activeView, setActiveView] = useState("main"); + const [selectedProtocol, setSelectedProtocol] = useState( + null, + ); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); + + const { isLoading, trendingItems, recentItems, dappsItems, refreshRecent } = + useDiscoverData(); + const { showWelcome, dismissWelcome } = useDiscoverWelcome(); + + const handleOpenProtocol = useCallback( + async (protocol: Protocol) => { + openTab(protocol.websiteUrl); + await addRecentProtocol(protocol.websiteUrl); + await refreshRecent(); + }, + [refreshRecent], + ); - useEffect(() => { - const getData = async () => { - await fetchData(); - }; - getData(); - // eslint-disable-next-line react-hooks/exhaustive-deps + const handleRowClick = useCallback((protocol: Protocol) => { + setSelectedProtocol(protocol); + setIsDetailsOpen(true); }, []); - const { state, data } = discoverData; - const isLoading = - state === RequestState.IDLE || state === RequestState.LOADING; - let allowedDiscoverRows = [] as DiscoverData; - if (isLoading) { - return ; - } + const handleDetailsOpen = useCallback( + async (protocol: Protocol) => { + setIsDetailsOpen(false); + setTimeout(async () => { + setSelectedProtocol(null); + await handleOpenProtocol(protocol); + }, 200); + }, + [handleOpenProtocol], + ); - if (state !== RequestState.ERROR) { - allowedDiscoverRows = data.discoverData.filter((row) => !row.isBlacklisted); + const handleClearRecent = useCallback(async () => { + await clearRecentProtocols(); + await refreshRecent(); + setActiveView("main"); + }, [refreshRecent]); + + if (isLoading) { + return ( +
+ +
+ ); } return ( - <> - - -
- - - {t("Dapps")} - -
-
- {allowedDiscoverRows.length ? ( - allowedDiscoverRows.map((row) => ( -
- {row.name} -
- - {row.name} - -
- - {row.tags.join(", ")} - -
-
- - - {t("Open")} - -
- -
-
-
- )) - ) : ( -
- - {t("There are no sites to display at this moment.")} - -
- )} - {allowedDiscoverRows.length ? ( -
-
- {`${t( - "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.", - )} ${t("Freighter does not endorse any listed items.")}`} -
-
- {t( - "By using these services, you act at your own risk, and Freighter or Stellar Development Foundation (SDF) bears no liability for any resulting losses or damages.", - )} -
-
- ) : null} -
-
- +
+ {activeView === "main" && ( + setActiveView("recent")} + onExpandDapps={() => setActiveView("dapps")} + onCardClick={handleRowClick} + onRowClick={handleRowClick} + onOpenClick={handleOpenProtocol} + /> + )} + {activeView === "recent" && ( + setActiveView("main")} + onRowClick={handleRowClick} + onOpenClick={handleOpenProtocol} + onClearRecent={handleClearRecent} + /> + )} + {activeView === "dapps" && ( + setActiveView("main")} + onRowClick={handleRowClick} + onOpenClick={handleOpenProtocol} + /> + )} + + { + setIsDetailsOpen(open); + if (!open) { + setSelectedProtocol(null); + } + }} + > + {selectedProtocol ? ( + + ) : ( +
+ )} + + + {showWelcome && } +
); }; diff --git a/extension/src/popup/views/Discover/styles.scss b/extension/src/popup/views/Discover/styles.scss index 468206eb21..088a73b363 100644 --- a/extension/src/popup/views/Discover/styles.scss +++ b/extension/src/popup/views/Discover/styles.scss @@ -1,70 +1,10 @@ -@use "../../styles/utils.scss" as *; - .Discover { - &__eyebrow { - align-items: center; - color: var(--sds-clr-gray-11); - display: flex; - gap: pxToRem(8); - margin-bottom: pxToRem(16); - } - - &__content { - display: flex; - flex-direction: column; - gap: pxToRem(24); - } - - &__row { - display: flex; - gap: pxToRem(12); - align-items: center; - - &__icon { - height: pxToRem(32); - width: pxToRem(32); - } - - &__label { - display: flex; - flex-direction: column; - flex: 1 0 0; - - &__subtitle { - color: var(--sds-clr-gray-11); - } - } - - &__button { - border-radius: 100px; - border: 1px solid var(--sds-clr-gray-06); - color: var(--sds-clr-gray-12); - display: flex; - gap: pxToRem(4); - padding: pxToRem(6) pxToRem(10); - justify-content: center; - align-items: center; - - &__icon { - height: pxToRem(14); - width: pxToRem(14); - color: var(--sds-clr-gray-09); - } - } - } - - &__footer { - display: flex; - flex-direction: column; - gap: pxToRem(8); - border-radius: pxToRem(16); - background-color: var(--sds-clr-gray-02); - padding: pxToRem(16); + height: 100%; + display: flex; + flex-direction: column; + background: var(--sds-clr-gray-01); - &__copy { - color: var(--sds-clr-gray-11); - font-size: pxToRem(12px); - line-height: pxToRem(14); - } + &__sheet-wrapper { + height: var(--popup--height); } } From 1099777827218a38354068e478228a831d00b90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:24:59 -0700 Subject: [PATCH 08/44] feat: wire up Discover Sheet in Account view and update AccountHeader Co-Authored-By: Claude Opus 4.6 (1M context) --- .../account/AccountHeader/index.tsx | 4 ++- extension/src/popup/views/Account/index.tsx | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/account/AccountHeader/index.tsx b/extension/src/popup/components/account/AccountHeader/index.tsx index 56e3e5a9a0..d51eda9499 100644 --- a/extension/src/popup/components/account/AccountHeader/index.tsx +++ b/extension/src/popup/components/account/AccountHeader/index.tsx @@ -42,6 +42,7 @@ interface AccountHeaderProps { roundedTotalBalanceUsd: string; refreshHiddenCollectibles: () => Promise; isCollectibleHidden: (collectionAddress: string, tokenId: string) => boolean; + onDiscoverClick: () => void; } export const AccountHeader = ({ @@ -54,6 +55,7 @@ export const AccountHeader = ({ roundedTotalBalanceUsd, refreshHiddenCollectibles, isCollectibleHidden, + onDiscoverClick, }: AccountHeaderProps) => { const { t } = useTranslation(); const networkDetails = useSelector(settingsNetworkDetailsSelector); @@ -307,7 +309,7 @@ export const AccountHeader = ({ rightContent={
navigateTo(ROUTES.discover, navigate)} + onClick={onDiscoverClick} > {t("Discover")}
diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index 822f84c463..078e34f635 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useContext } from "react"; +import React, { useEffect, useRef, useContext, useState } from "react"; import { Navigate, useLocation } from "react-router-dom"; import { useSelector } from "react-redux"; import { Notification } from "@stellar/design-system"; @@ -40,6 +40,14 @@ import { } from "./hooks/useGetIcons"; import { AccountTabsContext, TabsList } from "./contexts/activeTabContext"; +import { + Sheet, + SheetContent, + ScreenReaderOnly, + SheetTitle, +} from "popup/basics/shadcn/Sheet"; +import { Discover } from "popup/views/Discover"; + import "popup/metrics/authServices"; import "./styles.scss"; @@ -50,6 +58,7 @@ export const Account = () => { const { userNotification } = useSelector(settingsSelector); const currentAccountName = useSelector(accountNameSelector); const { activeTab } = useContext(AccountTabsContext); + const [isDiscoverOpen, setIsDiscoverOpen] = useState(false); const isFullscreenModeEnabled = isFullscreenMode(); const { @@ -204,6 +213,7 @@ export const Account = () => { isFunded={!!resolvedData?.balances?.isFunded} refreshHiddenCollectibles={refreshHiddenCollectibles} isCollectibleHidden={isCollectibleHidden} + onDiscoverClick={() => setIsDiscoverOpen(true)} />
@@ -300,6 +310,19 @@ export const Account = () => { /> )} + + e.preventDefault()} + aria-describedby={undefined} + side="bottom" + className="Discover__sheet-wrapper" + > + + Discover + + setIsDiscoverOpen(false)} /> + + ); }; From 5a35340d9c6c8c6846b6b00f951dea0695e9a4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:31:13 -0700 Subject: [PATCH 09/44] test: rewrite Discover tests for new Sheet-based component structure Co-Authored-By: Claude Sonnet 4.6 --- extension/src/popup/Router.tsx | 2 - extension/src/popup/constants/metricsNames.ts | 1 - extension/src/popup/constants/routes.ts | 1 - extension/src/popup/metrics/views.ts | 1 - .../popup/views/__tests__/Discover.test.tsx | 145 +++++++++++++----- 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/extension/src/popup/Router.tsx b/extension/src/popup/Router.tsx index dc23fec8ad..38d1285761 100644 --- a/extension/src/popup/Router.tsx +++ b/extension/src/popup/Router.tsx @@ -60,7 +60,6 @@ import { ManageNetwork } from "popup/views/ManageNetwork"; import { LeaveFeedback } from "popup/views/LeaveFeedback"; import { AccountMigration } from "popup/views/AccountMigration"; import { AddFunds } from "popup/views/AddFunds"; -import { Discover } from "popup/views/Discover"; import { Wallets } from "popup/views/Wallets"; import { DEV_SERVER } from "@shared/constants/services"; @@ -275,7 +274,6 @@ export const Router = () => ( element={} > } /> - } /> } /> {DEV_SERVER && ( diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index 675e8b3969..af0a195e01 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -88,7 +88,6 @@ export const METRIC_NAMES = { viewEditNetwork: "loaded screen: edit network", viewNetworkSettings: "loaded screen: network settings", viewAddFunds: "loaded screen: add fund", - discover: "loaded screen: discover", manageAssetAddAsset: "manage asset: add asset", manageAssetAddToken: "manage asset: add token", diff --git a/extension/src/popup/constants/routes.ts b/extension/src/popup/constants/routes.ts index 69a65d11af..86fb2133d6 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -53,6 +53,5 @@ export enum ROUTES { accountMigrationConfirmMigration = "/account-migration/confirm-migration", accountMigrationMigrationComplete = "/account-migration/migration-complete", - discover = "/discover", wallets = "/wallets", } diff --git a/extension/src/popup/metrics/views.ts b/extension/src/popup/metrics/views.ts index 0d63f6fa91..b01ae42369 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -66,7 +66,6 @@ const routeToEventName = { METRIC_NAMES.viewAccountMigrationMigrationComplete, [ROUTES.advancedSettings]: METRIC_NAMES.viewAdvancedSettings, [ROUTES.addFunds]: METRIC_NAMES.viewAddFunds, - [ROUTES.discover]: METRIC_NAMES.discover, [ROUTES.wallets]: METRIC_NAMES.wallets, }; diff --git a/extension/src/popup/views/__tests__/Discover.test.tsx b/extension/src/popup/views/__tests__/Discover.test.tsx index b35cf675df..a9b4ad897f 100644 --- a/extension/src/popup/views/__tests__/Discover.test.tsx +++ b/extension/src/popup/views/__tests__/Discover.test.tsx @@ -5,40 +5,70 @@ import { MAINNET_NETWORK_DETAILS, } from "@shared/constants/stellar"; import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState"; -import { ROUTES } from "popup/constants/routes"; import * as ApiInternal from "@shared/api/internal"; +import * as RecentProtocols from "popup/helpers/recentProtocols"; import { Wrapper, mockAccounts } from "../../__testHelpers__"; import { Discover } from "../Discover"; -describe("Discover view", () => { - it("displays Discover protocols with links that are not blacklisted", async () => { - jest.spyOn(ApiInternal, "getDiscoverData").mockImplementation(() => - Promise.resolve([ - { - description: "description text", - name: "Foo", - iconUrl: "https://example.com/icon.png", - websiteUrl: "https://foo.com", - tags: ["tag1", "tag2"], - isBlacklisted: false, - backgroundUrl: undefined, - isTrending: false, - }, - { - description: "description text", - name: "Baz", - iconUrl: "https://example.com/icon.png", - websiteUrl: "https://baz.com", - tags: ["tag1", "tag2"], - isBlacklisted: true, - backgroundUrl: undefined, - isTrending: false, - }, - ]), - ); +// Mock browser.storage.local +jest.mock("webextension-polyfill", () => ({ + storage: { + local: { + get: jest.fn().mockResolvedValue({}), + set: jest.fn().mockResolvedValue(undefined), + remove: jest.fn().mockResolvedValue(undefined), + }, + }, + tabs: { + create: jest.fn(), + }, +})); + +const mockProtocols = [ + { + description: "A lending protocol", + name: "Blend", + iconUrl: "https://example.com/blend.png", + websiteUrl: "https://blend.capital", + tags: ["Lending", "DeFi"], + isBlacklisted: false, + backgroundUrl: "https://example.com/blend-bg.png", + isTrending: true, + }, + { + description: "An exchange", + name: "Soroswap", + iconUrl: "https://example.com/soroswap.png", + websiteUrl: "https://soroswap.finance", + tags: ["Exchange"], + isBlacklisted: false, + isTrending: false, + }, + { + description: "Blacklisted protocol", + name: "BadProtocol", + iconUrl: "https://example.com/bad.png", + websiteUrl: "https://bad.com", + tags: ["Scam"], + isBlacklisted: true, + isTrending: false, + }, +]; + +describe("Discover", () => { + beforeEach(() => { + jest.spyOn(ApiInternal, "getDiscoverData").mockResolvedValue(mockProtocols); + jest.spyOn(RecentProtocols, "getRecentProtocols").mockResolvedValue([]); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const renderDiscover = () => render( { }, }} > - + , ); + it("displays trending protocols in the carousel", async () => { + renderDiscover(); + await waitFor(() => { - expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Discover", - ); + expect(screen.getByTestId("trending-carousel")).toBeInTheDocument(); }); - const protocolLinks = screen.queryAllByTestId("discover-row"); - expect(protocolLinks).toHaveLength(1); - expect(protocolLinks[0]).toHaveTextContent("Foo"); - expect(screen.getByTestId("discover-row-button")).toHaveAttribute( - "href", - "https://foo.com", - ); + const trendingCards = screen.getAllByTestId("trending-card"); + expect(trendingCards).toHaveLength(1); + expect(trendingCards[0]).toHaveTextContent("Blend"); + }); + + it("displays dApps section with non-blacklisted protocols", async () => { + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-section-dapps")).toBeInTheDocument(); + }); + + const protocolRows = screen.getAllByTestId("protocol-row"); + // Blend + Soroswap (BadProtocol is blacklisted) + expect(protocolRows).toHaveLength(2); + expect(protocolRows[0]).toHaveTextContent("Blend"); + expect(protocolRows[1]).toHaveTextContent("Soroswap"); + }); + + it("hides recent section when no recent protocols exist", async () => { + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-section-dapps")).toBeInTheDocument(); + }); + + expect( + screen.queryByTestId("discover-section-recent"), + ).not.toBeInTheDocument(); + }); + + it("shows recent section when recent protocols exist", async () => { + jest + .spyOn(RecentProtocols, "getRecentProtocols") + .mockResolvedValue([ + { websiteUrl: "https://blend.capital", lastAccessed: Date.now() }, + ]); + + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-section-recent")).toBeInTheDocument(); + }); }); }); From 50b3c088efd24f1b434c5c271d86d2a80c3d6a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Mon, 30 Mar 2026 21:32:31 -0700 Subject: [PATCH 10/44] refactor: remove /discover route, now handled via Sheet overlay Co-Authored-By: Claude Sonnet 4.6 --- extension/src/popup/views/__tests__/Discover.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extension/src/popup/views/__tests__/Discover.test.tsx b/extension/src/popup/views/__tests__/Discover.test.tsx index a9b4ad897f..089d6172b7 100644 --- a/extension/src/popup/views/__tests__/Discover.test.tsx +++ b/extension/src/popup/views/__tests__/Discover.test.tsx @@ -42,6 +42,7 @@ const mockProtocols = [ websiteUrl: "https://soroswap.finance", tags: ["Exchange"], isBlacklisted: false, + backgroundUrl: undefined, isTrending: false, }, { @@ -51,6 +52,7 @@ const mockProtocols = [ websiteUrl: "https://bad.com", tags: ["Scam"], isBlacklisted: true, + backgroundUrl: undefined, isTrending: false, }, ]; From 8a17902cba2412b19e467d1334493d7510fe9759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 11:00:52 -0700 Subject: [PATCH 11/44] fix: save recent protocol before opening tab to prevent data loss in popup mode The popup closes immediately when a new tab opens, so the storage write must complete before calling openTab. Co-Authored-By: Claude Opus 4.6 (1M context) --- extension/src/popup/views/Discover/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/views/Discover/index.tsx b/extension/src/popup/views/Discover/index.tsx index cb25e14a6a..f39ff634ed 100644 --- a/extension/src/popup/views/Discover/index.tsx +++ b/extension/src/popup/views/Discover/index.tsx @@ -39,9 +39,9 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { const handleOpenProtocol = useCallback( async (protocol: Protocol) => { - openTab(protocol.websiteUrl); await addRecentProtocol(protocol.websiteUrl); await refreshRecent(); + openTab(protocol.websiteUrl); }, [refreshRecent], ); From 18b7a0f5b91ff19bcfa78f6e4479bd2536d1f78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 11:29:25 -0700 Subject: [PATCH 12/44] fix: Discover Sheet fills full viewport and removes white border artifacts Use height: 100% instead of fixed popup height so the Sheet covers the full screen in both popup and fullscreen mode. Override Tailwind defaults that caused a visible white border around the overlay. Co-Authored-By: Claude Opus 4.6 (1M context) --- extension/src/popup/views/Discover/styles.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/views/Discover/styles.scss b/extension/src/popup/views/Discover/styles.scss index 088a73b363..4c4bd35c27 100644 --- a/extension/src/popup/views/Discover/styles.scss +++ b/extension/src/popup/views/Discover/styles.scss @@ -5,6 +5,10 @@ background: var(--sds-clr-gray-01); &__sheet-wrapper { - height: var(--popup--height); + background: var(--sds-clr-gray-01) !important; + height: 100%; + width: 100%; + border: none !important; + box-shadow: none !important; } } From e5b314def4695f9bbd1f775dfda07cd653b8d57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 11:56:31 -0700 Subject: [PATCH 13/44] fix carousel margins and paddings --- .../views/Discover/components/TrendingCarousel/styles.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss index f4fc2ca1c6..b8ba7511ca 100644 --- a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss @@ -9,9 +9,13 @@ display: flex; gap: pxToRem(16); overflow-x: auto; - scroll-snap-type: x mandatory; + scroll-behavior: smooth; -ms-overflow-style: none; scrollbar-width: none; + margin-left: calc(-1 * var(--popup--side-padding)); + margin-right: calc(-1 * var(--popup--side-padding)); + padding-left: var(--popup--side-padding); + padding-right: var(--popup--side-padding); &::-webkit-scrollbar { display: none; @@ -28,7 +32,6 @@ position: relative; overflow: hidden; cursor: pointer; - scroll-snap-align: start; flex-shrink: 0; &__gradient { From 0ecf85bd4b54c1e5b6b7733fd9f183aa547ccfbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 12:11:05 -0700 Subject: [PATCH 14/44] fix discover header spacing --- .../popup/views/Discover/components/DiscoverHome/index.tsx | 4 ++-- .../popup/views/Discover/components/ExpandedDapps/index.tsx | 4 ++-- .../popup/views/Discover/components/ExpandedRecent/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx index 0631f61d63..2782e45e61 100644 --- a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx @@ -38,7 +38,7 @@ export const DiscoverHome = ({ const { t } = useTranslation(); return ( - <> + } @@ -77,6 +77,6 @@ export const DiscoverHome = ({
)}
- + ); }; diff --git a/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx b/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx index 8008433a64..e987f1632b 100644 --- a/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx +++ b/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx @@ -26,7 +26,7 @@ export const ExpandedDapps = ({ const { t } = useTranslation(); return ( - <> +
@@ -40,6 +40,6 @@ export const ExpandedDapps = ({ ))}
- +
); }; diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx index eb4f09c062..6696c17836 100644 --- a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx @@ -30,7 +30,7 @@ export const ExpandedRecent = ({ const [isMenuOpen, setIsMenuOpen] = useState(false); return ( - <> + - + ); }; From ab5f8c032bb45583ab5894e736a82117e5ad718b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 12:13:39 -0700 Subject: [PATCH 15/44] remove duplicate padding --- .../popup/views/Discover/components/ExpandedDapps/styles.scss | 1 - .../popup/views/Discover/components/ExpandedRecent/styles.scss | 1 - 2 files changed, 2 deletions(-) diff --git a/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss b/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss index 4ddebd3e52..67ec545e2f 100644 --- a/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss +++ b/extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss @@ -5,6 +5,5 @@ display: flex; flex-direction: column; gap: pxToRem(24); - padding: 0 pxToRem(24); } } diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss index d50f634ed4..6ecbca8ba7 100644 --- a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss @@ -5,7 +5,6 @@ display: flex; flex-direction: column; gap: pxToRem(24); - padding: 0 pxToRem(24); } &__menu { From aea1ad2e6ebdbdeeb833026cc270b4393325c364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 13:10:38 -0700 Subject: [PATCH 16/44] update legal disclaimer --- .../popup/views/Discover/components/DiscoverHome/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx index 2782e45e61..fac0fe24ea 100644 --- a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx @@ -64,14 +64,9 @@ export const DiscoverHome = ({
{dappsItems.length > 0 && (
-
- {`${t( - "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.", - )} ${t("Freighter does not endorse any listed items.")}`} -
{t( - "By using these services, you act at your own risk, and Freighter or Stellar Development Foundation (SDF) bears no liability for any resulting losses or damages.", + "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", )}
From cb2339db9070bf1cff88dbe26b6cd3aa64c5e791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 16:36:45 -0700 Subject: [PATCH 17/44] clean-up !important values --- extension/src/popup/views/Discover/styles.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/extension/src/popup/views/Discover/styles.scss b/extension/src/popup/views/Discover/styles.scss index 4c4bd35c27..72d49025fd 100644 --- a/extension/src/popup/views/Discover/styles.scss +++ b/extension/src/popup/views/Discover/styles.scss @@ -5,10 +5,8 @@ background: var(--sds-clr-gray-01); &__sheet-wrapper { - background: var(--sds-clr-gray-01) !important; + background: var(--sds-clr-gray-01); height: 100%; width: 100%; - border: none !important; - box-shadow: none !important; } } From d9e7c37837dd74d18d21a19525fb8df505561eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 16:40:13 -0700 Subject: [PATCH 18/44] fix carousel card size --- .../views/Discover/components/TrendingCarousel/styles.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss index b8ba7511ca..f0fc11a609 100644 --- a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss @@ -23,8 +23,8 @@ } &__card { - min-width: pxToRem(264); - height: pxToRem(148); + min-width: pxToRem(288); + height: pxToRem(162); border-radius: pxToRem(12); background-color: var(--sds-clr-gray-03); background-size: cover; From 2509c15ffc071dd08cbe5777a6f08c7396ef16af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 16:50:28 -0700 Subject: [PATCH 19/44] tweak "Open" button --- .../Discover/components/ProtocolDetailsPanel/styles.scss | 8 +++++++- .../views/Discover/components/ProtocolRow/styles.scss | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss index a22a7a6af6..97f1dfc04b 100644 --- a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss +++ b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss @@ -30,7 +30,8 @@ border-radius: 100px; display: flex; gap: pxToRem(4); - padding: pxToRem(6) pxToRem(10); + height: pxToRem(32); + padding: 0 pxToRem(10); justify-content: center; align-items: center; cursor: pointer; @@ -39,6 +40,11 @@ &__icon { height: pxToRem(14); width: pxToRem(14); + + svg { + width: pxToRem(14); + height: pxToRem(14); + } } } diff --git a/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss b/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss index 76c15688c9..6b3e5795dc 100644 --- a/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss +++ b/extension/src/popup/views/Discover/components/ProtocolRow/styles.scss @@ -31,7 +31,8 @@ color: var(--sds-clr-gray-12); display: flex; gap: pxToRem(4); - padding: pxToRem(6) pxToRem(10); + height: pxToRem(32); + padding: 0 pxToRem(10); justify-content: center; align-items: center; cursor: pointer; @@ -43,6 +44,11 @@ height: pxToRem(14); width: pxToRem(14); color: var(--sds-clr-gray-09); + + svg { + width: pxToRem(14); + height: pxToRem(14); + } } } } From 7e025541f4a64be1fe4fde8108a480299d83e4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 17:06:52 -0700 Subject: [PATCH 20/44] use consistent "X" icon throughout screens --- .../popup/components/hardwareConnect/HardwareSign/index.tsx | 4 ++-- .../src/popup/components/manageAssets/ChooseAsset/index.tsx | 2 +- .../popup/views/Discover/components/DiscoverHome/index.tsx | 2 +- extension/src/popup/views/Wallets/index.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extension/src/popup/components/hardwareConnect/HardwareSign/index.tsx b/extension/src/popup/components/hardwareConnect/HardwareSign/index.tsx index a7bdcc1a1c..1b4638b8b4 100644 --- a/extension/src/popup/components/hardwareConnect/HardwareSign/index.tsx +++ b/extension/src/popup/components/hardwareConnect/HardwareSign/index.tsx @@ -148,7 +148,7 @@ export const HardwareSign = ({ onCancel(); } }} - customBackIcon={} + customBackIcon={} title={t("Connect {walletType}", { walletType })} />
@@ -199,7 +199,7 @@ export const HardwareSign = ({
} + customBackIcon={} title={t("Connect {walletType}", { walletType })} />
diff --git a/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx b/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx index 63b5ff020a..184e7c6fc6 100644 --- a/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx +++ b/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx @@ -93,7 +93,7 @@ export const ChooseAsset = ({ : undefined + !domainState.data?.isManagingAssets ? : undefined } customBackAction={goBack} rightButton={ diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx index fac0fe24ea..ba15484479 100644 --- a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx @@ -41,7 +41,7 @@ export const DiscoverHome = ({ } + customBackIcon={} customBackAction={onClose} /> diff --git a/extension/src/popup/views/Wallets/index.tsx b/extension/src/popup/views/Wallets/index.tsx index cdfaac6126..2f9097ddf5 100644 --- a/extension/src/popup/views/Wallets/index.tsx +++ b/extension/src/popup/views/Wallets/index.tsx @@ -380,7 +380,7 @@ export const Wallets = () => { navigateTo(ROUTES.account, navigate)} - customBackIcon={} + customBackIcon={} /> Date: Tue, 31 Mar 2026 17:40:22 -0700 Subject: [PATCH 21/44] figma tweaks --- extension/src/popup/basics/layout/View/index.tsx | 3 ++- extension/src/popup/styles/global.scss | 2 +- .../views/Discover/components/DiscoverSection/styles.scss | 1 + .../Discover/components/ProtocolDetailsPanel/styles.scss | 6 +++++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/extension/src/popup/basics/layout/View/index.tsx b/extension/src/popup/basics/layout/View/index.tsx index c21815a166..69fc490ebe 100644 --- a/extension/src/popup/basics/layout/View/index.tsx +++ b/extension/src/popup/basics/layout/View/index.tsx @@ -90,7 +90,8 @@ const ViewAppHeader: React.FC = ({
Date: Tue, 31 Mar 2026 17:45:57 -0700 Subject: [PATCH 22/44] figma tweak --- .../popup/views/Discover/components/ExpandedRecent/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss index 6ecbca8ba7..7eaafbe3fc 100644 --- a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss @@ -41,7 +41,7 @@ display: flex; align-items: center; gap: pxToRem(8); - padding: pxToRem(8); + padding: pxToRem(6) pxToRem(8); cursor: pointer; border-radius: pxToRem(4); color: var(--sds-clr-red-09); From e999c53bb8b12f38a869ac8d5627ab0f4847180d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 18:14:42 -0700 Subject: [PATCH 23/44] remove superpowers folder --- .../plans/2026-03-30-discover-v2.md | 2171 ----------------- .../specs/2026-03-30-discover-v2-design.md | 340 --- 2 files changed, 2511 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-30-discover-v2.md delete mode 100644 docs/superpowers/specs/2026-03-30-discover-v2-design.md diff --git a/docs/superpowers/plans/2026-03-30-discover-v2.md b/docs/superpowers/plans/2026-03-30-discover-v2.md deleted file mode 100644 index 036d560f33..0000000000 --- a/docs/superpowers/plans/2026-03-30-discover-v2.md +++ /dev/null @@ -1,2171 +0,0 @@ -# Discover V2 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use -> superpowers:subagent-driven-development (recommended) or -> superpowers:executing-plans to implement this plan task-by-task. Steps use -> checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Port the Discover V2 refactoring (Trending carousel, Recent protocols, -dApps sections, protocol details modal, welcome modal) from freighter-mobile to -the freighter browser extension. - -**Architecture:** The Discover screen renders inside a Radix Sheet overlay -(slide-from-bottom, same as AssetDetail). Sub-views (expanded Recent/dApps) swap -via local state inside the Sheet. Protocol details use the existing -SlideupModal. Recent protocols are stored in `browser.storage.local` via a -standalone helper. No new routes — the existing `/discover` route is removed. - -**Tech Stack:** React 19, TypeScript, SCSS (BEM), @stellar/design-system, Radix -UI Sheet (shadcn wrapper), webextension-polyfill, browser.storage.local - -**Spec:** `docs/superpowers/specs/2026-03-30-discover-v2-design.md` - ---- - -### Task 1: Update DiscoverData type and getDiscoverData API mapping - -**Files:** - -- Modify: `@shared/api/types/types.ts:399-406` -- Modify: `@shared/api/internal.ts:624-656` - -- [ ] **Step 1: Update DiscoverData type** - -In `@shared/api/types/types.ts`, add `backgroundUrl` and `isTrending` to the -`DiscoverData` type: - -```typescript -export type DiscoverData = { - description: string; - iconUrl: string; - name: string; - websiteUrl: string; - tags: string[]; - isBlacklisted: boolean; - backgroundUrl?: string; - isTrending: boolean; -}[]; -``` - -- [ ] **Step 2: Update getDiscoverData mapping** - -In `@shared/api/internal.ts`, update the response type and mapping in -`getDiscoverData()`: - -Add to the `parsedResponse` type assertion inside the function: - -```typescript -background_url?: string; -is_trending: boolean; -``` - -Add to the return mapping: - -```typescript -backgroundUrl: entry.background_url, -isTrending: entry.is_trending, -``` - -The full function should look like: - -```typescript -export const getDiscoverData = async () => { - const url = new URL(`${INDEXER_V2_URL}/protocols`); - const response = await fetch(url.href); - const parsedResponse = (await response.json()) as { - data: { - protocols: { - description: string; - icon_url: string; - name: string; - website_url: string; - tags: string[]; - is_blacklisted: boolean; - background_url?: string; - is_trending: boolean; - }[]; - }; - }; - - if (!response.ok || !parsedResponse.data) { - const _err = JSON.stringify(parsedResponse); - captureException( - `Failed to fetch discover entries - ${response.status}: ${response.statusText}`, - ); - throw new Error(_err); - } - - return parsedResponse.data.protocols.map((entry) => ({ - description: entry.description, - iconUrl: entry.icon_url, - name: entry.name, - websiteUrl: entry.website_url, - tags: entry.tags, - isBlacklisted: entry.is_blacklisted, - backgroundUrl: entry.background_url, - isTrending: entry.is_trending, - })); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add @shared/api/types/types.ts @shared/api/internal.ts -git commit -m "feat: add backgroundUrl and isTrending to DiscoverData type and API mapping" -``` - ---- - -### Task 2: Create recentProtocols helper - -**Files:** - -- Create: `extension/src/popup/helpers/recentProtocols.ts` - -- [ ] **Step 1: Create the recentProtocols helper** - -Create `extension/src/popup/helpers/recentProtocols.ts`: - -```typescript -import browser from "webextension-polyfill"; - -const STORAGE_KEY = "recentProtocols"; -const MAX_RECENT = 5; - -export interface RecentProtocolEntry { - websiteUrl: string; - lastAccessed: number; -} - -export const getRecentProtocols = async (): Promise => { - const result = await browser.storage.local.get(STORAGE_KEY); - return (result[STORAGE_KEY] as RecentProtocolEntry[]) || []; -}; - -export const addRecentProtocol = async (websiteUrl: string): Promise => { - const existing = await getRecentProtocols(); - const filtered = existing.filter((entry) => entry.websiteUrl !== websiteUrl); - const updated = [{ websiteUrl, lastAccessed: Date.now() }, ...filtered].slice( - 0, - MAX_RECENT, - ); - await browser.storage.local.set({ [STORAGE_KEY]: updated }); -}; - -export const clearRecentProtocols = async (): Promise => { - await browser.storage.local.remove(STORAGE_KEY); -}; -``` - -- [ ] **Step 2: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 3: Commit** - -```bash -git add extension/src/popup/helpers/recentProtocols.ts -git commit -m "feat: add recentProtocols helper for browser.storage.local" -``` - ---- - -### Task 3: Create useDiscoverData hook - -**Files:** - -- Create: `extension/src/popup/views/Discover/hooks/useDiscoverData.ts` - -- [ ] **Step 1: Create the hook** - -Create `extension/src/popup/views/Discover/hooks/useDiscoverData.ts`: - -```typescript -import { useCallback, useEffect, useReducer } from "react"; -import { captureException } from "@sentry/browser"; - -import { getDiscoverData } from "@shared/api/internal"; -import { DiscoverData } from "@shared/api/types"; -import { - getRecentProtocols, - RecentProtocolEntry, -} from "popup/helpers/recentProtocols"; - -interface DiscoverDataState { - isLoading: boolean; - error: unknown | null; - allProtocols: DiscoverData; - recentEntries: RecentProtocolEntry[]; -} - -type Action = - | { type: "FETCH_START" } - | { - type: "FETCH_SUCCESS"; - payload: { - protocols: DiscoverData; - recentEntries: RecentProtocolEntry[]; - }; - } - | { type: "FETCH_ERROR"; payload: unknown } - | { type: "REFRESH_RECENT"; payload: RecentProtocolEntry[] }; - -const initialState: DiscoverDataState = { - isLoading: true, - error: null, - allProtocols: [], - recentEntries: [], -}; - -const reducer = ( - state: DiscoverDataState, - action: Action, -): DiscoverDataState => { - switch (action.type) { - case "FETCH_START": - return { ...state, isLoading: true, error: null }; - case "FETCH_SUCCESS": - return { - isLoading: false, - error: null, - allProtocols: action.payload.protocols, - recentEntries: action.payload.recentEntries, - }; - case "FETCH_ERROR": - return { ...state, isLoading: false, error: action.payload }; - case "REFRESH_RECENT": - return { ...state, recentEntries: action.payload }; - default: - return state; - } -}; - -export const useDiscoverData = () => { - const [state, dispatch] = useReducer(reducer, initialState); - - const fetchData = useCallback(async () => { - dispatch({ type: "FETCH_START" }); - try { - const [protocols, recentEntries] = await Promise.all([ - getDiscoverData(), - getRecentProtocols(), - ]); - dispatch({ - type: "FETCH_SUCCESS", - payload: { protocols, recentEntries }, - }); - } catch (error) { - dispatch({ type: "FETCH_ERROR", payload: error }); - captureException(`Error loading discover data - ${error}`); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - const refreshRecent = useCallback(async () => { - const recentEntries = await getRecentProtocols(); - dispatch({ type: "REFRESH_RECENT", payload: recentEntries }); - }, []); - - const allowedProtocols = state.allProtocols.filter((p) => !p.isBlacklisted); - - const trendingItems = allowedProtocols.filter((p) => p.isTrending); - - const recentItems = state.recentEntries - .map((entry) => - allowedProtocols.find((p) => p.websiteUrl === entry.websiteUrl), - ) - .filter(Boolean) as DiscoverData; - - const dappsItems = allowedProtocols; - - return { - isLoading: state.isLoading, - error: state.error, - trendingItems, - recentItems, - dappsItems, - allProtocols: allowedProtocols, - refreshRecent, - }; -}; -``` - -- [ ] **Step 2: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 3: Commit** - -```bash -git add extension/src/popup/views/Discover/hooks/useDiscoverData.ts -git commit -m "feat: add useDiscoverData hook with trending, recent, and dapps lists" -``` - ---- - -### Task 4: Create useDiscoverWelcome hook - -**Files:** - -- Create: `extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts` - -- [ ] **Step 1: Create the hook** - -Create `extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts`: - -```typescript -import { useCallback, useEffect, useState } from "react"; -import browser from "webextension-polyfill"; - -const STORAGE_KEY = "hasSeenDiscoverWelcome"; - -export const useDiscoverWelcome = () => { - const [showWelcome, setShowWelcome] = useState(false); - - useEffect(() => { - const checkWelcome = async () => { - const result = await browser.storage.local.get(STORAGE_KEY); - if (!result[STORAGE_KEY]) { - setShowWelcome(true); - } - }; - checkWelcome(); - }, []); - - const dismissWelcome = useCallback(async () => { - setShowWelcome(false); - await browser.storage.local.set({ [STORAGE_KEY]: true }); - }, []); - - return { showWelcome, dismissWelcome }; -}; -``` - -- [ ] **Step 2: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 3: Commit** - -```bash -git add extension/src/popup/views/Discover/hooks/useDiscoverWelcome.ts -git commit -m "feat: add useDiscoverWelcome hook for first-visit modal" -``` - ---- - -### Task 5: Create ProtocolRow component - -**Files:** - -- Create: `extension/src/popup/views/Discover/components/ProtocolRow/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/ProtocolRow/styles.scss` - -- [ ] **Step 1: Create ProtocolRow styles** - -Create `extension/src/popup/views/Discover/components/ProtocolRow/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.ProtocolRow { - display: flex; - gap: pxToRem(12); - align-items: center; - cursor: pointer; - - &__icon { - height: pxToRem(32); - width: pxToRem(32); - border-radius: pxToRem(10); - object-fit: cover; - flex-shrink: 0; - } - - &__label { - display: flex; - flex-direction: column; - flex: 1 0 0; - min-width: 0; - - &__subtitle { - color: var(--sds-clr-gray-11); - } - } - - &__open-button { - border-radius: 100px; - border: 1px solid var(--sds-clr-gray-06); - color: var(--sds-clr-gray-12); - display: flex; - gap: pxToRem(4); - padding: pxToRem(6) pxToRem(10); - justify-content: center; - align-items: center; - cursor: pointer; - background: transparent; - text-decoration: none; - flex-shrink: 0; - - &__icon { - height: pxToRem(14); - width: pxToRem(14); - color: var(--sds-clr-gray-09); - } - } -} -``` - -- [ ] **Step 2: Create ProtocolRow component** - -Create `extension/src/popup/views/Discover/components/ProtocolRow/index.tsx`: - -```tsx -import React from "react"; -import { Icon, Text } from "@stellar/design-system"; -import { useTranslation } from "react-i18next"; - -import { DiscoverData } from "@shared/api/types"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -interface ProtocolRowProps { - protocol: Protocol; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; -} - -export const ProtocolRow = ({ - protocol, - onRowClick, - onOpenClick, -}: ProtocolRowProps) => { - const { t } = useTranslation(); - - return ( -
onRowClick(protocol)} - > - {protocol.name} -
- - {protocol.name} - -
- - {protocol.tags[0]} - -
-
- -
- ); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add extension/src/popup/views/Discover/components/ProtocolRow/ -git commit -m "feat: add ProtocolRow component for Discover sections" -``` - ---- - -### Task 6: Create TrendingCarousel component - -**Files:** - -- Create: - `extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss` - -- [ ] **Step 1: Create TrendingCarousel styles** - -Create -`extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.TrendingCarousel { - &__label { - margin-bottom: pxToRem(12); - } - - &__scroll-container { - display: flex; - gap: pxToRem(16); - overflow-x: auto; - scroll-snap-type: x mandatory; - -ms-overflow-style: none; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - } - - &__card { - min-width: pxToRem(264); - height: pxToRem(148); - border-radius: pxToRem(12); - background-color: var(--sds-clr-gray-03); - background-size: cover; - background-position: center; - position: relative; - overflow: hidden; - cursor: pointer; - scroll-snap-align: start; - flex-shrink: 0; - - &__gradient { - position: absolute; - inset: 0; - background: linear-gradient( - 180deg, - transparent 40%, - var(--sds-clr-gray-03) 100% - ); - } - - &__content { - position: absolute; - bottom: pxToRem(12); - left: pxToRem(12); - right: pxToRem(12); - display: flex; - justify-content: space-between; - align-items: flex-end; - } - - &__tag { - color: var(--sds-clr-gray-11); - } - } -} -``` - -- [ ] **Step 2: Create TrendingCarousel component** - -Create -`extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx`: - -```tsx -import React from "react"; -import { Text } from "@stellar/design-system"; -import { useTranslation } from "react-i18next"; - -import { DiscoverData } from "@shared/api/types"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -interface TrendingCarouselProps { - items: DiscoverData; - onCardClick: (protocol: Protocol) => void; -} - -export const TrendingCarousel = ({ - items, - onCardClick, -}: TrendingCarouselProps) => { - const { t } = useTranslation(); - - if (items.length === 0) { - return null; - } - - return ( -
-
- - {t("Trending")} - -
-
- {items.map((protocol) => ( -
onCardClick(protocol)} - > -
-
- - {protocol.name} - -
- - {protocol.tags[0]} - -
-
-
- ))} -
-
- ); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add extension/src/popup/views/Discover/components/TrendingCarousel/ -git commit -m "feat: add TrendingCarousel component for Discover" -``` - ---- - -### Task 7: Create DiscoverSection component - -**Files:** - -- Create: - `extension/src/popup/views/Discover/components/DiscoverSection/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/DiscoverSection/styles.scss` - -- [ ] **Step 1: Create DiscoverSection styles** - -Create -`extension/src/popup/views/Discover/components/DiscoverSection/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.DiscoverSection { - &__header { - display: flex; - align-items: center; - gap: pxToRem(4); - cursor: pointer; - margin-bottom: pxToRem(24); - color: var(--sds-clr-gray-12); - - svg { - width: pxToRem(16); - height: pxToRem(16); - } - } - - &__list { - display: flex; - flex-direction: column; - gap: pxToRem(24); - } -} -``` - -- [ ] **Step 2: Create DiscoverSection component** - -Create -`extension/src/popup/views/Discover/components/DiscoverSection/index.tsx`: - -```tsx -import React from "react"; -import { Icon, Text } from "@stellar/design-system"; - -import { DiscoverData } from "@shared/api/types"; -import { ProtocolRow } from "../ProtocolRow"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -const MAX_VISIBLE = 5; - -interface DiscoverSectionProps { - title: string; - items: DiscoverData; - onExpand: () => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; -} - -export const DiscoverSection = ({ - title, - items, - onExpand, - onRowClick, - onOpenClick, -}: DiscoverSectionProps) => { - if (items.length === 0) { - return null; - } - - const visibleItems = items.slice(0, MAX_VISIBLE); - - return ( -
-
- - {title} - - -
-
- {visibleItems.map((protocol) => ( - - ))} -
-
- ); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add extension/src/popup/views/Discover/components/DiscoverSection/ -git commit -m "feat: add DiscoverSection component with title, chevron, and protocol list" -``` - ---- - -### Task 8: Create ProtocolDetailsPanel component - -**Files:** - -- Create: - `extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss` - -- [ ] **Step 1: Create ProtocolDetailsPanel styles** - -Create -`extension/src/popup/views/Discover/components/ProtocolDetailsPanel/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.ProtocolDetailsPanel { - padding: pxToRem(32) pxToRem(16); - - &__header { - display: flex; - align-items: center; - gap: pxToRem(12); - margin-bottom: pxToRem(24); - } - - &__icon { - width: pxToRem(40); - height: pxToRem(40); - border-radius: pxToRem(10); - object-fit: cover; - flex-shrink: 0; - } - - &__name { - flex: 1 0 0; - min-width: 0; - } - - &__open-button { - background: var(--sds-clr-gray-12); - color: var(--sds-clr-gray-01); - border: none; - border-radius: 100px; - display: flex; - gap: pxToRem(4); - padding: pxToRem(6) pxToRem(10); - justify-content: center; - align-items: center; - cursor: pointer; - flex-shrink: 0; - - &__icon { - height: pxToRem(14); - width: pxToRem(14); - } - } - - &__section { - margin-bottom: pxToRem(24); - - &__label { - color: var(--sds-clr-gray-11); - margin-bottom: pxToRem(8); - } - } - - &__domain { - display: flex; - align-items: center; - gap: pxToRem(4); - - svg { - width: pxToRem(16); - height: pxToRem(16); - color: var(--sds-clr-gray-11); - } - } - - &__tags { - display: flex; - gap: pxToRem(8); - flex-wrap: wrap; - } - - &__tag { - border-radius: 100px; - background: var(--sds-clr-lime-02); - border: 1px solid var(--sds-clr-lime-06); - padding: pxToRem(2) pxToRem(8); - color: var(--sds-clr-lime-11); - } - - &__description { - color: var(--sds-clr-gray-12); - } -} -``` - -- [ ] **Step 2: Create ProtocolDetailsPanel component** - -Create -`extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx`: - -```tsx -import React from "react"; -import { Icon, Text } from "@stellar/design-system"; -import { useTranslation } from "react-i18next"; - -import { DiscoverData } from "@shared/api/types"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -interface ProtocolDetailsPanelProps { - protocol: Protocol; - onOpen: (protocol: Protocol) => void; -} - -const getHostname = (url: string): string => { - try { - return new URL(url).hostname; - } catch { - return url; - } -}; - -export const ProtocolDetailsPanel = ({ - protocol, - onOpen, -}: ProtocolDetailsPanelProps) => { - const { t } = useTranslation(); - - return ( -
-
- {protocol.name} -
- - {protocol.name} - -
- -
- -
-
- - {t("Domain")} - -
-
- - - {getHostname(protocol.websiteUrl)} - -
-
- -
-
- - {t("Tags")} - -
-
- {protocol.tags.map((tag) => ( -
- - {tag} - -
- ))} -
-
- -
-
- - {t("Overview")} - -
-
- - {protocol.description} - -
-
-
- ); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add extension/src/popup/views/Discover/components/ProtocolDetailsPanel/ -git commit -m "feat: add ProtocolDetailsPanel component for SlideupModal content" -``` - ---- - -### Task 9: Create ExpandedRecent and ExpandedDapps components - -**Files:** - -- Create: - `extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss` -- Create: - `extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss` - -- [ ] **Step 1: Create ExpandedRecent styles** - -Create -`extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.ExpandedRecent { - &__list { - display: flex; - flex-direction: column; - gap: pxToRem(24); - padding: 0 pxToRem(24); - } - - &__menu { - position: relative; - } - - &__menu-trigger { - background: transparent; - border: none; - cursor: pointer; - color: var(--sds-clr-gray-12); - padding: pxToRem(4); - - svg { - width: pxToRem(20); - height: pxToRem(20); - } - } - - &__dropdown { - position: absolute; - right: 0; - top: pxToRem(32); - width: pxToRem(180); - background: var(--sds-clr-gray-01); - border: 1px solid var(--sds-clr-gray-06); - border-radius: pxToRem(6); - box-shadow: 0 pxToRem(10) pxToRem(20) pxToRem(-10) rgba(0, 0, 0, 0.1); - padding: pxToRem(4); - z-index: 10; - } - - &__dropdown-item { - display: flex; - align-items: center; - gap: pxToRem(8); - padding: pxToRem(8); - cursor: pointer; - border-radius: pxToRem(4); - color: var(--sds-clr-red-09); - - svg { - width: pxToRem(16); - height: pxToRem(16); - } - - &:hover { - background: var(--sds-clr-gray-03); - } - } -} -``` - -- [ ] **Step 2: Create ExpandedRecent component** - -Create `extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx`: - -```tsx -import React, { useState } from "react"; -import { Icon, Text } from "@stellar/design-system"; -import { useTranslation } from "react-i18next"; - -import { DiscoverData } from "@shared/api/types"; -import { SubviewHeader } from "popup/components/SubviewHeader"; -import { View } from "popup/basics/layout/View"; -import { ProtocolRow } from "../ProtocolRow"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -interface ExpandedRecentProps { - items: DiscoverData; - onBack: () => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; - onClearRecent: () => void; -} - -export const ExpandedRecent = ({ - items, - onBack, - onRowClick, - onOpenClick, - onClearRecent, -}: ExpandedRecentProps) => { - const { t } = useTranslation(); - const [isMenuOpen, setIsMenuOpen] = useState(false); - - return ( - <> - - - {isMenuOpen && ( -
-
{ - setIsMenuOpen(false); - onClearRecent(); - }} - data-testid="clear-recents-button" - > - - - {t("Clear recents")} - -
-
- )} -
- } - /> - -
- {items.map((protocol) => ( - - ))} -
-
- - ); -}; -``` - -- [ ] **Step 3: Create ExpandedDapps styles** - -Create -`extension/src/popup/views/Discover/components/ExpandedDapps/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.ExpandedDapps { - &__list { - display: flex; - flex-direction: column; - gap: pxToRem(24); - padding: 0 pxToRem(24); - } -} -``` - -- [ ] **Step 4: Create ExpandedDapps component** - -Create `extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx`: - -```tsx -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { DiscoverData } from "@shared/api/types"; -import { SubviewHeader } from "popup/components/SubviewHeader"; -import { View } from "popup/basics/layout/View"; -import { ProtocolRow } from "../ProtocolRow"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -interface ExpandedDappsProps { - items: DiscoverData; - onBack: () => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; -} - -export const ExpandedDapps = ({ - items, - onBack, - onRowClick, - onOpenClick, -}: ExpandedDappsProps) => { - const { t } = useTranslation(); - - return ( - <> - - -
- {items.map((protocol) => ( - - ))} -
-
- - ); -}; -``` - -- [ ] **Step 5: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 6: Commit** - -```bash -git add extension/src/popup/views/Discover/components/ExpandedRecent/ extension/src/popup/views/Discover/components/ExpandedDapps/ -git commit -m "feat: add ExpandedRecent and ExpandedDapps sub-view components" -``` - ---- - -### Task 10: Create DiscoverWelcomeModal component - -**Files:** - -- Create: - `extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss` - -- [ ] **Step 1: Create DiscoverWelcomeModal styles** - -Create -`extension/src/popup/views/Discover/components/DiscoverWelcomeModal/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.DiscoverWelcomeModal { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: var(--z-index-modal); - padding: pxToRem(24); - - &__card { - background: var(--sds-clr-gray-01); - border: 1px solid var(--sds-clr-gray-06); - border-radius: pxToRem(16); - padding: pxToRem(24); - position: relative; - z-index: 2; - max-width: pxToRem(312); - width: 100%; - } - - &__icon { - width: pxToRem(40); - height: pxToRem(40); - border-radius: pxToRem(10); - background: var(--sds-clr-lilac-03, var(--sds-clr-gray-03)); - border: 1px solid var(--sds-clr-lilac-06, var(--sds-clr-gray-06)); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: pxToRem(16); - color: var(--sds-clr-gray-12); - - svg { - width: pxToRem(20); - height: pxToRem(20); - } - } - - &__title { - margin-bottom: pxToRem(12); - } - - &__body { - color: var(--sds-clr-gray-11); - margin-bottom: pxToRem(24); - - p { - margin-bottom: pxToRem(12); - - &:last-child { - margin-bottom: 0; - } - } - } -} -``` - -- [ ] **Step 2: Create DiscoverWelcomeModal component** - -Create -`extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx`: - -```tsx -import React from "react"; -import { Button, Icon, Text } from "@stellar/design-system"; -import { useTranslation } from "react-i18next"; - -import { LoadingBackground } from "popup/basics/LoadingBackground"; - -import "./styles.scss"; - -interface DiscoverWelcomeModalProps { - onDismiss: () => void; -} - -export const DiscoverWelcomeModal = ({ - onDismiss, -}: DiscoverWelcomeModalProps) => { - const { t } = useTranslation(); - - return ( - <> -
-
-
- -
-
- - {t("Welcome to Discover!")} - -
-
- - {t( - "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.", - )} - - - {t( - "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", - )} - -
- -
-
- - - ); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add extension/src/popup/views/Discover/components/DiscoverWelcomeModal/ -git commit -m "feat: add DiscoverWelcomeModal component for first-visit onboarding" -``` - ---- - -### Task 11: Create DiscoverHome component - -**Files:** - -- Create: `extension/src/popup/views/Discover/components/DiscoverHome/index.tsx` -- Create: - `extension/src/popup/views/Discover/components/DiscoverHome/styles.scss` - -- [ ] **Step 1: Create DiscoverHome styles** - -Create `extension/src/popup/views/Discover/components/DiscoverHome/styles.scss`: - -```scss -@use "../../../../styles/utils.scss" as *; - -.DiscoverHome { - &__sections { - display: flex; - flex-direction: column; - gap: pxToRem(32); - } - - &__footer { - display: flex; - flex-direction: column; - gap: pxToRem(8); - border-radius: pxToRem(16); - background-color: var(--sds-clr-gray-02); - padding: pxToRem(16); - margin-top: pxToRem(32); - - &__copy { - color: var(--sds-clr-gray-11); - font-size: pxToRem(12); - line-height: pxToRem(14); - } - } -} -``` - -- [ ] **Step 2: Create DiscoverHome component** - -Create `extension/src/popup/views/Discover/components/DiscoverHome/index.tsx`: - -```tsx -import React from "react"; -import { Icon } from "@stellar/design-system"; -import { useTranslation } from "react-i18next"; - -import { DiscoverData } from "@shared/api/types"; -import { SubviewHeader } from "popup/components/SubviewHeader"; -import { View } from "popup/basics/layout/View"; -import { TrendingCarousel } from "../TrendingCarousel"; -import { DiscoverSection } from "../DiscoverSection"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; - -interface DiscoverHomeProps { - trendingItems: DiscoverData; - recentItems: DiscoverData; - dappsItems: DiscoverData; - onClose: () => void; - onExpandRecent: () => void; - onExpandDapps: () => void; - onCardClick: (protocol: Protocol) => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; -} - -export const DiscoverHome = ({ - trendingItems, - recentItems, - dappsItems, - onClose, - onExpandRecent, - onExpandDapps, - onCardClick, - onRowClick, - onOpenClick, -}: DiscoverHomeProps) => { - const { t } = useTranslation(); - - return ( - <> - } - customBackAction={onClose} - /> - -
- - - -
- {dappsItems.length > 0 && ( -
-
- {`${t( - "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.", - )} ${t("Freighter does not endorse any listed items.")}`} -
-
- {t( - "By using these services, you act at your own risk, and Freighter or Stellar Development Foundation (SDF) bears no liability for any resulting losses or damages.", - )} -
-
- )} -
- - ); -}; -``` - -- [ ] **Step 3: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 4: Commit** - -```bash -git add extension/src/popup/views/Discover/components/DiscoverHome/ -git commit -m "feat: add DiscoverHome main view with trending, sections, and footer" -``` - ---- - -### Task 12: Rewrite Discover orchestrator - -**Files:** - -- Modify: `extension/src/popup/views/Discover/index.tsx` -- Modify: `extension/src/popup/views/Discover/styles.scss` - -- [ ] **Step 1: Rewrite Discover styles** - -Replace the contents of `extension/src/popup/views/Discover/styles.scss` with: - -```scss -.Discover { - height: 100%; - display: flex; - flex-direction: column; - background: var(--sds-clr-gray-01); -} -``` - -- [ ] **Step 2: Rewrite Discover orchestrator** - -Replace the contents of `extension/src/popup/views/Discover/index.tsx` with: - -```tsx -import React, { useCallback, useState } from "react"; - -import { DiscoverData } from "@shared/api/types"; -import { openTab } from "popup/helpers/navigate"; -import { - addRecentProtocol, - clearRecentProtocols, -} from "popup/helpers/recentProtocols"; -import { SlideupModal } from "popup/components/SlideupModal"; -import { Loading } from "popup/components/Loading"; - -import { useDiscoverData } from "./hooks/useDiscoverData"; -import { useDiscoverWelcome } from "./hooks/useDiscoverWelcome"; -import { DiscoverHome } from "./components/DiscoverHome"; -import { ExpandedRecent } from "./components/ExpandedRecent"; -import { ExpandedDapps } from "./components/ExpandedDapps"; -import { ProtocolDetailsPanel } from "./components/ProtocolDetailsPanel"; -import { DiscoverWelcomeModal } from "./components/DiscoverWelcomeModal"; - -import "./styles.scss"; - -type Protocol = DiscoverData[number]; -type DiscoverView = "main" | "recent" | "dapps"; - -interface DiscoverProps { - onClose: () => void; -} - -export const Discover = ({ onClose }: DiscoverProps) => { - const [activeView, setActiveView] = useState("main"); - const [selectedProtocol, setSelectedProtocol] = useState( - null, - ); - const [isDetailsOpen, setIsDetailsOpen] = useState(false); - - const { isLoading, trendingItems, recentItems, dappsItems, refreshRecent } = - useDiscoverData(); - const { showWelcome, dismissWelcome } = useDiscoverWelcome(); - - const handleOpenProtocol = useCallback( - async (protocol: Protocol) => { - openTab(protocol.websiteUrl); - await addRecentProtocol(protocol.websiteUrl); - await refreshRecent(); - }, - [refreshRecent], - ); - - const handleRowClick = useCallback((protocol: Protocol) => { - setSelectedProtocol(protocol); - setIsDetailsOpen(true); - }, []); - - const handleDetailsOpen = useCallback( - async (protocol: Protocol) => { - setIsDetailsOpen(false); - setTimeout(async () => { - setSelectedProtocol(null); - await handleOpenProtocol(protocol); - }, 200); - }, - [handleOpenProtocol], - ); - - const handleClearRecent = useCallback(async () => { - await clearRecentProtocols(); - await refreshRecent(); - setActiveView("main"); - }, [refreshRecent]); - - if (isLoading) { - return ( -
- -
- ); - } - - return ( -
- {activeView === "main" && ( - setActiveView("recent")} - onExpandDapps={() => setActiveView("dapps")} - onCardClick={handleRowClick} - onRowClick={handleRowClick} - onOpenClick={handleOpenProtocol} - /> - )} - {activeView === "recent" && ( - setActiveView("main")} - onRowClick={handleRowClick} - onOpenClick={handleOpenProtocol} - onClearRecent={handleClearRecent} - /> - )} - {activeView === "dapps" && ( - setActiveView("main")} - onRowClick={handleRowClick} - onOpenClick={handleOpenProtocol} - /> - )} - - { - setIsDetailsOpen(open); - if (!open) { - setSelectedProtocol(null); - } - }} - > - {selectedProtocol ? ( - - ) : ( -
- )} - - - {showWelcome && } -
- ); -}; -``` - -- [ ] **Step 3: Delete old useGetDiscoverData hook** - -Delete `extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts` — it is -replaced by `useDiscoverData.ts`. - -- [ ] **Step 4: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 5: Commit** - -```bash -git add extension/src/popup/views/Discover/index.tsx extension/src/popup/views/Discover/styles.scss -git rm extension/src/popup/views/Discover/hooks/useGetDiscoverData.ts -git commit -m "feat: rewrite Discover orchestrator with sub-views, modals, and data hooks" -``` - ---- - -### Task 13: Wire up the Sheet in Account view and update AccountHeader - -**Files:** - -- Modify: `extension/src/popup/views/Account/index.tsx` -- Modify: `extension/src/popup/components/account/AccountHeader/index.tsx` - -- [ ] **Step 1: Add isDiscoverOpen state and Sheet to Account view** - -In `extension/src/popup/views/Account/index.tsx`: - -Add imports at the top (with the existing imports): - -```typescript -import { useState } from "react"; -import { - Sheet, - SheetContent, - ScreenReaderOnly, - SheetTitle, -} from "popup/basics/shadcn/Sheet"; -import { Discover } from "popup/views/Discover"; -``` - -Update the `useEffect, useRef, useContext` import from React to also include -`useState`: - -```typescript -import React, { useEffect, useRef, useContext, useState } from "react"; -``` - -Inside the `Account` component, add state after the existing hooks (around line -53): - -```typescript -const [isDiscoverOpen, setIsDiscoverOpen] = useState(false); -``` - -Add `onDiscoverClick` prop to `AccountHeader` (around line 188): - -```tsx - setIsDiscoverOpen(true)} -/> -``` - -Add the Discover Sheet after the closing `` of the existing return but before -the final fragment close. Place it right before the final `` at the end of -the return (after the `NotFundedMessage` conditional block, around line 302): - -```tsx - - e.preventDefault()} - aria-describedby={undefined} - side="bottom" - className="Discover__sheet-wrapper" - > - - Discover - - setIsDiscoverOpen(false)} /> - - -``` - -- [ ] **Step 2: Update AccountHeader to accept onDiscoverClick prop** - -In `extension/src/popup/components/account/AccountHeader/index.tsx`: - -Add `onDiscoverClick` to the `AccountHeaderProps` interface: - -```typescript -interface AccountHeaderProps { - allowList: string[]; - currentAccountName: string; - isFunded: boolean; - onAllowListRemove: () => void; - onClickRow: (updatedValues: { - publicKey?: string; - network?: NetworkDetails; - }) => Promise; - publicKey: string; - roundedTotalBalanceUsd: string; - refreshHiddenCollectibles: () => Promise; - isCollectibleHidden: (collectionAddress: string, tokenId: string) => boolean; - onDiscoverClick: () => void; -} -``` - -Add `onDiscoverClick` to the destructured props: - -```typescript -export const AccountHeader = ({ - allowList, - currentAccountName, - isFunded, - onAllowListRemove, - onClickRow, - publicKey, - roundedTotalBalanceUsd, - refreshHiddenCollectibles, - isCollectibleHidden, - onDiscoverClick, -}: AccountHeaderProps) => { -``` - -Update the Discover button's `onClick` handler (around line 310). Change: - -```tsx -onClick={() => navigateTo(ROUTES.discover, navigate)} -``` - -to: - -```tsx -onClick = { onDiscoverClick }; -``` - -Remove the `ROUTES` import for `discover` if it's no longer used elsewhere in -this file (it is still used for other routes like `ROUTES.manageAssets`, -`ROUTES.settings`, etc., so keep the import but the `discover` route reference -is gone from this file). - -- [ ] **Step 3: Add Sheet wrapper styles** - -Add to `extension/src/popup/views/Discover/styles.scss`: - -```scss -.Discover { - height: 100%; - display: flex; - flex-direction: column; - background: var(--sds-clr-gray-01); - - &__sheet-wrapper { - height: var(--popup--height); - } -} -``` - -Note: the `&__sheet-wrapper` class is used on the `SheetContent` to constrain it -to the popup height. - -- [ ] **Step 4: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 5: Commit** - -```bash -git add extension/src/popup/views/Account/index.tsx extension/src/popup/components/account/AccountHeader/index.tsx extension/src/popup/views/Discover/styles.scss -git commit -m "feat: wire up Discover Sheet in Account view and update AccountHeader" -``` - ---- - -### Task 14: Remove the /discover route - -**Files:** - -- Modify: `extension/src/popup/constants/routes.ts` -- Modify: `extension/src/popup/Router.tsx` -- Modify: `extension/src/popup/constants/metricsNames.ts` -- Modify: `extension/src/popup/metrics/views.ts` - -- [ ] **Step 1: Remove discover from routes enum** - -In `extension/src/popup/constants/routes.ts`, remove: - -```typescript -discover = "/discover", -``` - -- [ ] **Step 2: Remove Discover route and import from Router** - -In `extension/src/popup/Router.tsx`: - -Remove the import (line 63): - -```typescript -import { Discover } from "popup/views/Discover"; -``` - -Remove the route (line 278): - -```tsx -} /> -``` - -- [ ] **Step 3: Remove discover metric name** - -In `extension/src/popup/constants/metricsNames.ts`, remove: - -```typescript -discover: "loaded screen: discover", -``` - -- [ ] **Step 4: Remove discover metric route mapping** - -In `extension/src/popup/metrics/views.ts`, remove: - -```typescript -[ROUTES.discover]: METRIC_NAMES.discover, -``` - -- [ ] **Step 5: Check for any remaining ROUTES.discover references** - -Run: `cd extension && grep -r "ROUTES.discover" src/` Expected: No matches -(AccountHeader was already updated in Task 13) - -- [ ] **Step 6: Verify build** - -Run: `cd extension && npx tsc --noEmit` Expected: No type errors - -- [ ] **Step 7: Commit** - -```bash -git add extension/src/popup/constants/routes.ts extension/src/popup/Router.tsx extension/src/popup/constants/metricsNames.ts extension/src/popup/metrics/views.ts -git commit -m "refactor: remove /discover route, now handled via Sheet overlay" -``` - ---- - -### Task 15: Update existing tests - -**Files:** - -- Modify: `extension/src/popup/views/__tests__/Discover.test.tsx` - -- [ ] **Step 1: Rewrite the Discover test** - -Replace the contents of `extension/src/popup/views/__tests__/Discover.test.tsx` -with: - -```tsx -import React from "react"; -import { render, waitFor, screen } from "@testing-library/react"; -import { - DEFAULT_NETWORKS, - MAINNET_NETWORK_DETAILS, -} from "@shared/constants/stellar"; -import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState"; -import * as ApiInternal from "@shared/api/internal"; -import * as RecentProtocols from "popup/helpers/recentProtocols"; -import { Wrapper, mockAccounts } from "../../__testHelpers__"; -import { Discover } from "../Discover"; - -// Mock browser.storage.local -jest.mock("webextension-polyfill", () => ({ - storage: { - local: { - get: jest.fn().mockResolvedValue({}), - set: jest.fn().mockResolvedValue(undefined), - remove: jest.fn().mockResolvedValue(undefined), - }, - }, - tabs: { - create: jest.fn(), - }, -})); - -const mockProtocols = [ - { - description: "A lending protocol", - name: "Blend", - iconUrl: "https://example.com/blend.png", - websiteUrl: "https://blend.capital", - tags: ["Lending", "DeFi"], - isBlacklisted: false, - backgroundUrl: "https://example.com/blend-bg.png", - isTrending: true, - }, - { - description: "An exchange", - name: "Soroswap", - iconUrl: "https://example.com/soroswap.png", - websiteUrl: "https://soroswap.finance", - tags: ["Exchange"], - isBlacklisted: false, - isTrending: false, - }, - { - description: "Blacklisted protocol", - name: "BadProtocol", - iconUrl: "https://example.com/bad.png", - websiteUrl: "https://bad.com", - tags: ["Scam"], - isBlacklisted: true, - isTrending: false, - }, -]; - -describe("Discover", () => { - beforeEach(() => { - jest.spyOn(ApiInternal, "getDiscoverData").mockResolvedValue(mockProtocols); - jest.spyOn(RecentProtocols, "getRecentProtocols").mockResolvedValue([]); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - const renderDiscover = () => - render( - - - , - ); - - it("displays trending protocols in the carousel", async () => { - renderDiscover(); - - await waitFor(() => { - expect(screen.getByTestId("trending-carousel")).toBeInTheDocument(); - }); - - const trendingCards = screen.getAllByTestId("trending-card"); - expect(trendingCards).toHaveLength(1); - expect(trendingCards[0]).toHaveTextContent("Blend"); - }); - - it("displays dApps section with non-blacklisted protocols", async () => { - renderDiscover(); - - await waitFor(() => { - expect(screen.getByTestId("discover-section-dapps")).toBeInTheDocument(); - }); - - const protocolRows = screen.getAllByTestId("protocol-row"); - // Blend + Soroswap (BadProtocol is blacklisted) - expect(protocolRows).toHaveLength(2); - expect(protocolRows[0]).toHaveTextContent("Blend"); - expect(protocolRows[1]).toHaveTextContent("Soroswap"); - }); - - it("hides recent section when no recent protocols exist", async () => { - renderDiscover(); - - await waitFor(() => { - expect(screen.getByTestId("discover-section-dapps")).toBeInTheDocument(); - }); - - expect( - screen.queryByTestId("discover-section-recent"), - ).not.toBeInTheDocument(); - }); - - it("shows recent section when recent protocols exist", async () => { - jest - .spyOn(RecentProtocols, "getRecentProtocols") - .mockResolvedValue([ - { websiteUrl: "https://blend.capital", lastAccessed: Date.now() }, - ]); - - renderDiscover(); - - await waitFor(() => { - expect(screen.getByTestId("discover-section-recent")).toBeInTheDocument(); - }); - }); -}); -``` - -- [ ] **Step 2: Run the tests** - -Run: -`cd extension && npx jest src/popup/views/__tests__/Discover.test.tsx --no-coverage` -Expected: All tests pass - -- [ ] **Step 3: Commit** - -```bash -git add extension/src/popup/views/__tests__/Discover.test.tsx -git commit -m "test: rewrite Discover tests for new Sheet-based component structure" -``` - ---- - -### Task 16: Manual visual verification - -- [ ] **Step 1: Build the extension** - -Run: `cd extension && yarn build:extension` Expected: Build succeeds with no -errors - -- [ ] **Step 2: Load in browser and verify** - -Load the built extension in Chrome as an unpacked extension from -`extension/build/`. - -Verify the following flows: - -1. Home screen shows the "Discover" button (compass icon + label) in the header -2. Clicking "Discover" slides the Discover Sheet up from the bottom, revealing - Home underneath -3. Trending carousel shows cards with background images, horizontally scrollable -4. Recent section is hidden (no recent visits yet) -5. dApps section shows all non-blacklisted protocols -6. Clicking a protocol row opens the protocol details SlideupModal (dark theme) -7. Clicking "Open" in the details modal opens the protocol in a new browser tab -8. After opening a protocol, the Recent section appears on return -9. Clicking "Recent >" expands to the full recent list with back arrow and - three-dot menu -10. "Clear recents" removes all recent entries and returns to main view -11. Clicking "dApps >" expands to the full dApps list -12. Back arrow in expanded views returns to the main Discover view -13. "X" button closes the Sheet with a smooth slide-down animation revealing the - Home screen -14. Welcome modal appears on first visit, "Let's go" dismisses it, does not - appear again - -- [ ] **Step 3: Fix any visual issues found during verification** - -Address spacing, colors, or layout issues comparing against the Figma designs. - -- [ ] **Step 4: Final commit** - -```bash -git add -A -git commit -m "fix: visual polish for Discover V2 after manual verification" -``` diff --git a/docs/superpowers/specs/2026-03-30-discover-v2-design.md b/docs/superpowers/specs/2026-03-30-discover-v2-design.md deleted file mode 100644 index 2ae03267e3..0000000000 --- a/docs/superpowers/specs/2026-03-30-discover-v2-design.md +++ /dev/null @@ -1,340 +0,0 @@ -# Discover V2 — Extension Design Spec - -Port the Discover tab refactoring from freighter-mobile (PR #780) to the -freighter browser extension. The mobile version has a bottom navigation bar, -in-app WebView browser, and tab management — none of which apply to the -extension. The extension version is simpler: a Sheet overlay with scrollable -sections, protocol details modal, and new-tab navigation. - -## Figma References - -- **Extension Discover tab**: Figma file `C3G0a4Gd6RQyplRBppGDsL`, nodes - `7839-5623`, `7839-5921` -- **Extension Recent expanded**: node `7840-31089` -- **Extension dApps expanded**: node `7839-5513` -- **Extension Protocol details**: node `7376-5216` -- **Extension Home with Discover button**: node `7301-18521` -- **Mobile Discover (reference)**: Figma file `KwkHXQxbNmDllwermJtnRu`, nodes - `11115-14760`, `9892-93605`, `10233-25250`, `10052-14241`, `10852-35337` - -## Decisions - -| Decision | Choice | Rationale | -| --------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------- | -| Main Discover entry | Sheet overlay (like AssetDetail) | Keeps Home mounted underneath for smooth slide-down reveal. Matches existing pattern. | -| Sub-views (Recent/dApps expanded) | Local state inside Sheet | Sheet stays open, content swaps. Instant transition. Same approach as mobile. | -| Protocol details | SlideupModal (like TransactionDetail) | Dark theme. Existing component, well-tested. | -| Recent protocols storage | Standalone helper + `browser.storage.local` | Self-contained, no Redux overhead. Custom hook for React integration. | -| Welcome modal storage | `browser.storage.local` flag | Consistent with recent protocols. Async read on mount is trivial. | -| Opening a protocol | `openTab(url)` — new browser tab | Extension is web-based, no need for in-app WebView. | -| Trending card size | 264x148px (near full-width) | ~85% of content width. One card visible + peek of next. Matches mobile proportions. | -| Route changes | Remove `/discover` route | Everything lives inside the Sheet overlay now. | -| Analytics/metrics | Deferred | Implement after initial feature is stable. | -| E2E tests | Deferred | Implement after initial feature is stable. | - -## Data Layer - -### DiscoverData type update - -`@shared/api/types/types.ts` — add two fields to the existing `DiscoverData` -type: - -```typescript -export type DiscoverData = { - description: string; - iconUrl: string; - name: string; - websiteUrl: string; - tags: string[]; - isBlacklisted: boolean; - backgroundUrl?: string; // NEW — trending card background image - isTrending: boolean; // NEW — whether protocol appears in Trending -}[]; -``` - -### getDiscoverData() update - -`@shared/api/internal.ts` — map `background_url` and `is_trending` from the -backend response (already returned by the API, just not mapped): - -```typescript -return parsedResponse.data.protocols.map((entry) => ({ - // ...existing fields... - backgroundUrl: entry.background_url, - isTrending: entry.is_trending, -})); -``` - -### Recent protocols helper - -`popup/helpers/recentProtocols.ts` — standalone module using -`browser.storage.local`: - -- `getRecentProtocols(): Promise` — reads stored entries -- `addRecentProtocol(url: string): Promise` — adds or moves URL to front, - caps at 5 entries -- `clearRecentProtocols(): Promise` — removes all entries - -Storage shape: - -```typescript -interface RecentProtocolEntry { - websiteUrl: string; - lastAccessed: number; // timestamp -} -// Stored under key "recentProtocols" in browser.storage.local -``` - -### useDiscoverData hook - -`popup/views/Discover/hooks/useDiscoverData.ts` — replaces the existing -`useGetDiscoverData`: - -- Fetches protocols via `getDiscoverData()` -- Reads recent protocols via `getRecentProtocols()` -- Returns computed lists: - - `trendingItems` — protocols where `isTrending === true` - - `recentItems` — recent URLs matched against fetched protocols (max 5) - - `dappsItems` — all non-blacklisted protocols - - `allProtocols` — full list for lookups - - `isLoading`, `error` — request state -- Exposes `refreshRecent()` to re-read recent protocols after a visit - -### useDiscoverWelcome hook - -`popup/views/Discover/hooks/useDiscoverWelcome.ts`: - -- Reads `hasSeenDiscoverWelcome` from `browser.storage.local` on mount -- Returns `{ showWelcome: boolean, dismissWelcome: () => void }` -- `dismissWelcome()` writes flag to storage and sets local state to false - -## Navigation & Transitions - -### Main Discover screen — Sheet overlay - -The `Account` view (`popup/views/Account/index.tsx`) is the common parent of -`AccountHeader` and `AccountAssets`. It owns the `isDiscoverOpen` state and -passes it down: - -- `Account` holds `const [isDiscoverOpen, setIsDiscoverOpen] = useState(false)` -- `AccountHeader` receives `onDiscoverClick` prop — the Discover button calls - `onDiscoverClick()` instead of `navigateTo` -- The `Sheet` + `SheetContent side="bottom"` is rendered in the `Account` view - itself (same level as the existing layout), controlled by `isDiscoverOpen` -- Home screen stays mounted underneath — smooth slide-down reveal on close -- The Discover content renders inside the Sheet -- Header uses `SubviewHeader` with `customBackIcon` (X icon) and - `customBackAction` calling `setIsDiscoverOpen(false)` - -### Sub-views inside the Sheet - -Local state variable controls which content is rendered: - -```typescript -type DiscoverView = "main" | "recent" | "dapps"; -const [activeView, setActiveView] = useState("main"); -``` - -- `'main'` — DiscoverHome (trending + sections + footer) -- `'recent'` — ExpandedRecent (full list with clear menu) -- `'dapps'` — ExpandedDapps (full list) - -Instant content swap between sub-views (no transition animation). Back buttons -in expanded views call `setActiveView('main')`. - -### Protocol details — SlideupModal - -- Rendered within the Sheet, layered on top of the current sub-view -- Managed by `selectedProtocol` local state (null = hidden, protocol object = - shown) -- Uses the existing `SlideupModal` component with dark theme - (`--sds-clr-gray-01` background) -- Dismissed by clicking the `LoadingBackground` overlay - -### Opening a protocol - -- Calls `openTab(protocol.websiteUrl)` to open a new browser tab -- Calls `addRecentProtocol(protocol.websiteUrl)` to record the visit -- Calls `refreshRecent()` to update the Recent section - -### Welcome modal - -- Rendered within the Sheet on first visit (when `showWelcome` is true) -- Overlay modal on top of the Discover content -- "Let's go" button calls `dismissWelcome()` - -### Route cleanup - -- Remove the `/discover` route from `routes.ts` and `Router.tsx` -- Remove the old `Discover` view import from `Router.tsx` -- Update `AccountHeader` to toggle Sheet state instead of calling `navigateTo` - -## Components - -### Discover (index.tsx) — orchestrator - -Main component rendered inside the Sheet. Manages: - -- `activeView` state (`'main' | 'recent' | 'dapps'`) -- `selectedProtocol` state (for SlideupModal) -- Welcome modal visibility (via `useDiscoverWelcome`) -- Data fetching (via `useDiscoverData`) -- Renders the active sub-view + ProtocolDetailsPanel in SlideupModal + - DiscoverWelcomeModal - -### DiscoverHome — main scrollable view - -Content of the `'main'` sub-view: - -- `SubviewHeader` with "Discover" title, X close icon via `customBackIcon` / - `customBackAction` -- `TrendingCarousel` section with "Trending" label -- `DiscoverSection` for Recent (hidden if no recent items) — chevron navigates - to `'recent'` view -- `DiscoverSection` for dApps — chevron navigates to `'dapps'` view -- Legal disclaimer footer (same text as current implementation) - -### TrendingCarousel - -Horizontal scrollable container: - -- Cards: 264px wide x 148px tall, 12px border-radius -- Background image with gradient overlay (bottom-to-top, from - `--sds-clr-gray-03` to transparent) -- Protocol name (bottom-left, semi-bold 14px) + primary tag (bottom-right, - medium 14px, secondary color) -- 16px gap between cards -- Container has horizontal overflow scroll, no scrollbar -- Tapping a card sets `selectedProtocol` (opens details modal) - -### DiscoverSection - -Reusable section wrapper (used for both Recent and dApps on the main view): - -- Header row: section title (Text, sm, semi-bold) + `Icon.ChevronRight` — - clickable, triggers expand callback -- Vertical list of up to 5 `ProtocolRow` items -- 24px gap between rows - -### ProtocolRow - -Reusable row component (used in sections and expanded views): - -- 32px x 32px rounded (10px radius) protocol icon -- 12px gap to text column -- Protocol name (Text, sm, medium) -- Primary tag as subtitle (Text, xs, medium, `--sds-clr-gray-11`) -- "Open" pill button on far right: border `--sds-clr-gray-06`, text + - `Icon.LinkExternal01` (14px) -- Tapping the row (not the button) opens protocol details modal -- Tapping "Open" button calls `openTab(url)` + `addRecentProtocol(url)` - -### ExpandedRecent - -Full-page sub-view for recent protocols: - -- `SubviewHeader` with back arrow (default), "Recent" title, `rightButton` with - three-dot menu icon -- Three-dot menu: dropdown with "Clear recents" option (red text - `--sds-clr-red-09`, trash icon) -- Full scrollable list of recent `ProtocolRow` items -- "Clear recents" calls `clearRecentProtocols()` + `refreshRecent()` + returns - to main view - -### ExpandedDapps - -Full-page sub-view for all dApps: - -- `SubviewHeader` with back arrow, "dApps" title, no right button -- Full scrollable list of all `ProtocolRow` items - -### ProtocolDetailsPanel - -Content rendered inside `SlideupModal` (dark theme): - -- Header row: 40px icon (10px radius) + protocol name (Text, lg, medium) + - filled "Open" button (dark bg, white text, pill shape) -- Domain section: "Domain" label (Text, xs, medium, `--sds-clr-gray-11`) + globe - icon + hostname (Text, sm, medium) -- Tags section: "Tags" label + pill badges (success variant, lime colors) -- Overview section: "Overview" label + description text (Text, sm, regular) -- 24px vertical spacing between sections -- "Open" button calls `openTab(url)` + `addRecentProtocol(url)` - -### DiscoverWelcomeModal - -First-time onboarding modal: - -- Rendered as an overlay on top of Discover content (using `LoadingBackground` + - positioned card) -- Compass icon in a rounded container -- "Welcome to Discover!" title -- Two paragraphs: ecosystem intro + third-party disclaimer -- "Let's go" CTA button — dismisses the modal and writes flag to - `browser.storage.local` - -## File Structure - -``` -@shared/api/ - types/types.ts # UPDATE: add backgroundUrl, isTrending to DiscoverData - internal.ts # UPDATE: map background_url, is_trending - -extension/src/popup/ - views/Discover/ - index.tsx # REWRITE: orchestrator (sub-views, modals, data) - styles.scss # REWRITE: top-level styles - hooks/ - useDiscoverData.ts # NEW: replaces useGetDiscoverData - useDiscoverWelcome.ts # NEW: browser.storage.local welcome flag - components/ - DiscoverHome/ - index.tsx # NEW: main scrollable view - styles.scss - TrendingCarousel/ - index.tsx # NEW: horizontal card carousel - styles.scss - DiscoverSection/ - index.tsx # NEW: section wrapper (title + chevron + rows) - styles.scss - ProtocolRow/ - index.tsx # NEW: reusable protocol row - styles.scss - ExpandedRecent/ - index.tsx # NEW: full recent list with clear menu - styles.scss - ExpandedDapps/ - index.tsx # NEW: full dApps list - styles.scss - ProtocolDetailsPanel/ - index.tsx # NEW: SlideupModal content - styles.scss - DiscoverWelcomeModal/ - index.tsx # NEW: first-time modal - styles.scss - helpers/ - recentProtocols.ts # NEW: browser.storage.local CRUD - views/Account/ - index.tsx # UPDATE: add isDiscoverOpen state + Discover Sheet - components/account/ - AccountHeader/index.tsx # UPDATE: Discover button calls onDiscoverClick prop - -MODIFY: - popup/constants/routes.ts # remove discover route - popup/Router.tsx # remove Discover route + import - -REMOVE: - popup/views/Discover/hooks/useGetDiscoverData.ts # replaced by useDiscoverData - -REWRITE: - popup/views/__tests__/Discover.test.tsx # rewrite for new Sheet-based structure -``` - -## Out of Scope - -- Analytics/metrics events — deferred to follow-up -- E2E tests — deferred to follow-up -- Bottom navigation bar (mobile-only concept) -- In-app WebView / tab management (mobile-only concept) -- Search/URL bar (mobile-only concept) -- Tab overview grid (mobile-only concept) From f59150c96db6fdde85bd4c670bdbb41143035d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 18:38:50 -0700 Subject: [PATCH 24/44] globally hides the scrollbar --- extension/src/popup/styles/global.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/extension/src/popup/styles/global.scss b/extension/src/popup/styles/global.scss index 0f5dc2e54e..409aeb4238 100644 --- a/extension/src/popup/styles/global.scss +++ b/extension/src/popup/styles/global.scss @@ -28,6 +28,15 @@ --z-index-maintenance-screen: 100; } +html, +body { + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} + body { color: var(--sds-clr-gray-12); overscroll-behavior: none; From 8321cc4d2de2a9c0c8f4f0e1d3ee1534a7d16321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Tue, 31 Mar 2026 18:52:56 -0700 Subject: [PATCH 25/44] add translation wrapper --- extension/src/popup/views/Account/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index 078e34f635..0eb8a397e4 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -318,7 +318,7 @@ export const Account = () => { className="Discover__sheet-wrapper" > - Discover + {t("Discover")} setIsDiscoverOpen(false)} /> From 3cc3cfca7be32866bcaab20d2aaa0328f4894738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 10:22:48 -0700 Subject: [PATCH 26/44] add comment --- extension/src/popup/views/Discover/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/extension/src/popup/views/Discover/index.tsx b/extension/src/popup/views/Discover/index.tsx index f39ff634ed..f6d555c3e6 100644 --- a/extension/src/popup/views/Discover/index.tsx +++ b/extension/src/popup/views/Discover/index.tsx @@ -54,6 +54,7 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { const handleDetailsOpen = useCallback( async (protocol: Protocol) => { setIsDetailsOpen(false); + // Wait for the SlideupModal close animation before clearing state setTimeout(async () => { setSelectedProtocol(null); await handleOpenProtocol(protocol); From 6dc2d5d02b56da693d3cdd4e24cf58a2d2a7ba3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 10:39:16 -0700 Subject: [PATCH 27/44] pt translations --- .../src/popup/locales/pt/translation.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index c6a7c8e914..19f8b79b38 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -97,7 +97,7 @@ "Clear": "Limpar", "Clear Flags": "Limpar Flags", "Clear Override": "Clear Override", - "Clear recents": "Clear recents", + "Clear recents": "Limpar recentes", "Close": "Fechar", "Coinbase Logo": "Logo Coinbase", "Collectible": "Colecionável", @@ -167,7 +167,7 @@ "Discover": "Descobrir", "Do not proceed": "Não prosseguir", "Do this later": "Fazer isso depois", - "Domain": "Domain", + "Domain": "Domínio", "Don’t share this phrase with anyone": "Não compartilhe esta frase com ninguém", "Done": "Concluído", "Download on iOS or Android today": "Baixe no iOS ou Android hoje", @@ -314,7 +314,7 @@ "Leave Feedback": "Deixar Feedback", "Leave feedback about Blockaid warnings and messages": "Deixar feedback sobre avisos e mensagens do Blockaid", "Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust.": "O Ledger não exibirá os detalhes da transação na tela do dispositivo antes de assinar, então certifique-se de interagir apenas com aplicativos que você conhece e confia.", - "Let's go": "Let's go", + "Let's go": "Vamos lá", "Limit": "Limite", "Links": "Links", "Liquidity Pool ID": "ID do Pool de Liquidez", @@ -403,7 +403,7 @@ "Order is incorrect, try again": "A ordem está incorreta, tente novamente", "Overridden response": "Overridden response", "Override Blockaid security responses for testing different security states (DEV only)": "Override Blockaid security responses for testing different security states (DEV only)", - "Overview": "Overview", + "Overview": "Visão geral", "Parameters": "Parâmetros", "Passphrase": "Frase secreta", "Password": "Senha", @@ -436,7 +436,7 @@ "Ready to migrate": "Pronto para migrar", "Receive": "Receber", "Received": "Recebido", - "Recent": "Recent", + "Recent": "Recentes", "Recents": "Recentes", "Recovery Phrase": "Frase de Recuperação", "Refresh": "Atualizar", @@ -542,7 +542,7 @@ "Swapped!": "Trocado!", "Swapping": "Trocando", "Switch to this network": "Alternar para esta rede", - "Tags": "Tags", + "Tags": "Categorias", "Terms of Service": "Termos de Serviço", "Terms of Use": "Termos de Uso", "The authorization entry is for": "A entrada de autorização é para", @@ -560,7 +560,7 @@ "The website <1>{url} does not use an SSL certificate.": "O site <1>{url} não usa um certificado SSL.", "There are no sites to display at this moment.": "Não há sites para exibir no momento.", "These assets are not on any of your lists. Proceed with caution before adding.": "Esses ativos não estão em nenhuma de suas listas. Prossiga com cautela antes de adicionar.", - "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.": "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", + "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.": "Estes serviços são operados por terceiros independentes, não pela Freighter ou SDF. A inclusão aqui não constitui um endosso. DeFi envolve riscos, incluindo perda de fundos. Use por sua conta e risco.", "These words are your wallet’s keys—store them securely to keep your funds safe.": "Essas palavras são as chaves da sua carteira—guarde-as com segurança para manter seus fundos seguros.", "This asset does not appear safe for the following reasons.": "This asset does not appear safe for the following reasons.", "This asset has a balance": "Este ativo tem um saldo", @@ -619,7 +619,7 @@ "Transaction Timeout": "Tempo Limite da Transação", "Transfer from another account": "Transferir de outra conta", "Transfer from Coinbase, buy with debit and credit cards or bank transfer *": "Transferir do Coinbase, comprar com cartões de débito e crédito ou transferência bancária *", - "Trending": "Trending", + "Trending": "Em alta", "trustlines": "linhas de confiança", "Trustor": "Fidedigno", "Type": "Tipo", @@ -673,7 +673,7 @@ "We were unable to scan this transaction for security issues": "We were unable to scan this transaction for security issues", "WEBSITE CONNECTION IS NOT SECURE": "CONEXÃO DO WEBSITE NÃO É SEGURA", "Welcome back": "Bem-vindo de volta", - "Welcome to Discover!": "Welcome to Discover!", + "Welcome to Discover!": "Bem-vindo ao Discover!", "What is this transaction for? (optional)": "Para que é esta transação? (opcional)", "What’s new": "O que há de novo", "Wrong simulation result": "Resultado de simulação incorreto", @@ -710,7 +710,7 @@ "Your account data could not be fetched at this time.": "Os dados da sua conta não puderam ser buscados neste momento.", "Your assets": "Seus ativos", "Your available XLM balance is not enough to pay for the transaction fee.": "Seu saldo XLM disponível não é suficiente para pagar a taxa de transação.", - "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.": "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.", + "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.": "Sua porta de entrada para o ecossistema Stellar. Navegue e conecte-se a aplicações descentralizadas construídas na Stellar.", "Your recovery phrase": "Sua frase de recuperação", "Your Recovery Phrase": "Sua Frase de Recuperação", "Your recovery phrase gives you access to your account and is the only way to access it in a new browser.": "Sua frase de recuperação lhe dá acesso à sua conta e é a única maneira de acessá-la em um novo navegador.", From ccd20d8c2be9afba223b122fc5ec2e1922ff161f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 10:43:49 -0700 Subject: [PATCH 28/44] more missing pt translations --- .../src/popup/locales/pt/translation.json | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 19f8b79b38..c2f80e817f 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -79,7 +79,7 @@ "Balance ID": "ID do Saldo", "Before we start with migration, please read": "Antes de começarmos com a migration, por favor leia", "Blockaid": "Blockaid", - "Blockaid Response Override": "Blockaid Response Override", + "Blockaid Response Override": "Substituição de Resposta do Blockaid", "Blockaid unfunded destination": "Esta é uma nova conta e precisa de 1 XLM para começar. Qualquer transação para enviar não-XLM para uma conta não financiada falhará.", "Blockaid unfunded destination native": "Esta é uma nova conta e precisa de pelo menos 1 XLM para ser criada. Enviar menos de 1 XLM para criá-la falhará.", "Bump To": "Bump Para", @@ -96,7 +96,7 @@ "Choose your method": "Escolha seu método", "Clear": "Limpar", "Clear Flags": "Limpar Flags", - "Clear Override": "Clear Override", + "Clear Override": "Limpar Substituição", "Clear recents": "Limpar recentes", "Close": "Fechar", "Coinbase Logo": "Logo Coinbase", @@ -151,7 +151,7 @@ "Dapps": "Dapps", "data entries": "entradas de dados", "Debug": "Depuração", - "Debug menu is only available in development mode.": "Debug menu is only available in development mode.", + "Debug menu is only available in development mode.": "O menu de depuração está disponível apenas no modo de desenvolvimento.", "Default": "Padrão", "Description": "Descrição", "Destination": "Destino", @@ -327,7 +327,7 @@ "Make sure you have your 12 word backup phrase": "Certifique-se de ter sua backup phrase de 12 palavras", "Make sure you have your current 12 words backup phrase before continuing.": "Certifique-se de ter sua frase de backup atual de 12 palavras antes de continuar.", "Make sure your Ledger wallet is connected to your computer and the Stellar app is open on the Ledger wallet.": "Certifique-se de que sua carteira Ledger está conectada ao seu computador e o aplicativo Stellar está aberto na carteira Ledger.", - "Malicious": "Malicious", + "Malicious": "Malicioso", "Manage assets": "Gerenciar ativos", "Manage List": "Gerenciar Lista", "Manage tokens": "Gerenciar tokens", @@ -359,7 +359,7 @@ "must be at least": "deve ser pelo menos", "must be below": "deve estar abaixo de", "Muxed address not supported": "Endereço muxed não suportado", - "N/A": "N/A", + "N/A": "N/D", "Name": "Nome", "Network": "Rede", "Network fees": "Taxas de rede", @@ -401,8 +401,8 @@ "Operations": "Operações", "Optional": "Opcional", "Order is incorrect, try again": "A ordem está incorreta, tente novamente", - "Overridden response": "Overridden response", - "Override Blockaid security responses for testing different security states (DEV only)": "Override Blockaid security responses for testing different security states (DEV only)", + "Overridden response": "Resposta substituída", + "Override Blockaid security responses for testing different security states (DEV only)": "Substituir respostas de segurança do Blockaid para testar diferentes estados de segurança (apenas DEV)", "Overview": "Visão geral", "Parameters": "Parâmetros", "Passphrase": "Frase secreta", @@ -431,7 +431,7 @@ "Preferences": "Preferências", "Price": "Preço", "Privacy Policy": "Política de Privacidade", - "Proceed with caution": "Proceed with caution", + "Proceed with caution": "Prossiga com cautela", "Read before importing your key": "Leia antes de importar sua chave", "Ready to migrate": "Pronto para migrar", "Receive": "Receber", @@ -456,7 +456,7 @@ "Review swap": "Revisar troca", "Review transaction on device": "Revisar transação no dispositivo", "Running integration tests ...": "Executando testes de integração ...", - "Safe": "Safe", + "Safe": "Seguro", "Salt": "Salt", "Save": "Salvar", "Save failed!": "Falha ao salvar!", @@ -510,7 +510,7 @@ "Some destination accounts on the Stellar network require a memo to identify your payment.": "Algumas contas de destino na rede Stellar exigem um memo para identificar seu pagamento.", "Some features may be disabled at this time": "Alguns recursos podem estar desabilitados neste momento.", "Some of your assets may not appear, but they are still safe on the network!": "Alguns de seus ativos podem não aparecer, mas ainda estão seguros na rede!", - "Soroban is temporarily experiencing issues": "Soroban is temporarily experiencing issues", + "Soroban is temporarily experiencing issues": "O Soroban está temporariamente com problemas", "Soroban RPC is temporarily experiencing issues": "O Soroban RPC está temporariamente com problemas", "SOROBAN RPC URL": "URL RPC SOROBAN", "Source": "Origem", @@ -528,7 +528,7 @@ "Submitting": "Enviando", "Success": "Sucesso", "Success!": "Sucesso!", - "Suspicious": "Suspicious", + "Suspicious": "Suspeito", "Suspicious Request": "Solicitação Suspeita", "Swap": "Trocar", "Swap destination": "Destino da troca", @@ -562,7 +562,7 @@ "These assets are not on any of your lists. Proceed with caution before adding.": "Esses ativos não estão em nenhuma de suas listas. Prossiga com cautela antes de adicionar.", "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.": "Estes serviços são operados por terceiros independentes, não pela Freighter ou SDF. A inclusão aqui não constitui um endosso. DeFi envolve riscos, incluindo perda de fundos. Use por sua conta e risco.", "These words are your wallet’s keys—store them securely to keep your funds safe.": "Essas palavras são as chaves da sua carteira—guarde-as com segurança para manter seus fundos seguros.", - "This asset does not appear safe for the following reasons.": "This asset does not appear safe for the following reasons.", + "This asset does not appear safe for the following reasons.": "Este ativo não parece seguro pelos seguintes motivos.", "This asset has a balance": "Este ativo tem um saldo", "This asset has been flagged as malicious for the following reasons.": "Este ativo foi marcado como malicioso pelos seguintes motivos.", "This asset has been flagged as spam for the following reasons.": "Este ativo foi marcado como spam pelos seguintes motivos.", @@ -571,16 +571,16 @@ "This asset is not on your lists": "Este ativo não está em suas listas", "This asset is on your lists": "Este ativo está em suas listas", "This asset was flagged as malicious": "Este ativo foi marcado como malicioso", - "This asset was flagged as malicious (override active)": "This asset was flagged as malicious (override active)", + "This asset was flagged as malicious (override active)": "Este ativo foi sinalizado como malicioso (substituição ativa)", "This asset was flagged as spam": "Este ativo foi marcado como spam", "This asset was flagged as suspicious": "Este ativo foi marcado como suspeito", - "This asset was flagged as suspicious (override active)": "This asset was flagged as suspicious (override active)", + "This asset was flagged as suspicious (override active)": "Este ativo foi sinalizado como suspeito (substituição ativa)", "This can be used to sign arbitrary transaction hashes without having to decode them first.": "Isso pode ser usado para assinar hashes de transação arbitrários sem precisar decodificá-los primeiro.", "This collectible is hidden": "Este colecionável está oculto", "This is not a valid contract id.": "Este não é um ID de contrato válido.", "This setting enables access to the Futurenet network and disables access to Pubnet.": "Esta configuração permite acesso à rede Futurenet e desabilita o acesso ao Pubnet.", - "This site does not appear safe for the following reasons": "This site does not appear safe for the following reasons", - "This site has been flagged with potential concerns": "This site has been flagged with potential concerns", + "This site does not appear safe for the following reasons": "Este site não parece seguro pelos seguintes motivos", + "This site has been flagged with potential concerns": "Este site foi sinalizado com possíveis preocupações", "This site was flagged as malicious": "Este site foi marcado como malicioso", "This token does not support muxed address (M-) as a target destination.": "Este token não suporta endereço muxed (M-) como destino.", "This transaction could not be completed.": "Esta transação não pôde ser concluída.", @@ -589,11 +589,11 @@ "This transaction is expected to fail": "Esta transação deve falhar", "This transaction is expected to fail for the following reasons.": "Esta transação deve falhar pelos seguintes motivos.", "This transaction was flagged as malicious": "Esta transação foi marcada como maliciosa", - "This transaction was flagged as malicious (override active)": "This transaction was flagged as malicious (override active)", + "This transaction was flagged as malicious (override active)": "Esta transação foi sinalizada como maliciosa (substituição ativa)", "This transaction was flagged as suspicious": "Esta transação foi marcada como suspeita", - "This transaction was flagged as suspicious (override active)": "This transaction was flagged as suspicious (override active)", + "This transaction was flagged as suspicious (override active)": "Esta transação foi sinalizada como suspeita (substituição ativa)", "This will be used to unlock your wallet": "Isso será usado para desbloquear sua carteira", - "Timeout": "Timeout", + "Timeout": "Tempo esgotado", "Timeout (seconds)": "Timeout (segundos)", "to": "para", "To access your wallet, click Freighter from your browser Extensions browser menu.": "Para acessar sua carteira, clique em Freighter no menu de Extensões do seu navegador.", @@ -629,12 +629,13 @@ "Unable to load network details": "Não foi possível carregar os detalhes da rede", "Unable to migrate": "Não foi possível migrar", "Unable to parse assets lists": "Não foi possível fazer parse das asset lists", - "Unable to Scan": "Unable to Scan", - "Unable to scan asset": "Unable to scan asset", - "Unable to scan site": "Unable to scan site", + "Unable to Scan": "Não foi possível verificar", + "Unable to scan asset": "Não foi possível verificar o ativo", + "Unable to scan destination token": "Não foi possível verificar o token de destino", + "Unable to scan site": "Não foi possível verificar o site", "Unable to scan site for malicious behavior": "Não foi possível escanear o site para comportamento malicioso", - "Unable to scan token": "Unable to scan token", - "Unable to scan transaction": "Unable to scan transaction", + "Unable to scan token": "Não foi possível verificar o token", + "Unable to scan transaction": "Não foi possível verificar a transação", "Unable to sign out": "Não foi possível sair", "Unexpected Error": "Erro Inesperado", "Unknown error occured": "Erro desconhecido ocorreu", @@ -655,6 +656,7 @@ "View": "Ver", "View maintenance details": "Ver detalhes da manutenção", "View on": "Ver em", + "View on stellar": "Ver na Stellar", "View on stellar.expert": "Ver em stellar.expert", "View options": "Ver opções", "View transaction": "Ver transação", @@ -668,9 +670,10 @@ "was swapped to": "foi trocado para", "wasm": "wasm", "Wasm Hash": "Hash Wasm", - "We were unable to scan this site for security issues": "We were unable to scan this site for security issues", - "We were unable to scan this token for security threats": "We were unable to scan this token for security threats", - "We were unable to scan this transaction for security issues": "We were unable to scan this transaction for security issues", + "We were unable to scan this site for security issues": "Não foi possível verificar este site quanto a problemas de segurança", + "We were unable to scan this token for security threats": "Não foi possível verificar este token quanto a ameaças de segurança", + "We were unable to scan this transaction for security issues": "Não foi possível verificar esta transação quanto a problemas de segurança", + "We were unable to scan this transaction for security threats": "Não foi possível verificar esta transação quanto a ameaças de segurança", "WEBSITE CONNECTION IS NOT SECURE": "CONEXÃO DO WEBSITE NÃO É SEGURA", "Welcome back": "Bem-vindo de volta", "Welcome to Discover!": "Bem-vindo ao Discover!", From 6f2f8cde834aea22947b28535a4a1409d1d64c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 10:45:12 -0700 Subject: [PATCH 29/44] remove unused keys --- extension/src/popup/locales/pt/translation.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index c2f80e817f..489d5705dd 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -339,8 +339,6 @@ "Medium": "Média", "Medium Threshold": "Limite Médio", "Memo": "Memo", - "Memo is disabled for this transaction": "Memo está desabilitado para esta transação", - "Memo is not supported for this operation": "Memo não é suportado para esta operação", "Memo is required": "Memo é obrigatório", "Memo is too long. Maximum {{max}} bytes allowed": "O memo é muito longo. Máximo de {{max}} bytes permitidos", "Memo required": "Memo obrigatório", From 1df31a379dddd51e177a4fa833158b946012490a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 10:48:47 -0700 Subject: [PATCH 30/44] safe access empty array --- .../src/popup/views/Discover/components/ProtocolRow/index.tsx | 2 +- .../popup/views/Discover/components/TrendingCarousel/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx b/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx index d45d33d63c..61053866c9 100644 --- a/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx +++ b/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx @@ -38,7 +38,7 @@ export const ProtocolRow = ({
- {protocol.tags[0]} + {protocol.tags[0] ?? ""}
diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx index 57faea459a..cccd6fecc5 100644 --- a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx @@ -50,7 +50,7 @@ export const TrendingCarousel = ({
- {protocol.tags[0]} + {protocol.tags[0] ?? ""}
From a1f53b33abe25b34ba4fed84809f7805bb947c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 12:07:40 -0700 Subject: [PATCH 31/44] allow dismissing popover menu by tapping on the outside --- .../components/ExpandedRecent/index.tsx | 58 +++++++++++-------- .../components/ExpandedRecent/styles.scss | 9 +-- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx index 6696c17836..86db8727c0 100644 --- a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx @@ -5,6 +5,11 @@ import { useTranslation } from "react-i18next"; import { DiscoverData } from "@shared/api/types"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { View } from "popup/basics/layout/View"; +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "popup/basics/shadcn/Popover"; import { ProtocolRow } from "../ProtocolRow"; import "./styles.scss"; @@ -27,7 +32,7 @@ export const ExpandedRecent = ({ onClearRecent, }: ExpandedRecentProps) => { const { t } = useTranslation(); - const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); return ( @@ -35,32 +40,35 @@ export const ExpandedRecent = ({ title={t("Recent")} customBackAction={onBack} rightButton={ -
- + + - - - {isMenuOpen && ( -
-
{ - setIsMenuOpen(false); - onClearRecent(); - }} - data-testid="clear-recents-button" - > - - - {t("Clear recents")} - -
+
{ + setIsPopoverOpen(false); + onClearRecent(); + }} + data-testid="clear-recents-button" + > + + + {t("Clear recents")} +
- )} -
+
+ } /> diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss index 7eaafbe3fc..a34d88ab99 100644 --- a/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/styles.scss @@ -7,10 +7,6 @@ gap: pxToRem(24); } - &__menu { - position: relative; - } - &__menu-trigger { background: transparent; border: none; @@ -25,16 +21,13 @@ } &__dropdown { - position: absolute; - right: 0; - top: pxToRem(32); width: pxToRem(180); background: var(--sds-clr-gray-01); border: 1px solid var(--sds-clr-gray-06); border-radius: pxToRem(6); box-shadow: 0 pxToRem(10) pxToRem(20) pxToRem(-10) rgba(0, 0, 0, 0.1); padding: pxToRem(4); - z-index: 10; + z-index: 100; } &__dropdown-item { From 670d0c20d9e690f9652f329279adb1a3ce8f3087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 14:05:52 -0700 Subject: [PATCH 32/44] cleaner ProtocolEntry interface importing/exporting --- @shared/api/types/types.ts | 6 ++++-- .../Discover/components/DiscoverHome/index.tsx | 11 ++++------- .../Discover/components/DiscoverSection/index.tsx | 9 +++------ .../Discover/components/ExpandedDapps/index.tsx | 9 +++------ .../Discover/components/ExpandedRecent/index.tsx | 9 +++------ .../components/ProtocolDetailsPanel/index.tsx | 8 +++----- .../Discover/components/ProtocolRow/index.tsx | 10 ++++------ .../components/TrendingCarousel/index.tsx | 6 ++---- extension/src/popup/views/Discover/index.tsx | 15 ++++++--------- 9 files changed, 32 insertions(+), 51 deletions(-) diff --git a/@shared/api/types/types.ts b/@shared/api/types/types.ts index 8b73652eef..3ec7ad1504 100644 --- a/@shared/api/types/types.ts +++ b/@shared/api/types/types.ts @@ -396,7 +396,7 @@ export interface ApiTokenPrices { [key: string]: ApiTokenPrice | null; } -export type DiscoverData = { +export interface ProtocolEntry { description: string; iconUrl: string; name: string; @@ -405,7 +405,9 @@ export type DiscoverData = { isBlacklisted: boolean; backgroundUrl?: string; isTrending: boolean; -}[]; +} + +export type DiscoverData = ProtocolEntry[]; export interface LedgerKeyAccount { account_id: string; diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx index ba15484479..135169b5b9 100644 --- a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx @@ -2,16 +2,13 @@ import React from "react"; import { Icon } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; -import { DiscoverData } from "@shared/api/types"; +import { DiscoverData, ProtocolEntry } from "@shared/api/types"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { View } from "popup/basics/layout/View"; import { TrendingCarousel } from "../TrendingCarousel"; import { DiscoverSection } from "../DiscoverSection"; - import "./styles.scss"; -type Protocol = DiscoverData[number]; - interface DiscoverHomeProps { trendingItems: DiscoverData; recentItems: DiscoverData; @@ -19,9 +16,9 @@ interface DiscoverHomeProps { onClose: () => void; onExpandRecent: () => void; onExpandDapps: () => void; - onCardClick: (protocol: Protocol) => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; + onCardClick: (protocol: ProtocolEntry) => void; + onRowClick: (protocol: ProtocolEntry) => void; + onOpenClick: (protocol: ProtocolEntry) => void; } export const DiscoverHome = ({ diff --git a/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx b/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx index 8192d487ed..92cf173028 100644 --- a/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverSection/index.tsx @@ -1,21 +1,18 @@ import React from "react"; import { Icon, Text } from "@stellar/design-system"; -import { DiscoverData } from "@shared/api/types"; +import { DiscoverData, ProtocolEntry } from "@shared/api/types"; import { ProtocolRow } from "../ProtocolRow"; - import "./styles.scss"; -type Protocol = DiscoverData[number]; - const MAX_VISIBLE = 5; interface DiscoverSectionProps { title: string; items: DiscoverData; onExpand: () => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; + onRowClick: (protocol: ProtocolEntry) => void; + onOpenClick: (protocol: ProtocolEntry) => void; } export const DiscoverSection = ({ diff --git a/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx b/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx index e987f1632b..3b34cf0641 100644 --- a/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx +++ b/extension/src/popup/views/Discover/components/ExpandedDapps/index.tsx @@ -1,20 +1,17 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { DiscoverData } from "@shared/api/types"; +import { DiscoverData, ProtocolEntry } from "@shared/api/types"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { View } from "popup/basics/layout/View"; import { ProtocolRow } from "../ProtocolRow"; - import "./styles.scss"; -type Protocol = DiscoverData[number]; - interface ExpandedDappsProps { items: DiscoverData; onBack: () => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; + onRowClick: (protocol: ProtocolEntry) => void; + onOpenClick: (protocol: ProtocolEntry) => void; } export const ExpandedDapps = ({ diff --git a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx index 86db8727c0..f567a6692f 100644 --- a/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx +++ b/extension/src/popup/views/Discover/components/ExpandedRecent/index.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Icon, Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; -import { DiscoverData } from "@shared/api/types"; +import { DiscoverData, ProtocolEntry } from "@shared/api/types"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { View } from "popup/basics/layout/View"; import { @@ -11,16 +11,13 @@ import { PopoverContent, } from "popup/basics/shadcn/Popover"; import { ProtocolRow } from "../ProtocolRow"; - import "./styles.scss"; -type Protocol = DiscoverData[number]; - interface ExpandedRecentProps { items: DiscoverData; onBack: () => void; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; + onRowClick: (protocol: ProtocolEntry) => void; + onOpenClick: (protocol: ProtocolEntry) => void; onClearRecent: () => void; } diff --git a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx index 4e2098dda8..4dbb554999 100644 --- a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx +++ b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx @@ -2,15 +2,13 @@ import React from "react"; import { Icon, Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; -import { DiscoverData } from "@shared/api/types"; +import { ProtocolEntry } from "@shared/api/types"; import "./styles.scss"; -type Protocol = DiscoverData[number]; - interface ProtocolDetailsPanelProps { - protocol: Protocol; - onOpen: (protocol: Protocol) => void; + protocol: ProtocolEntry; + onOpen: (protocol: ProtocolEntry) => void; } const getHostname = (url: string): string => { diff --git a/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx b/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx index 61053866c9..8d08f6e9ce 100644 --- a/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx +++ b/extension/src/popup/views/Discover/components/ProtocolRow/index.tsx @@ -2,16 +2,14 @@ import React from "react"; import { Icon, Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; -import { DiscoverData } from "@shared/api/types"; +import { ProtocolEntry } from "@shared/api/types"; import "./styles.scss"; -type Protocol = DiscoverData[number]; - interface ProtocolRowProps { - protocol: Protocol; - onRowClick: (protocol: Protocol) => void; - onOpenClick: (protocol: Protocol) => void; + protocol: ProtocolEntry; + onRowClick: (protocol: ProtocolEntry) => void; + onOpenClick: (protocol: ProtocolEntry) => void; } export const ProtocolRow = ({ diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx index cccd6fecc5..4b12f2114c 100644 --- a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx @@ -2,15 +2,13 @@ import React from "react"; import { Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; -import { DiscoverData } from "@shared/api/types"; +import { DiscoverData, ProtocolEntry } from "@shared/api/types"; import "./styles.scss"; -type Protocol = DiscoverData[number]; - interface TrendingCarouselProps { items: DiscoverData; - onCardClick: (protocol: Protocol) => void; + onCardClick: (protocol: ProtocolEntry) => void; } export const TrendingCarousel = ({ diff --git a/extension/src/popup/views/Discover/index.tsx b/extension/src/popup/views/Discover/index.tsx index f6d555c3e6..90ab2c5dbc 100644 --- a/extension/src/popup/views/Discover/index.tsx +++ b/extension/src/popup/views/Discover/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from "react"; -import { DiscoverData } from "@shared/api/types"; +import { ProtocolEntry } from "@shared/api/types"; import { openTab } from "popup/helpers/navigate"; import { addRecentProtocol, @@ -16,10 +16,8 @@ import { ExpandedRecent } from "./components/ExpandedRecent"; import { ExpandedDapps } from "./components/ExpandedDapps"; import { ProtocolDetailsPanel } from "./components/ProtocolDetailsPanel"; import { DiscoverWelcomeModal } from "./components/DiscoverWelcomeModal"; - import "./styles.scss"; -type Protocol = DiscoverData[number]; type DiscoverView = "main" | "recent" | "dapps"; interface DiscoverProps { @@ -28,9 +26,8 @@ interface DiscoverProps { export const Discover = ({ onClose = () => {} }: DiscoverProps) => { const [activeView, setActiveView] = useState("main"); - const [selectedProtocol, setSelectedProtocol] = useState( - null, - ); + const [selectedProtocol, setSelectedProtocol] = + useState(null); const [isDetailsOpen, setIsDetailsOpen] = useState(false); const { isLoading, trendingItems, recentItems, dappsItems, refreshRecent } = @@ -38,7 +35,7 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { const { showWelcome, dismissWelcome } = useDiscoverWelcome(); const handleOpenProtocol = useCallback( - async (protocol: Protocol) => { + async (protocol: ProtocolEntry) => { await addRecentProtocol(protocol.websiteUrl); await refreshRecent(); openTab(protocol.websiteUrl); @@ -46,13 +43,13 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { [refreshRecent], ); - const handleRowClick = useCallback((protocol: Protocol) => { + const handleRowClick = useCallback((protocol: ProtocolEntry) => { setSelectedProtocol(protocol); setIsDetailsOpen(true); }, []); const handleDetailsOpen = useCallback( - async (protocol: Protocol) => { + async (protocol: ProtocolEntry) => { setIsDetailsOpen(false); // Wait for the SlideupModal close animation before clearing state setTimeout(async () => { From 865a685b3af86f013d74a5f5397a99b99d9e409c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 14:09:55 -0700 Subject: [PATCH 33/44] move style between files --- extension/src/popup/views/Account/index.tsx | 2 +- extension/src/popup/views/Account/styles.scss | 6 ++++++ extension/src/popup/views/Discover/styles.scss | 6 ------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index 0eb8a397e4..a6a8208184 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -315,7 +315,7 @@ export const Account = () => { onOpenAutoFocus={(e) => e.preventDefault()} aria-describedby={undefined} side="bottom" - className="Discover__sheet-wrapper" + className="AccountView__discover-sheet" > {t("Discover")} diff --git a/extension/src/popup/views/Account/styles.scss b/extension/src/popup/views/Account/styles.scss index fdfad87b25..778f80fbef 100644 --- a/extension/src/popup/views/Account/styles.scss +++ b/extension/src/popup/views/Account/styles.scss @@ -91,4 +91,10 @@ padding-top: var(--View-inset-padding-top); padding-bottom: 1.5rem; } + + &__discover-sheet { + background: var(--sds-clr-gray-01); + height: 100%; + width: 100%; + } } diff --git a/extension/src/popup/views/Discover/styles.scss b/extension/src/popup/views/Discover/styles.scss index 72d49025fd..1d57e090f2 100644 --- a/extension/src/popup/views/Discover/styles.scss +++ b/extension/src/popup/views/Discover/styles.scss @@ -3,10 +3,4 @@ display: flex; flex-direction: column; background: var(--sds-clr-gray-01); - - &__sheet-wrapper { - background: var(--sds-clr-gray-01); - height: 100%; - width: 100%; - } } From 44981ce59c46151eb32ed6e3cfb7c0e537d7068e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 14:12:09 -0700 Subject: [PATCH 34/44] remove unused output --- extension/src/popup/views/Discover/hooks/useDiscoverData.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extension/src/popup/views/Discover/hooks/useDiscoverData.ts b/extension/src/popup/views/Discover/hooks/useDiscoverData.ts index 9db30a6f25..4c51af3181 100644 --- a/extension/src/popup/views/Discover/hooks/useDiscoverData.ts +++ b/extension/src/popup/views/Discover/hooks/useDiscoverData.ts @@ -104,7 +104,6 @@ export const useDiscoverData = () => { trendingItems, recentItems, dappsItems, - allProtocols: allowedProtocols, refreshRecent, }; }; From e7b6dae23c510ee837c1b3d7a30dc825414dd1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 14:47:01 -0700 Subject: [PATCH 35/44] display fallback image when carousel card image fails to load --- @shared/api/internal.ts | 3 +- .../components/TrendingCarousel/index.tsx | 73 +++++++++++++------ .../components/TrendingCarousel/styles.scss | 24 +++++- .../popup/views/__tests__/Discover.test.tsx | 3 +- 4 files changed, 75 insertions(+), 28 deletions(-) diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 9fc4daf98d..4baecdb1e7 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -58,6 +58,7 @@ import { HorizonOperation, UserNotification, CollectibleContract, + DiscoverData, } from "./types"; import { AccountBalancesInterface, @@ -621,7 +622,7 @@ export const getTokenPrices = async (tokens: string[]) => { return parsedResponse.data; }; -export const getDiscoverData = async () => { +export const getDiscoverData = async (): Promise => { const url = new URL(`${INDEXER_V2_URL}/protocols`); const response = await fetch(url.href); const parsedResponse = (await response.json()) as { diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx index 4b12f2114c..7d530d4d29 100644 --- a/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/index.tsx @@ -1,11 +1,54 @@ -import React from "react"; -import { Text } from "@stellar/design-system"; +import React, { useState } from "react"; +import { Icon, Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; import { DiscoverData, ProtocolEntry } from "@shared/api/types"; import "./styles.scss"; +const TrendingCard = ({ + protocol, + onCardClick, +}: { + protocol: ProtocolEntry; + onCardClick: (protocol: ProtocolEntry) => void; +}) => { + const [imgFailed, setImgFailed] = useState(false); + const showPlaceholder = !protocol.backgroundUrl || imgFailed; + + return ( +
onCardClick(protocol)} + > + {showPlaceholder ? ( +
+ +
+ ) : ( + {`${protocol.name} setImgFailed(true)} + /> + )} +
+
+ + {protocol.name} + +
+ + {protocol.tags[0] ?? ""} + +
+
+
+ ); +}; + interface TrendingCarouselProps { items: DiscoverData; onCardClick: (protocol: ProtocolEntry) => void; @@ -30,29 +73,11 @@ export const TrendingCarousel = ({
{items.map((protocol) => ( -
onCardClick(protocol)} - > -
-
- - {protocol.name} - -
- - {protocol.tags[0] ?? ""} - -
-
-
+ protocol={protocol} + onCardClick={onCardClick} + /> ))}
diff --git a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss index f0fc11a609..9fed55568f 100644 --- a/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss +++ b/extension/src/popup/views/Discover/components/TrendingCarousel/styles.scss @@ -27,13 +27,33 @@ height: pxToRem(162); border-radius: pxToRem(12); background-color: var(--sds-clr-gray-03); - background-size: cover; - background-position: center; position: relative; overflow: hidden; cursor: pointer; flex-shrink: 0; + &__bg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + } + + &__placeholder { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--sds-clr-gray-11); + + svg { + width: pxToRem(40); + height: pxToRem(40); + } + } + &__gradient { position: absolute; inset: 0; diff --git a/extension/src/popup/views/__tests__/Discover.test.tsx b/extension/src/popup/views/__tests__/Discover.test.tsx index 089d6172b7..a39ff63fe7 100644 --- a/extension/src/popup/views/__tests__/Discover.test.tsx +++ b/extension/src/popup/views/__tests__/Discover.test.tsx @@ -5,6 +5,7 @@ import { MAINNET_NETWORK_DETAILS, } from "@shared/constants/stellar"; import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState"; +import { DiscoverData } from "@shared/api/types"; import * as ApiInternal from "@shared/api/internal"; import * as RecentProtocols from "popup/helpers/recentProtocols"; import { Wrapper, mockAccounts } from "../../__testHelpers__"; @@ -24,7 +25,7 @@ jest.mock("webextension-polyfill", () => ({ }, })); -const mockProtocols = [ +const mockProtocols: DiscoverData = [ { description: "A lending protocol", name: "Blend", From 06df44ac683fa97acd86d0aebfc70ed1e5919ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 15:11:37 -0700 Subject: [PATCH 36/44] polish "Welcome to Discover!" modal --- .../Discover/components/DiscoverWelcomeModal/index.tsx | 5 +++-- .../Discover/components/DiscoverWelcomeModal/styles.scss | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx index a42ad5cc2f..196c4c48c8 100644 --- a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx @@ -40,9 +40,10 @@ export const DiscoverWelcomeModal = ({
{dappsItems.length > 0 && (
-
+ {t( "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.", )} -
+
)} diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss b/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss index 2b1da6851d..fcef226cce 100644 --- a/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss +++ b/extension/src/popup/views/Discover/components/DiscoverHome/styles.scss @@ -18,8 +18,6 @@ &__copy { color: var(--sds-clr-gray-11); - font-size: pxToRem(12); - line-height: pxToRem(14); } } } From c388362d6116ae9ba41b9b39f1f584ce352b30cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Wed, 1 Apr 2026 17:05:50 -0700 Subject: [PATCH 39/44] smaller button --- .../views/Discover/components/DiscoverWelcomeModal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx index 196c4c48c8..77d10dd4fb 100644 --- a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx @@ -40,7 +40,7 @@ export const DiscoverWelcomeModal = ({
+
+ ); +}; diff --git a/extension/src/popup/views/Discover/components/DiscoverError/styles.scss b/extension/src/popup/views/Discover/components/DiscoverError/styles.scss new file mode 100644 index 0000000000..990f0431d1 --- /dev/null +++ b/extension/src/popup/views/Discover/components/DiscoverError/styles.scss @@ -0,0 +1,41 @@ +@use "../../../../styles/utils.scss" as *; + +.DiscoverError { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: pxToRem(16); + padding: pxToRem(16); + border-radius: pxToRem(24); + background-color: var(--sds-clr-gray-03); + text-align: center; + + &__icon { + width: pxToRem(24); + height: pxToRem(24); + border-radius: pxToRem(6); + background: var(--sds-clr-amber-03); + border: 1px solid var(--sds-clr-amber-06); + display: flex; + align-items: center; + justify-content: center; + color: var(--sds-clr-amber-11); + + svg { + width: pxToRem(16); + height: pxToRem(16); + } + } + + &__text { + display: flex; + flex-direction: column; + gap: pxToRem(8); + width: 100%; + + &__subtitle { + color: var(--sds-clr-gray-11); + } + } +} diff --git a/extension/src/popup/views/Discover/hooks/useDiscoverData.ts b/extension/src/popup/views/Discover/hooks/useDiscoverData.ts index 4c51af3181..fcff0b3a38 100644 --- a/extension/src/popup/views/Discover/hooks/useDiscoverData.ts +++ b/extension/src/popup/views/Discover/hooks/useDiscoverData.ts @@ -105,5 +105,6 @@ export const useDiscoverData = () => { recentItems, dappsItems, refreshRecent, + retry: fetchData, }; }; diff --git a/extension/src/popup/views/Discover/index.tsx b/extension/src/popup/views/Discover/index.tsx index 90ab2c5dbc..31dc847456 100644 --- a/extension/src/popup/views/Discover/index.tsx +++ b/extension/src/popup/views/Discover/index.tsx @@ -1,6 +1,10 @@ import React, { useCallback, useState } from "react"; +import { Icon } from "@stellar/design-system"; +import { useTranslation } from "react-i18next"; import { ProtocolEntry } from "@shared/api/types"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; import { openTab } from "popup/helpers/navigate"; import { addRecentProtocol, @@ -16,6 +20,7 @@ import { ExpandedRecent } from "./components/ExpandedRecent"; import { ExpandedDapps } from "./components/ExpandedDapps"; import { ProtocolDetailsPanel } from "./components/ProtocolDetailsPanel"; import { DiscoverWelcomeModal } from "./components/DiscoverWelcomeModal"; +import { DiscoverError } from "./components/DiscoverError"; import "./styles.scss"; type DiscoverView = "main" | "recent" | "dapps"; @@ -25,13 +30,21 @@ interface DiscoverProps { } export const Discover = ({ onClose = () => {} }: DiscoverProps) => { + const { t } = useTranslation(); const [activeView, setActiveView] = useState("main"); const [selectedProtocol, setSelectedProtocol] = useState(null); const [isDetailsOpen, setIsDetailsOpen] = useState(false); - const { isLoading, trendingItems, recentItems, dappsItems, refreshRecent } = - useDiscoverData(); + const { + isLoading, + error, + trendingItems, + recentItems, + dappsItems, + refreshRecent, + retry, + } = useDiscoverData(); const { showWelcome, dismissWelcome } = useDiscoverWelcome(); const handleOpenProtocol = useCallback( @@ -74,6 +87,23 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { ); } + if (error) { + return ( +
+ + } + customBackAction={onClose} + /> + + + + +
+ ); + } + return (
{activeView === "main" && ( diff --git a/extension/src/popup/views/__tests__/Discover.test.tsx b/extension/src/popup/views/__tests__/Discover.test.tsx index eae50b469b..cf8d918b0c 100644 --- a/extension/src/popup/views/__tests__/Discover.test.tsx +++ b/extension/src/popup/views/__tests__/Discover.test.tsx @@ -172,6 +172,43 @@ describe("Discover", () => { }); }); + describe("error state", () => { + it("shows error screen with retry when API fails", async () => { + jest + .spyOn(ApiInternal, "getDiscoverData") + .mockRejectedValue(new Error("Network error")); + + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-error")).toBeInTheDocument(); + }); + + expect(screen.getByText("Unable to fetch protocols")).toBeInTheDocument(); + }); + + it("retries fetching data when Refresh is clicked", async () => { + const getDiscoverSpy = jest + .spyOn(ApiInternal, "getDiscoverData") + .mockRejectedValueOnce(new Error("Network error")) + .mockResolvedValueOnce(mockProtocols); + + renderDiscover(); + + await waitFor(() => { + expect(screen.getByTestId("discover-error")).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByTestId("discover-error-retry")); + + await waitFor(() => { + expect(screen.getByTestId("trending-carousel")).toBeInTheDocument(); + }); + + expect(getDiscoverSpy).toHaveBeenCalledTimes(2); + }); + }); + describe("open protocol", () => { it("saves to recents before opening a new tab", async () => { renderDiscover(); From 1fc3b41e403ccaf32377a905c56e21ed253a34dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Thu, 2 Apr 2026 16:49:46 -0700 Subject: [PATCH 43/44] pt translations --- extension/src/popup/locales/pt/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 14d6fbef98..0a0a662072 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -557,7 +557,7 @@ "The transaction you’re trying to sign is on": "A transação que você está tentando assinar está em", "The website <1>{url} does not use an SSL certificate.": "O site <1>{url} não usa um certificado SSL.", "There are no sites to display at this moment.": "Não há sites para exibir no momento.", - "There was an error fetching protocols. Please refresh and try again.": "There was an error fetching protocols. Please refresh and try again.", + "There was an error fetching protocols. Please refresh and try again.": "Ocorreu um erro ao buscar os protocolos. Atualize e tente novamente.", "These assets are not on any of your lists. Proceed with caution before adding.": "Esses ativos não estão em nenhuma de suas listas. Prossiga com cautela antes de adicionar.", "These services are operated by independent third parties, not by Freighter or SDF. Inclusion here is not an endorsement. DeFi carries risk, including loss of funds. Use at your own risk.": "Estes serviços são operados por terceiros independentes, não pela Freighter ou SDF. A inclusão aqui não constitui um endosso. DeFi envolve riscos, incluindo perda de fundos. Use por sua conta e risco.", "These words are your wallet’s keys—store them securely to keep your funds safe.": "Essas palavras são as chaves da sua carteira—guarde-as com segurança para manter seus fundos seguros.", @@ -624,7 +624,7 @@ "Type": "Tipo", "Type your memo": "Digite seu memo", "Unable to connect to": "Não foi possível conectar a", - "Unable to fetch protocols": "Unable to fetch protocols", + "Unable to fetch protocols": "Não foi possível buscar os protocolos", "Unable to find your asset.": "Não foi possível encontrar seu ativo.", "Unable to load network details": "Não foi possível carregar os detalhes da rede", "Unable to migrate": "Não foi possível migrar", From 5717f5d6c6ebcce109ea8e2a3a24b263327876b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=CC=81ssio=20Marcos=20Goulart?= Date: Thu, 2 Apr 2026 18:41:00 -0700 Subject: [PATCH 44/44] add metrics --- extension/src/popup/constants/metricsNames.ts | 6 ++ extension/src/popup/metrics/discover.ts | 72 ++++++++++++++++++ .../components/DiscoverHome/index.tsx | 20 +++-- .../components/DiscoverWelcomeModal/index.tsx | 7 +- .../components/ProtocolDetailsPanel/index.tsx | 7 +- extension/src/popup/views/Discover/index.tsx | 73 +++++++++++++++---- 6 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 extension/src/popup/metrics/discover.ts diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index af0a195e01..d888e1200a 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -89,6 +89,12 @@ export const METRIC_NAMES = { viewNetworkSettings: "loaded screen: network settings", viewAddFunds: "loaded screen: add fund", + viewDiscover: "loaded screen: discover", + discoverProtocolOpened: "discover: protocol opened", + discoverProtocolDetailsViewed: "discover: protocol details viewed", + discoverProtocolOpenedFromDetails: "discover: protocol opened from details", + discoverWelcomeModalViewed: "discover: welcome modal viewed", + manageAssetAddAsset: "manage asset: add asset", manageAssetAddToken: "manage asset: add token", manageAssetAddUnsafeAsset: "manage asset: add unsafe asset", diff --git a/extension/src/popup/metrics/discover.ts b/extension/src/popup/metrics/discover.ts new file mode 100644 index 0000000000..eff37e4a8d --- /dev/null +++ b/extension/src/popup/metrics/discover.ts @@ -0,0 +1,72 @@ +import { emitMetric } from "helpers/metrics"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; + +/** Strip query parameters and fragments from a URL to avoid leaking + * sensitive data (tokens, session IDs) to the analytics backend. + * This is not a big risk right now on extension as we only allow + * known protocols that come from our backend but we include this + * function for future-proofing and also to match mobile behavior. + */ +export const stripQueryParams = (url: string): string => { + try { + const parsed = new URL(url); + return `${parsed.protocol}//${parsed.host}${parsed.pathname}`; + } catch { + return url.split(/[?#]/)[0]; + } +}; + +export const DISCOVER_SOURCE = { + TRENDING_CAROUSEL: "trending_carousel", + RECENT_LIST: "recent_list", + DAPPS_LIST: "dapps_list", + EXPANDED_RECENT_LIST: "expanded_recent_list", + EXPANDED_DAPPS_LIST: "expanded_dapps_list", +} as const; + +export type DiscoverSource = + (typeof DISCOVER_SOURCE)[keyof typeof DISCOVER_SOURCE]; + +export const trackDiscoverProtocolOpened = ( + protocolName: string, + url: string, + source: DiscoverSource, +): void => { + emitMetric(METRIC_NAMES.discoverProtocolOpened, { + url: stripQueryParams(url), + protocolName, + source, + // We currently only allow known protocols in the Discover view for extension, + // but this field is included for future-proofing in case we later expand to + // allowing unknown protocols (e.g. from a search bar like we have on mobile). + isKnownProtocol: true, + }); +}; + +export const trackDiscoverProtocolDetailsViewed = ( + protocolName: string, + tags: string[], +): void => { + emitMetric(METRIC_NAMES.discoverProtocolDetailsViewed, { + protocolName, + tags, + }); +}; + +export const trackDiscoverProtocolOpenedFromDetails = ( + protocolName: string, + url: string, +): void => { + emitMetric(METRIC_NAMES.discoverProtocolOpenedFromDetails, { + protocolName, + url: stripQueryParams(url), + }); +}; + +export const trackDiscoverViewed = (): void => { + emitMetric(METRIC_NAMES.viewDiscover); +}; + +export const trackDiscoverWelcomeModalViewed = (): void => { + emitMetric(METRIC_NAMES.discoverWelcomeModalViewed); +}; diff --git a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx index f040673739..e74ac9491a 100644 --- a/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverHome/index.tsx @@ -17,8 +17,10 @@ interface DiscoverHomeProps { onExpandRecent: () => void; onExpandDapps: () => void; onCardClick: (protocol: ProtocolEntry) => void; - onRowClick: (protocol: ProtocolEntry) => void; - onOpenClick: (protocol: ProtocolEntry) => void; + onRecentRowClick: (protocol: ProtocolEntry) => void; + onDappsRowClick: (protocol: ProtocolEntry) => void; + onOpenRecentClick: (protocol: ProtocolEntry) => void; + onOpenDappsClick: (protocol: ProtocolEntry) => void; } export const DiscoverHome = ({ @@ -29,8 +31,10 @@ export const DiscoverHome = ({ onExpandRecent, onExpandDapps, onCardClick, - onRowClick, - onOpenClick, + onRecentRowClick, + onDappsRowClick, + onOpenRecentClick, + onOpenDappsClick, }: DiscoverHomeProps) => { const { t } = useTranslation(); @@ -48,15 +52,15 @@ export const DiscoverHome = ({ title={t("Recent")} items={recentItems} onExpand={onExpandRecent} - onRowClick={onRowClick} - onOpenClick={onOpenClick} + onRowClick={onRecentRowClick} + onOpenClick={onOpenRecentClick} />
{dappsItems.length > 0 && ( diff --git a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx index 77d10dd4fb..d86b504601 100644 --- a/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx +++ b/extension/src/popup/views/Discover/components/DiscoverWelcomeModal/index.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useEffect } from "react"; import { Button, Icon, Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; +import { trackDiscoverWelcomeModalViewed } from "popup/metrics/discover"; import { LoadingBackground } from "popup/basics/LoadingBackground"; import "./styles.scss"; @@ -15,6 +16,10 @@ export const DiscoverWelcomeModal = ({ }: DiscoverWelcomeModalProps) => { const { t } = useTranslation(); + useEffect(() => { + trackDiscoverWelcomeModalViewed(); + }, []); + return ( <>
diff --git a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx index 4dbb554999..4aa922ea9e 100644 --- a/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx +++ b/extension/src/popup/views/Discover/components/ProtocolDetailsPanel/index.tsx @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useEffect } from "react"; import { Icon, Text } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; import { ProtocolEntry } from "@shared/api/types"; +import { trackDiscoverProtocolDetailsViewed } from "popup/metrics/discover"; import "./styles.scss"; @@ -25,6 +26,10 @@ export const ProtocolDetailsPanel = ({ }: ProtocolDetailsPanelProps) => { const { t } = useTranslation(); + useEffect(() => { + trackDiscoverProtocolDetailsViewed(protocol.name, protocol.tags); + }, [protocol]); + return (
diff --git a/extension/src/popup/views/Discover/index.tsx b/extension/src/popup/views/Discover/index.tsx index 31dc847456..0c1a81887f 100644 --- a/extension/src/popup/views/Discover/index.tsx +++ b/extension/src/popup/views/Discover/index.tsx @@ -1,8 +1,15 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Icon } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; import { ProtocolEntry } from "@shared/api/types"; +import { + trackDiscoverViewed, + trackDiscoverProtocolOpened, + trackDiscoverProtocolOpenedFromDetails, + DiscoverSource, + DISCOVER_SOURCE, +} from "popup/metrics/discover"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { View } from "popup/basics/layout/View"; import { openTab } from "popup/helpers/navigate"; @@ -34,6 +41,9 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { const [activeView, setActiveView] = useState("main"); const [selectedProtocol, setSelectedProtocol] = useState(null); + const [selectedSource, setSelectedSource] = useState( + DISCOVER_SOURCE.DAPPS_LIST, + ); const [isDetailsOpen, setIsDetailsOpen] = useState(false); const { @@ -47,8 +57,13 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { } = useDiscoverData(); const { showWelcome, dismissWelcome } = useDiscoverWelcome(); + useEffect(() => { + trackDiscoverViewed(); + }, []); + const handleOpenProtocol = useCallback( - async (protocol: ProtocolEntry) => { + async (protocol: ProtocolEntry, source: DiscoverSource) => { + trackDiscoverProtocolOpened(protocol.name, protocol.websiteUrl, source); await addRecentProtocol(protocol.websiteUrl); await refreshRecent(); openTab(protocol.websiteUrl); @@ -56,21 +71,29 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { [refreshRecent], ); - const handleRowClick = useCallback((protocol: ProtocolEntry) => { - setSelectedProtocol(protocol); - setIsDetailsOpen(true); - }, []); + const handleRowClick = useCallback( + (protocol: ProtocolEntry, source: DiscoverSource) => { + setSelectedProtocol(protocol); + setSelectedSource(source); + setIsDetailsOpen(true); + }, + [], + ); const handleDetailsOpen = useCallback( async (protocol: ProtocolEntry) => { + trackDiscoverProtocolOpenedFromDetails( + protocol.name, + protocol.websiteUrl, + ); setIsDetailsOpen(false); // Wait for the SlideupModal close animation before clearing state setTimeout(async () => { setSelectedProtocol(null); - await handleOpenProtocol(protocol); + await handleOpenProtocol(protocol, selectedSource); }, 200); }, - [handleOpenProtocol], + [handleOpenProtocol, selectedSource], ); const handleClearRecent = useCallback(async () => { @@ -114,17 +137,33 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { onClose={onClose} onExpandRecent={() => setActiveView("recent")} onExpandDapps={() => setActiveView("dapps")} - onCardClick={handleRowClick} - onRowClick={handleRowClick} - onOpenClick={handleOpenProtocol} + onCardClick={(p: ProtocolEntry) => + handleRowClick(p, DISCOVER_SOURCE.TRENDING_CAROUSEL) + } + onRecentRowClick={(p: ProtocolEntry) => + handleRowClick(p, DISCOVER_SOURCE.RECENT_LIST) + } + onDappsRowClick={(p: ProtocolEntry) => + handleRowClick(p, DISCOVER_SOURCE.DAPPS_LIST) + } + onOpenRecentClick={(p: ProtocolEntry) => + handleOpenProtocol(p, DISCOVER_SOURCE.RECENT_LIST) + } + onOpenDappsClick={(p: ProtocolEntry) => + handleOpenProtocol(p, DISCOVER_SOURCE.DAPPS_LIST) + } /> )} {activeView === "recent" && ( setActiveView("main")} - onRowClick={handleRowClick} - onOpenClick={handleOpenProtocol} + onRowClick={(p: ProtocolEntry) => + handleRowClick(p, DISCOVER_SOURCE.EXPANDED_RECENT_LIST) + } + onOpenClick={(p) => + handleOpenProtocol(p, DISCOVER_SOURCE.EXPANDED_RECENT_LIST) + } onClearRecent={handleClearRecent} /> )} @@ -132,8 +171,12 @@ export const Discover = ({ onClose = () => {} }: DiscoverProps) => { setActiveView("main")} - onRowClick={handleRowClick} - onOpenClick={handleOpenProtocol} + onRowClick={(p: ProtocolEntry) => + handleRowClick(p, DISCOVER_SOURCE.EXPANDED_DAPPS_LIST) + } + onOpenClick={(p) => + handleOpenProtocol(p, DISCOVER_SOURCE.EXPANDED_DAPPS_LIST) + } /> )}