From a3ee98ff0c8ea3bc06bb3a9e613858643411c9dc Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 25 Jun 2024 19:31:32 +0545 Subject: [PATCH 01/31] feat: Auto import ao tokens to dashboard --- src/background.ts | 4 + src/routes/auth/signDataItem.tsx | 6 +- src/routes/popup/index.tsx | 4 + src/routes/popup/tokens.tsx | 25 ++-- src/tokens/aoTokens/ao.ts | 7 +- src/tokens/aoTokens/sync.ts | 197 ++++++++++++++++++++++++++++--- src/tokens/index.ts | 39 +++++- src/utils/notifications.ts | 8 +- 8 files changed, 255 insertions(+), 35 deletions(-) diff --git a/src/background.ts b/src/background.ts index 26be1b205..99db60f24 100644 --- a/src/background.ts +++ b/src/background.ts @@ -15,6 +15,7 @@ import browser from "webextension-polyfill"; import { syncLabels } from "~wallets"; import { trackBalance } from "~utils/analytics"; import { subscriptionsHandler } from "~subscriptions/api"; +import { importAoTokens } from "~tokens/aoTokens/sync"; // watch for API calls onMessage("api_call", handleApiCalls); @@ -46,6 +47,9 @@ browser.alarms.onAlarm.addListener(syncLabels); // handle decryption key removal alarm browser.alarms.onAlarm.addListener(keyRemoveAlarmListener); +// handle importing ao tokens +browser.alarms.onAlarm.addListener(importAoTokens); + // handle window close browser.windows.onRemoved.addListener(onWindowClose); diff --git a/src/routes/auth/signDataItem.tsx b/src/routes/auth/signDataItem.tsx index d0c9d42fc..8712e2f70 100644 --- a/src/routes/auth/signDataItem.tsx +++ b/src/routes/auth/signDataItem.tsx @@ -33,7 +33,7 @@ import prettyBytes from "pretty-bytes"; import { formatFiatBalance } from "~tokens/currency"; import useSetting from "~settings/hook"; import { getPrice } from "~lib/coingecko"; -import type { TokenInfo } from "~tokens/aoTokens/ao"; +import type { TokenInfo, TokenInfoWithProcessId } from "~tokens/aoTokens/ao"; import { ChevronUpIcon, ChevronDownIcon } from "@iconicicons/react"; import { getUserAvatar } from "~lib/avatar"; import { LogoWrapper, Logo } from "~components/popup/Token"; @@ -180,11 +180,11 @@ export default function SignDataItem() { console.log("err", err); try { const aoTokens = - (await ExtensionStorage.get<(TokenInfo & { processId: string })[]>( + (await ExtensionStorage.get( "ao_tokens" )) || []; const aoTokensCache = - (await ExtensionStorage.get<(TokenInfo & { processId: string })[]>( + (await ExtensionStorage.get( "ao_tokens_cache" )) || []; const aoTokensCombined = [...aoTokens, ...aoTokensCache]; diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx index 163642239..a02db10ef 100644 --- a/src/routes/popup/index.tsx +++ b/src/routes/popup/index.tsx @@ -15,6 +15,7 @@ import { useActiveWallet, useBalance } from "~wallets/hooks"; import BuyButton from "~components/popup/home/BuyButton"; import Tabs from "~components/popup/home/Tabs"; import AoBanner from "~components/popup/home/AoBanner"; +import { scheduleImportAoTokens } from "~tokens/aoTokens/sync"; export default function Home() { // get if the user has no balance @@ -93,6 +94,9 @@ export default function Home() { } }; checkExpiration(); + + // schedule import ao tokens + scheduleImportAoTokens(); }, []); useEffect(() => { diff --git a/src/routes/popup/tokens.tsx b/src/routes/popup/tokens.tsx index 4d7daae0c..b15e3ead1 100644 --- a/src/routes/popup/tokens.tsx +++ b/src/routes/popup/tokens.tsx @@ -1,7 +1,11 @@ import { useHistory } from "~utils/hash_router"; import { ButtonV2, Section, useToasts, Loading } from "@arconnect/components"; import { EditIcon } from "@iconicicons/react"; -import { getAoTokens, useTokens } from "~tokens"; +import { + getAoTokens, + getAoTokensAutoImportRestrictedIds, + useTokens +} from "~tokens"; import { useEffect, useMemo, useState } from "react"; import browser from "webextension-polyfill"; import Token from "~components/popup/Token"; @@ -54,7 +58,7 @@ export default function Tokens() { try { const aoTokens = await getAoTokens(); - if (aoTokens.find(({ processId }) => processId === token.id)) { + if (aoTokens.some(({ processId }) => processId === token.id)) { setToast({ type: "error", content: browser.i18n.getMessage("token_already_added"), @@ -85,7 +89,7 @@ export default function Tokens() { try { const aoTokens = await getAoTokens(); - if (!aoTokens.find(({ processId }) => processId === token.id)) { + if (!aoTokens.some(({ processId }) => processId === token.id)) { setToast({ type: "error", content: browser.i18n.getMessage("token_already_removed"), @@ -94,9 +98,17 @@ export default function Tokens() { throw new Error("Token already removed"); } + const restrictedTokenIds = await getAoTokensAutoImportRestrictedIds(); const updatedTokens = aoTokens.filter( ({ processId }) => processId !== token.id ); + if (!restrictedTokenIds.includes(token.id)) { + restrictedTokenIds.push(token.id); + await ExtensionStorage.set( + "ao_tokens_auto_import_restricted_ids", + restrictedTokenIds + ); + } await ExtensionStorage.set("ao_tokens", updatedTokens); setToast({ type: "success", @@ -112,11 +124,8 @@ export default function Tokens() { if (!aoSupport) return; try { setIsLoading(true); - for (let i = 0; i < 2; i++) { - const { hasNextPage, syncCount } = await syncAoTokens(); - setHasNextPage(!!hasNextPage); - if (!hasNextPage || (hasNextPage && syncCount > 0)) break; - } + const { hasNextPage } = await syncAoTokens(); + setHasNextPage(!!hasNextPage); } finally { setIsLoading(false); } diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index b52fadf5c..5484b68a1 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -216,7 +216,7 @@ export function useAoTokensCache(): [TokenInfoWithBalance[], boolean] { instance: ExtensionStorage }); - const [aoTokens] = useStorage<(TokenInfo & { processId: string })[]>( + const [aoTokens] = useStorage( { key: "ao_tokens", instance: ExtensionStorage @@ -224,7 +224,7 @@ export function useAoTokensCache(): [TokenInfoWithBalance[], boolean] { [] ); - const [aoTokensCache] = useStorage<(TokenInfo & { processId: string })[]>( + const [aoTokensCache] = useStorage( { key: "ao_tokens_cache", instance: ExtensionStorage }, [] ); @@ -388,6 +388,9 @@ export interface TokenInfo { Denomination: number; processId?: string; } + +export type TokenInfoWithProcessId = TokenInfo & { processId: string }; + export interface TokenInfoWithBalance extends TokenInfo { id: string; balance: string; diff --git a/src/tokens/aoTokens/sync.ts b/src/tokens/aoTokens/sync.ts index 93abac516..e32023f7c 100644 --- a/src/tokens/aoTokens/sync.ts +++ b/src/tokens/aoTokens/sync.ts @@ -1,5 +1,11 @@ -import { defaultGateway } from "~gateways/gateway"; import Arweave from "arweave"; +import type { Alarms } from "webextension-polyfill"; +import browser from "webextension-polyfill"; +import { + getAoTokens, + getAoTokensCache, + getAoTokensAutoImportRestrictedIds +} from "~tokens"; import type { GQLTransactionsResultInterface } from "ar-gql/dist/faces"; import { ExtensionStorage } from "~utils/storage"; import { getActiveAddress } from "~wallets"; @@ -11,8 +17,22 @@ import { } from "./ao"; /** Tokens storage name */ +const AO_TOKENS = "ao_tokens"; const AO_TOKENS_CACHE = "ao_tokens_cache"; const AO_TOKENS_IDS = "ao_tokens_ids"; +const AO_TOKENS_IMPORT_TIMESTAMP = "ao_tokens_import_timestamp"; +const AO_TOKENS_AUTO_IMPORT_RESTRICTED_IDS = + "ao_tokens_auto_import_restricted_ids"; + +/** Variables for sync */ +let isSyncInProgress = false; +let lastHasNextPage = true; + +const gateway = { + host: "arweave-search.goldsky.com", + port: 443, + protocol: "https" +}; /** * Generic retry function for any async operation. @@ -136,14 +156,15 @@ function getNoticeTransactionsQuery( async function getNoticeTransactions( arweave: Arweave, address: string, - filterProcesses: string[] = [] + filterProcesses: string[] = [], + fetchCountLimit = 5 ) { let fetchCount = 0; let hasNextPage = true; let ids = new Set(); // Fetch atmost 500 transactions - while (hasNextPage && fetchCount <= 5) { + while (hasNextPage && fetchCount <= fetchCountLimit) { try { const query = getNoticeTransactionsQuery(address, filterProcesses); const transactions = await withRetry(async () => { @@ -179,26 +200,43 @@ async function getNoticeTransactions( * Sync AO Tokens */ export async function syncAoTokens() { + if (isSyncInProgress) { + console.log("Already syncing AO tokens, please wait..."); + await new Promise((resolve) => { + const checkState = setInterval(() => { + if (!isSyncInProgress) { + clearInterval(checkState); + resolve(null); + } + }, 100); + }); + return { hasNextPage: lastHasNextPage, syncCount: 0 }; + } + + isSyncInProgress = true; + try { const activeAddress = await getActiveAddress(); - if (!activeAddress) return { hasNextPage: false, syncCount: 0 }; + if (!activeAddress) { + lastHasNextPage = false; + return { hasNextPage: false, syncCount: 0 }; + } const aoSupport = await ExtensionStorage.get("setting_ao_support"); - if (!aoSupport) return { hasNextPage: false, syncCount: 0 }; + if (!aoSupport) { + lastHasNextPage = false; + return { hasNextPage: false, syncCount: 0 }; + } console.log("Synchronizing AO tokens..."); - const aoTokensCache = - (await ExtensionStorage.get<(TokenInfo & { processId: string })[]>( - AO_TOKENS_CACHE - )) || []; - const aoTokensIds = - (await ExtensionStorage.get<{ [key: string]: string[] }>( - AO_TOKENS_IDS - )) || {}; + const aoTokensCache = await getAoTokensCache(); + let aoTokensIds = + (await ExtensionStorage.get>(AO_TOKENS_IDS)) || + {}; const walletTokenIds = aoTokensIds[activeAddress] || []; - const arweave = new Arweave(defaultGateway); + const arweave = new Arweave(gateway); const { processIds, hasNextPage } = await getNoticeTransactions( arweave, activeAddress, @@ -211,6 +249,7 @@ export async function syncAoTokens() { if (newProcessIds.length === 0) { console.log("No new ao tokens found!"); + lastHasNextPage = hasNextPage; return { hasNextPage, syncCount: 0 }; } @@ -231,11 +270,24 @@ export async function syncAoTokens() { .map((result) => result.value) .filter((token) => !!token.Ticker); + const tokensWithoutTicker = results + .filter((result) => result.status === "fulfilled") + .map((result) => result.value) + .filter( + (token) => !token.Ticker && !walletTokenIds.includes(token.processId) + ); + const updatedTokens = [...aoTokensCache, ...tokens]; const updatedProcessIds = newProcessIds.filter((processId) => updatedTokens.some((token) => token.processId === processId) ); + if (tokensWithoutTicker.length > 0) { + updatedProcessIds.push( + ...tokensWithoutTicker.map(({ processId }) => processId) + ); + } + walletTokenIds.push(...updatedProcessIds); aoTokensIds[activeAddress] = walletTokenIds; @@ -246,9 +298,126 @@ export async function syncAoTokens() { ]); console.log("Synchronized ao tokens!"); + lastHasNextPage = hasNextPage; return { hasNextPage, syncCount: tokens.length }; } catch (error: any) { console.log("Error syncing tokens: ", error?.message); + lastHasNextPage = false; return { hasNextPage: false, syncCount: 0 }; + } finally { + isSyncInProgress = false; + } +} + +/** + * Import AO Tokens + */ +export async function importAoTokens(alarm: Alarms.Alarm) { + if (alarm?.name !== "import_ao_tokens") return; + + try { + const activeAddress = await getActiveAddress(); + + console.log("Importing AO tokens..."); + + let [aoTokens, aoTokensCache, removedTokenIds = []] = await Promise.all([ + getAoTokens(), + getAoTokensCache(), + getAoTokensAutoImportRestrictedIds() + ]); + + let aoTokensIds = new Set(aoTokens.map(({ processId }) => processId)); + const aoTokensCacheIds = new Set( + aoTokensCache.map(({ processId }) => processId) + ); + let tokenIdstoExclude = new Set([...aoTokensIds, ...removedTokenIds]); + const walletTokenIds = new Set([...tokenIdstoExclude, ...aoTokensCacheIds]); + + const arweave = new Arweave(gateway); + const { processIds } = await getNoticeTransactions( + arweave, + activeAddress, + Array.from(walletTokenIds) + ); + + const newProcessIds = Array.from( + new Set([...processIds, ...aoTokensCacheIds]) + ).filter((processId) => !tokenIdstoExclude.has(processId)); + + if (newProcessIds.length === 0) { + console.log("No new ao tokens found!"); + return; + } + + const promises = newProcessIds + .filter((processId) => !aoTokensCacheIds.has(processId)) + .map((processId) => + withRetry(async () => { + const token = await timeoutPromise(getTokenInfo(processId), 3000); + return { ...token, processId }; + }, 2) + ); + const results = await Promise.allSettled(promises); + const tokens = results + .filter((result) => result.status === "fulfilled") + .map((result) => result.value) + .filter((token) => !!token.Ticker); + + const tokensToRestrict = results + .filter((result) => result.status === "fulfilled") + .map((result) => result.value) + .filter( + (token) => !token.Ticker && !removedTokenIds.includes(token.processId) + ); + + const updatedTokens = [...aoTokensCache, ...tokens]; + + aoTokens = await getAoTokens(); + aoTokensIds = new Set(aoTokens.map(({ processId }) => processId)); + tokenIdstoExclude = new Set([...aoTokensIds, ...removedTokenIds]); + + if (tokensToRestrict.length > 0) { + removedTokenIds.push( + ...tokensToRestrict.map(({ processId }) => processId) + ); + await ExtensionStorage.set( + AO_TOKENS_AUTO_IMPORT_RESTRICTED_IDS, + removedTokenIds + ); + } + + const newTokens = updatedTokens.filter( + (token) => !tokenIdstoExclude.has(token.processId) + ); + if (newTokens.length === 0) return; + + newTokens.forEach((token) => aoTokens.push(token)); + await ExtensionStorage.set(AO_TOKENS, aoTokens); + + console.log("Imported ao tokens!"); + } catch (error: any) { + console.log("Error importing tokens: ", error?.message); + } finally { + await ExtensionStorage.set(AO_TOKENS_IMPORT_TIMESTAMP, 0); + } +} + +export async function scheduleImportAoTokens() { + const timestamp = await ExtensionStorage.get( + AO_TOKENS_IMPORT_TIMESTAMP + ); + if (timestamp && Date.now() - timestamp < 5 * 60 * 1000) { + console.log("Importing ao tokens is already running. Skipping..."); + return; } + + const activeAddress = await getActiveAddress(); + if (!activeAddress) return; + + const aoSupport = await ExtensionStorage.get("setting_ao_support"); + if (!aoSupport) return; + + await ExtensionStorage.set(AO_TOKENS_IMPORT_TIMESTAMP, Date.now()); + + browser.alarms.create("import_ao_tokens", { when: Date.now() + 2000 }); } diff --git a/src/tokens/index.ts b/src/tokens/index.ts index 53b765ea6..f781b73f5 100644 --- a/src/tokens/index.ts +++ b/src/tokens/index.ts @@ -12,7 +12,7 @@ import { type TokenState, type TokenType } from "./token"; -import type { TokenInfo } from "./aoTokens/ao"; +import type { TokenInfoWithProcessId } from "./aoTokens/ao"; /** Default tokens */ export const defaultTokens: Token[] = [ @@ -53,17 +53,40 @@ export async function getTokens() { return tokens || defaultTokens; } + /** * Get stored ao tokens */ export async function getAoTokens() { - const tokens = await ExtensionStorage.get< - (TokenInfo & { processId: string })[] - >("ao_tokens"); + const tokens = await ExtensionStorage.get( + "ao_tokens" + ); + + return tokens || []; +} + +/** + * Get stored ao tokens cache + */ +export async function getAoTokensCache() { + const tokens = await ExtensionStorage.get( + "ao_tokens_cache" + ); return tokens || []; } +/** + * Get stored ao tokens removed ids + */ +export async function getAoTokensAutoImportRestrictedIds() { + const removedIds = await ExtensionStorage.get( + "ao_tokens_auto_import_restricted_ids" + ); + + return removedIds || []; +} + /** * Add a token to the stored tokens * @@ -120,6 +143,14 @@ export async function removeToken(id: string) { tokens.filter((token) => token.id !== id) ); } else if (aoTokens.some((token) => token.processId === id)) { + const restrictedTokenIds = await getAoTokensAutoImportRestrictedIds(); + if (!restrictedTokenIds.includes(id)) { + restrictedTokenIds.push(id); + await ExtensionStorage.set( + "ao_tokens_auto_import_restricted_ids", + restrictedTokenIds + ); + } await ExtensionStorage.set( "ao_tokens", aoTokens.filter((token) => token.processId !== id) diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index fe8283447..51f4eef66 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -3,7 +3,7 @@ import { ExtensionStorage } from "./storage"; import { getActiveAddress } from "~wallets"; import type { Transaction } from "~notifications/api"; import type { Token } from "~tokens/token"; -import type { TokenInfo } from "~tokens/aoTokens/ao"; +import type { TokenInfo, TokenInfoWithProcessId } from "~tokens/aoTokens/ao"; export const fetchNotifications = async (address: string) => { const n = await ExtensionStorage.get(`notifications_${address}`); @@ -53,9 +53,9 @@ export const mergeAndSortNotifications = ( export const fetchTokenByProcessId = async ( processId: string ): Promise => { - const tokens = await ExtensionStorage.get< - (TokenInfo & { processId: string })[] - >("ao_tokens"); + const tokens = await ExtensionStorage.get( + "ao_tokens" + ); if (!tokens || !processId) return null; return tokens.find((token) => token.processId === processId); From a8e55c10026f57a6efb864514de8951005e5c9b3 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 25 Jun 2024 22:38:58 +0545 Subject: [PATCH 02/31] refactor: sync functions --- src/tokens/aoTokens/sync.ts | 68 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/tokens/aoTokens/sync.ts b/src/tokens/aoTokens/sync.ts index e32023f7c..2c78bed93 100644 --- a/src/tokens/aoTokens/sync.ts +++ b/src/tokens/aoTokens/sync.ts @@ -216,24 +216,22 @@ export async function syncAoTokens() { isSyncInProgress = true; try { - const activeAddress = await getActiveAddress(); - if (!activeAddress) { - lastHasNextPage = false; - return { hasNextPage: false, syncCount: 0 }; - } + const [activeAddress, aoSupport] = await Promise.all([ + getActiveAddress(), + ExtensionStorage.get("setting_ao_support") + ]); - const aoSupport = await ExtensionStorage.get("setting_ao_support"); - if (!aoSupport) { + if (!activeAddress || !aoSupport) { lastHasNextPage = false; return { hasNextPage: false, syncCount: 0 }; } console.log("Synchronizing AO tokens..."); - const aoTokensCache = await getAoTokensCache(); - let aoTokensIds = - (await ExtensionStorage.get>(AO_TOKENS_IDS)) || - {}; + const [aoTokensCache, aoTokensIds = {}] = await Promise.all([ + getAoTokensCache(), + ExtensionStorage.get>(AO_TOKENS_IDS) + ]); const walletTokenIds = aoTokensIds[activeAddress] || []; const arweave = new Arweave(gateway); @@ -265,17 +263,19 @@ export async function syncAoTokens() { }, 2) ); const results = await Promise.allSettled(promises); - const tokens = results - .filter((result) => result.status === "fulfilled") - .map((result) => result.value) - .filter((token) => !!token.Ticker); - - const tokensWithoutTicker = results - .filter((result) => result.status === "fulfilled") - .map((result) => result.value) - .filter( - (token) => !token.Ticker && !walletTokenIds.includes(token.processId) - ); + + const tokens = []; + const tokensWithoutTicker = []; + results.forEach((result) => { + if (result.status === "fulfilled") { + const token = result.value; + if (token.Ticker) { + tokens.push(token); + } else if (!walletTokenIds.includes(token.processId)) { + tokensWithoutTicker.push(token); + } + } + }); const updatedTokens = [...aoTokensCache, ...tokens]; const updatedProcessIds = newProcessIds.filter((processId) => @@ -358,17 +358,19 @@ export async function importAoTokens(alarm: Alarms.Alarm) { }, 2) ); const results = await Promise.allSettled(promises); - const tokens = results - .filter((result) => result.status === "fulfilled") - .map((result) => result.value) - .filter((token) => !!token.Ticker); - - const tokensToRestrict = results - .filter((result) => result.status === "fulfilled") - .map((result) => result.value) - .filter( - (token) => !token.Ticker && !removedTokenIds.includes(token.processId) - ); + + const tokens = []; + const tokensToRestrict = []; + results.forEach((result) => { + if (result.status === "fulfilled") { + const token = result.value; + if (token.Ticker) { + tokens.push(token); + } else if (!removedTokenIds.includes(token.processId)) { + tokensToRestrict.push(token); + } + } + }); const updatedTokens = [...aoTokensCache, ...tokens]; From 77de61ad3f38590ff2a7a139299e1d314b5c8929 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 27 Jun 2024 00:04:01 +0545 Subject: [PATCH 03/31] feat: Add wallets quick settings --- assets/_locales/en/messages.json | 22 +- assets/_locales/zh_CN/messages.json | 22 +- .../dashboard/list/WalletListItem.tsx | 1 + src/components/popup/WalletHeader.tsx | 7 + src/popup.tsx | 16 + src/routes/dashboard/index.tsx | 6 +- src/routes/popup/settings/quickSettings.tsx | 169 ++++++++ .../settings/wallets/[address]/export.tsx | 111 +++++ .../settings/wallets/[address]/index.tsx | 395 ++++++++++++++++++ src/routes/popup/settings/wallets/index.tsx | 172 ++++++++ 10 files changed, 915 insertions(+), 6 deletions(-) create mode 100644 src/routes/popup/settings/quickSettings.tsx create mode 100644 src/routes/popup/settings/wallets/[address]/export.tsx create mode 100644 src/routes/popup/settings/wallets/[address]/index.tsx create mode 100644 src/routes/popup/settings/wallets/index.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 619a7a38a..31358eaed 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -347,6 +347,10 @@ "message": "Settings", "description": "Settings title" }, + "quick_settings": { + "message": "Quick Settings", + "description": "Quick Settings title" + }, "setting_apps": { "message": "Applications", "description": "Apps settings title" @@ -412,9 +416,17 @@ "description": "Notifications settings title" }, "setting_notifications_description": { - "message": "Toggle to receive alerts for new transactions in your wallet.", + "message": "Manage alerts for activities in your wallets", "description": "Notifications settings description" }, + "setting_all_settings": { + "message": "All Settings", + "description": "All settings title" + }, + "setting_all_settings_description": { + "message": "", + "description": "All settings description" + }, "setting_apps_description": { "message": "View all connected apps & settings", "description": "App settings description" @@ -487,6 +499,14 @@ "message": "Export keyfile", "description": "Export keyfile button" }, + "export_keyfile_description": { + "message": "Enter your password to export wallet keyfile", + "description": "Export keyfile description" + }, + "edit_wallet": { + "message": "Edit Wallet", + "description": "Edit wallet button" + }, "remove_wallet": { "message": "Remove wallet", "description": "Remove wallet button" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 9d01a8931..d71c1194f 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -347,6 +347,10 @@ "message": "设置", "description": "Settings title" }, + "quick_settings": { + "message": "快速设置", + "description": "Quick Settings title" + }, "setting_apps": { "message": "应用程序", "description": "Apps settings title" @@ -412,9 +416,17 @@ "description": "Notifications settings title" }, "setting_notifications_description": { - "message": "切换以接收钱包中新交易的提醒", + "message": "管理钱包活动警报", "description": "Notifications settings description" }, + "setting_all_settings": { + "message": "所有设置", + "description": "All settings description" + }, + "setting_all_settings_description": { + "message": "", + "description": "All settings description" + }, "setting_apps_description": { "message": "查看所有已连接的应用程序和设置", "description": "App settings description" @@ -487,6 +499,14 @@ "message": "导出密钥文件", "description": "Export keyfile button" }, + "export_keyfile_description": { + "message": "输入密码导出钱包密钥文件", + "description": "Export keyfile description" + }, + "edit_wallet": { + "message": "编辑钱包", + "description": "Edit wallet button" + }, "remove_wallet": { "message": "移除钱包", "description": "Remove wallet button" diff --git a/src/components/dashboard/list/WalletListItem.tsx b/src/components/dashboard/list/WalletListItem.tsx index 96f4920b9..bed9566bc 100644 --- a/src/components/dashboard/list/WalletListItem.tsx +++ b/src/components/dashboard/list/WalletListItem.tsx @@ -59,6 +59,7 @@ interface Props { name: string; address: string; active: boolean; + small?: boolean; } const HardwareIcon = styled(HardwareWalletIcon)` diff --git a/src/components/popup/WalletHeader.tsx b/src/components/popup/WalletHeader.tsx index d84a1d679..3018ffe61 100644 --- a/src/components/popup/WalletHeader.tsx +++ b/src/components/popup/WalletHeader.tsx @@ -443,6 +443,13 @@ export default function WalletHeader() { url: browser.runtime.getURL("tabs/dashboard.html") }) }, + { + icon: , + title: "Settings", + route: () => { + push("/quick-settings"); + } + }, { icon: , title: "expand_view", diff --git a/src/popup.tsx b/src/popup.tsx index 6dd422422..d70cb4cd1 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -34,6 +34,10 @@ import SubscriptionDetails from "~routes/popup/subscriptions/subscriptionDetails import SubscriptionPayment from "~routes/popup/subscriptions/subscriptionPayment"; import SubscriptionManagement from "~routes/popup/subscriptions/subscriptionManagement"; import Transactions from "~routes/popup/transaction/transactions"; +import QuickSettings from "~routes/popup/settings/quickSettings"; +import Wallets from "~routes/popup/settings/wallets"; +import Wallet from "~routes/popup/settings/wallets/[address]"; +import ExportWallet from "~routes/popup/settings/wallets/[address]/export"; export default function Popup() { const theme = useTheme(); @@ -81,6 +85,18 @@ export default function Popup() { + + + + {(params: { address: string }) => ( + + )} + + + {(params: { address: string }) => ( + + )} + {(params: { id: string }) => ( diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index db7444bcf..9a33f3f0e 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -9,14 +9,13 @@ import { import { useEffect, useMemo } from "react"; import { useLocation } from "wouter"; import { - TicketIcon, GridIcon, InformationIcon, TrashIcon, WalletIcon, BellIcon } from "@iconicicons/react"; -import { Users01 } from "@untitled-ui/icons-react"; +import { Coins04, Users01 } from "@untitled-ui/icons-react"; import WalletSettings from "~components/dashboard/subsettings/WalletSettings"; import TokenSettings from "~components/dashboard/subsettings/TokenSettings"; import AppSettings from "~components/dashboard/subsettings/AppSettings"; @@ -35,7 +34,6 @@ import browser from "webextension-polyfill"; import styled from "styled-components"; import settings from "~settings"; import { PageType, trackPage } from "~utils/analytics"; -import { formatSettingName } from "~utils/format"; import SignSettings from "~components/dashboard/SignSettings"; import AddToken from "~components/dashboard/subsettings/AddToken"; import NotificationSettings from "~components/dashboard/NotificationSettings"; @@ -283,7 +281,7 @@ const allSettings: Omit[] = [ name: "tokens", displayName: "setting_tokens", description: "setting_tokens_description", - icon: TicketIcon, + icon: Coins04, component: Tokens }, { diff --git a/src/routes/popup/settings/quickSettings.tsx b/src/routes/popup/settings/quickSettings.tsx new file mode 100644 index 000000000..eb6b0de3d --- /dev/null +++ b/src/routes/popup/settings/quickSettings.tsx @@ -0,0 +1,169 @@ +import HeadV2 from "~components/popup/HeadV2"; +import browser from "webextension-polyfill"; +import { GridIcon, WalletIcon, BellIcon } from "@iconicicons/react"; +import { + Settings01, + Users01, + LinkExternal02, + Coins04 +} from "@untitled-ui/icons-react"; +import Wallets from "~components/dashboard/Wallets"; +import Applications from "~components/dashboard/Applications"; +import Contacts from "~components/dashboard/Contacts"; +import NotificationSettings from "~components/dashboard/NotificationSettings"; +import Tokens from "../tokens"; +import { useLocation } from "wouter"; +import { useMemo } from "react"; +import { ListItem, ListItemIcon } from "@arconnect/components"; +import type { Icon } from "~settings/setting"; +import type { HTMLProps, ReactNode } from "react"; +import styled from "styled-components"; + +interface Props { + params: { + setting?: string; + subsetting?: string; + }; +} + +interface Setting extends SettingItemData { + name: string; + component?: (...args: any[]) => JSX.Element; + externalLink?: string; +} + +type SettingItemData = { + icon: Icon; + displayName: string; + description: string; + active: boolean; + isExternalLink?: boolean; +}; + +export default function QuickSettings({ params }: Props) { + // router location + const [, setLocation] = useLocation(); + + // active setting val + const activeSetting = useMemo(() => params.setting, [params.setting]); + + return ( + <> + + + {allSettings.map((setting, i) => ( + { + if (setting.externalLink) { + browser.tabs.create({ + url: browser.runtime.getURL(setting.externalLink) + }); + } else { + setLocation("/quick-settings/" + setting.name); + } + }} + key={i} + /> + ))} + + + ); +} + +function SettingListItem({ + displayName, + description, + icon, + active, + ...props +}: SettingItemData & HTMLProps) { + return ( + + {browser.i18n.getMessage(displayName)}{" "} + {props.isExternalLink && } + + ) as ReactNode & string + } + description={browser.i18n.getMessage(description)} + active={active} + small={true} + {...props} + > + + + ); +} + +const ExternalLinkIcon = styled(LinkExternal02)` + height: 1rem; + width: 1rem; + color: ${(props) => props.theme.secondaryTextv2}; +`; + +const Title = styled.div` + display: flex; + align-items: center; + justify-items: center; + gap: 8px; +`; + +const SettingsList = styled.div` + position: relative; + display: flex; + flex-direction: column; + gap: 1.125rem; + padding: 0 1rem; +`; + +const allSettings: Omit[] = [ + { + name: "wallets", + displayName: "setting_wallets", + description: "setting_wallets_description", + icon: WalletIcon, + component: Wallets + }, + { + name: "apps", + displayName: "setting_apps", + description: "setting_apps_description", + icon: GridIcon, + component: Applications + }, + { + name: "tokens", + displayName: "setting_tokens", + description: "setting_tokens_description", + icon: Coins04, + component: Tokens + }, + { + name: "contacts", + displayName: "setting_contacts", + description: "setting_contacts_description", + icon: Users01, + component: Contacts + }, + { + name: "notifications", + displayName: "setting_notifications", + description: "setting_notifications_description", + icon: BellIcon, + component: NotificationSettings + }, + { + name: "All Settings", + displayName: "setting_all_settings", + description: "setting_all_settings_description", + icon: Settings01, + externalLink: "tabs/dashboard.html" + } +]; diff --git a/src/routes/popup/settings/wallets/[address]/export.tsx b/src/routes/popup/settings/wallets/[address]/export.tsx new file mode 100644 index 000000000..8b73aa7f1 --- /dev/null +++ b/src/routes/popup/settings/wallets/[address]/export.tsx @@ -0,0 +1,111 @@ +import { + ButtonV2, + InputV2, + Spacer, + Text, + useInput, + useToasts +} from "@arconnect/components"; +import { type StoredWallet } from "~wallets"; +import { useMemo, useState } from "react"; +import { useStorage } from "@plasmohq/storage/hook"; +import { decryptWallet, freeDecryptedWallet } from "~wallets/encryption"; +import { ExtensionStorage } from "~utils/storage"; +import { downloadFile } from "~utils/file"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import HeadV2 from "~components/popup/HeadV2"; + +export default function ExportWallet({ address }: Props) { + // wallets + const [wallets] = useStorage( + { + key: "wallets", + instance: ExtensionStorage + }, + [] + ); + + const [loading, setLoading] = useState(false); + + // this wallet + const wallet = useMemo( + () => wallets?.find((w) => w.address === address), + [wallets, address] + ); + + // toasts + const { setToast } = useToasts(); + + // password input + const passwordInput = useInput(); + + // export the wallet + async function exportWallet() { + setLoading(true); + if (wallet.type === "hardware") { + throw new Error("Hardware wallet cannot be exported"); + } + + try { + // decrypt keyfile + const decrypted = await decryptWallet( + wallet.keyfile, + passwordInput.state + ); + + // download the file + downloadFile( + JSON.stringify(decrypted, null, 2), + "application/json", + `arweave-keyfile-${address}.json` + ); + + // remove wallet from memory + freeDecryptedWallet(decrypted); + } catch (e) { + console.log("Error exporting wallet", e.message); + setToast({ + type: "error", + content: browser.i18n.getMessage("export_wallet_error"), + duration: 2200 + }); + } finally { + setLoading(false); + } + } + + if (!wallet) return <>; + + return ( + <> + + + + {browser.i18n.getMessage("export_keyfile_description")} + + + + + {browser.i18n.getMessage("export")} + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + padding: 0 1rem; +`; + +interface Props { + address: string; +} diff --git a/src/routes/popup/settings/wallets/[address]/index.tsx b/src/routes/popup/settings/wallets/[address]/index.tsx new file mode 100644 index 000000000..96b6c0b63 --- /dev/null +++ b/src/routes/popup/settings/wallets/[address]/index.tsx @@ -0,0 +1,395 @@ +import { + ButtonV2, + InputV2, + ModalV2, + Spacer, + Text, + TooltipV2, + useInput, + useModal, + useToasts +} from "@arconnect/components"; +import { CopyIcon, DownloadIcon, TrashIcon } from "@iconicicons/react"; +import { InputWithBtn, InputWrapper } from "~components/arlocal/InputWrapper"; +import { removeWallet, type StoredWallet } from "~wallets"; +import { useEffect, useMemo, useState } from "react"; +import { useStorage } from "@plasmohq/storage/hook"; +import { IconButton } from "~components/IconButton"; +import { type AnsUser, getAnsProfile } from "~lib/ans"; +import { ExtensionStorage } from "~utils/storage"; +import keystoneLogo from "url:/assets/hardware/keystone.png"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import copy from "copy-to-clipboard"; +import { formatAddress } from "~utils/format"; +import HeadV2 from "~components/popup/HeadV2"; +import { QRCodeSVG } from "qrcode.react"; +import { useLocation } from "wouter"; + +export default function Wallet({ address }: Props) { + // wallets + const [wallets, setWallets] = useStorage( + { + key: "wallets", + instance: ExtensionStorage + }, + [] + ); + + // this wallet + const wallet = useMemo( + () => wallets?.find((w) => w.address === address), + [wallets, address] + ); + + // toasts + const { setToast } = useToasts(); + + // location + const [, setLocation] = useLocation(); + + // ans + const [ansLabel, setAnsLabel] = useState(); + + useEffect(() => { + (async () => { + if (!wallet) return; + + // get ans profile + const profile = (await getAnsProfile(wallet.address)) as AnsUser; + + if (!profile?.currentLabel) return; + + setAnsLabel(profile.currentLabel + ".ar"); + })(); + }, [wallet?.address]); + + // wallet name input + const walletNameInput = useInput(); + + useEffect(() => { + if (!wallet) return; + walletNameInput.setState(ansLabel || wallet.nickname); + }, [wallet, ansLabel]); + + // update nickname function + async function updateNickname() { + if (!!ansLabel) return; + + // check name + const newName = walletNameInput.state; + + if (!newName || newName === "") { + return setToast({ + type: "error", + content: "Please enter a valid nickname", + duration: 2200 + }); + } + + // update wallets + try { + await setWallets((val) => + val.map((wallet) => { + if (wallet.address !== address) { + return wallet; + } + + return { + ...wallet, + nickname: newName + }; + }) + ); + + setToast({ + type: "info", + content: browser.i18n.getMessage("updated_wallet_name"), + duration: 3000 + }); + } catch (e) { + console.log("Could not update nickname", e); + setToast({ + type: "error", + content: browser.i18n.getMessage("error_updating_wallet_name"), + duration: 3000 + }); + } + } + + // wallet remove modal + const removeModal = useModal(); + + if (!wallet) return <>; + + return ( + <> + + +
+ +
+ + {ansLabel || wallet.nickname} + {wallet.type === "hardware" && ( + + + + )} + + + {formatAddress(wallet.address, 8)} + + { + copy(wallet.address); + setToast({ + type: "info", + content: browser.i18n.getMessage("copied_address", [ + wallet.nickname, + formatAddress(wallet.address, 3) + ]), + duration: 2200 + }); + }} + /> + + +
+ +
+ {browser.i18n.getMessage("edit_wallet_name")} + {!!ansLabel && ( + {browser.i18n.getMessage("cannot_edit_with_ans")} + )} + + + + + + Save + + +
+
+ + setLocation(`/quick-settings/wallets/${address}/export`) + } + disabled={wallet.type === "hardware"} + > + + {browser.i18n.getMessage("export_keyfile")} + + + removeModal.setOpen(true)} + > + + {browser.i18n.getMessage("remove_wallet")} + +
+ + removeModal.setOpen(false)} + > + {browser.i18n.getMessage("cancel")} + + { + try { + await removeWallet(address); + setToast({ + type: "success", + content: browser.i18n.getMessage( + "removed_wallet_notification" + ), + duration: 2000 + }); + } catch (e) { + console.log("Error removing wallet", e); + setToast({ + type: "error", + content: browser.i18n.getMessage( + "remove_wallet_error_notification" + ), + duration: 2000 + }); + } + }} + > + {browser.i18n.getMessage("confirm")} + + + } + > + + {browser.i18n.getMessage("remove_wallet_modal_title")} + + + + {browser.i18n.getMessage("remove_wallet_modal_content")} + + + +
+ + ); +} + +function QrCodeIcon({ address }: { address: string }) { + // QR modal + const qrModal = useModal(); + + return ( + <> + qrModal.setOpen(true)}> + + + + + + + + + ); +} + +const LogoWrapper = styled.div<{ small?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + width: ${(props) => (props.small ? "2.1875rem" : "2.8rem;")}; + height: ${(props) => (props.small ? "2.1875rem" : "2.8rem;")}; + background-color: ${(props) => props.theme.primary}; + cursor: pointer; + border-radius: 11.905px; + + &:hover { + background-color: ${(props) => props.theme.primaryBtnHover}; + } +`; + +const CenterText = styled(Text)` + text-align: center; +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 80px); +`; + +const WalletName = styled(Text).attrs({ + title: true, + noMargin: true +})` + display: flex; + align-items: center; + gap: 0.45rem; + font-size: 1.25rem; + font-weight: 600; +`; + +const WalletWrapper = styled.div` + display: flex; + justify-content: space-between; +`; + +const HardwareWalletIcon = styled.img.attrs({ + draggable: false +})` + width: 32px; + height: 32px; + object-fit: contain; + user-select: none; +`; + +const WalletAddress = styled(Text)` + display: flex; + align-items: center; + gap: 0.37rem; + font-size: 0.875rem; +`; + +export const CopyButton = styled(CopyIcon)` + font-size: 1em; + width: 1em; + height: 1em; + color: rgb(${(props) => props.theme.secondaryText}); + cursor: pointer; + transition: all 0.23s ease-in-out; + + &:hover { + opacity: 0.8; + } + + &:focus { + transform: scale(0.87); + } +`; + +const Title = styled(Text).attrs({ + heading: true +})` + margin-bottom: 0.6em; + font-size: 1rem; +`; + +const Warning = styled(Text)` + color: rgb(255, 0, 0, 0.6); +`; + +interface Props { + address: string; +} diff --git a/src/routes/popup/settings/wallets/index.tsx b/src/routes/popup/settings/wallets/index.tsx new file mode 100644 index 000000000..965a7c83d --- /dev/null +++ b/src/routes/popup/settings/wallets/index.tsx @@ -0,0 +1,172 @@ +import { concatGatewayURL } from "~gateways/utils"; +import { ButtonV2, Spacer, useInput } from "@arconnect/components"; +import { useEffect, useState } from "react"; +import { useStorage } from "@plasmohq/storage/hook"; +import { type AnsUser, getAnsProfile } from "~lib/ans"; +import { ExtensionStorage } from "~utils/storage"; +import { useLocation } from "wouter"; +import type { StoredWallet } from "~wallets"; +import { Reorder } from "framer-motion"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import { useGateway } from "~gateways/wayfinder"; +import WalletListItem from "~components/dashboard/list/WalletListItem"; +import SearchInput from "~components/dashboard/SearchInput"; +import HeadV2 from "~components/popup/HeadV2"; + +export default function Wallets() { + // wallets + const [wallets, setWallets] = useStorage( + { + key: "wallets", + instance: ExtensionStorage + }, + [] + ); + + const [, setLocation] = useLocation(); + + // ans data + const [ansProfiles, setAnsProfiles] = useState([]); + + useEffect(() => { + (async () => { + if (!wallets) return; + + // fetch profiles + const profiles = await getAnsProfile(wallets.map((w) => w.address)); + + setAnsProfiles(profiles as AnsUser[]); + })(); + }, [wallets]); + + // ans shortcuts + const findProfile = (address: string) => + ansProfiles.find((profile) => profile.user === address); + + const gateway = useGateway({ startBlock: 0 }); + + function findAvatar(address: string) { + const avatar = findProfile(address)?.avatar; + const gatewayUrl = concatGatewayURL(gateway); + + if (!avatar) return undefined; + return gatewayUrl + "/" + avatar; + } + + function findLabel(address: string) { + const label = findProfile(address)?.currentLabel; + + if (!label) return undefined; + return label + ".ar"; + } + + // search + const searchInput = useInput(); + + // search filter function + function filterSearchResults(wallet: StoredWallet) { + const query = searchInput.state; + + if (query === "" || !query) { + return true; + } + + return ( + wallet.address.toLowerCase().includes(query.toLowerCase()) || + wallet.nickname.toLowerCase().includes(query.toLowerCase()) || + findLabel(wallet.address)?.includes(query.toLowerCase()) + ); + } + + return ( + <> + + +
+ + + {wallets && ( + + + {wallets.filter(filterSearchResults).map((wallet) => ( + + setLocation("/quick-settings/wallets/" + wallet.address) + } + key={wallet.address} + /> + ))} + + + )} +
+ + + + browser.tabs.create({ + url: browser.runtime.getURL("tabs/dashboard.html#/wallets/new") + }) + } + > + {browser.i18n.getMessage("add_wallet")} + + +
+ + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 10px); +`; + +const WalletsWrapper = styled.div` + max-height: 80vh; + overflow-y: auto; + padding: 0; + + /* Hide Scrollbar */ + scrollbar-width: none; /* For Firefox */ + -ms-overflow-style: none; /* For Internet Explorer and Edge */ + &::-webkit-scrollbar { + display: none; /* For Chrome, Safari, and Opera */ + } +`; + +const ActionBar = styled.div` + position: sticky; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 0; + background-color: rgb(${(props) => props.theme.background}); +`; From 8b659eb0353c034e334ea0443b0ad3840a822920 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 27 Jun 2024 17:23:33 +0545 Subject: [PATCH 04/31] feat: Add apps quick settings --- assets/_locales/en/messages.json | 4 +- assets/_locales/zh_CN/messages.json | 4 +- src/components/dashboard/SearchInput.tsx | 1 + src/components/dashboard/list/AppListItem.tsx | 5 +- .../dashboard/subsettings/AppSettings.tsx | 2 +- src/popup.tsx | 14 + .../popup/settings/apps/[url]/index.tsx | 466 ++++++++++++++++++ .../popup/settings/apps/[url]/permissions.tsx | 89 ++++ src/routes/popup/settings/apps/index.tsx | 166 +++++++ 9 files changed, 744 insertions(+), 7 deletions(-) create mode 100644 src/routes/popup/settings/apps/[url]/index.tsx create mode 100644 src/routes/popup/settings/apps/[url]/permissions.tsx create mode 100644 src/routes/popup/settings/apps/index.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 31358eaed..56bcedf3e 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -332,11 +332,11 @@ "description": "Remove button text" }, "block": { - "message": "Block", + "message": "Block app", "description": "Block button" }, "unblock": { - "message": "Unblock", + "message": "Unblock app", "description": "Unblock button" }, "setCustomGateway": { diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index d71c1194f..93dc8443e 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -332,11 +332,11 @@ "description": "Remove button text" }, "block": { - "message": "阻止", + "message": "阻止应用程序", "description": "Block button" }, "unblock": { - "message": "取消阻止", + "message": "解锁应用程序", "description": "Unblock button" }, "setCustomGateway": { diff --git a/src/components/dashboard/SearchInput.tsx b/src/components/dashboard/SearchInput.tsx index 938dd18fc..31d9d591e 100644 --- a/src/components/dashboard/SearchInput.tsx +++ b/src/components/dashboard/SearchInput.tsx @@ -27,4 +27,5 @@ const Wrapper = styled.div` interface Props { sticky?: boolean; + small?: boolean; } diff --git a/src/components/dashboard/list/AppListItem.tsx b/src/components/dashboard/list/AppListItem.tsx index c7617b2a3..a6221c360 100644 --- a/src/components/dashboard/list/AppListItem.tsx +++ b/src/components/dashboard/list/AppListItem.tsx @@ -18,7 +18,8 @@ export default function AppListItem({ interface Props { icon?: string; - name: string; - url: string; + name: React.ReactNode; + url: React.ReactNode; active: boolean; + small?: boolean; } diff --git a/src/components/dashboard/subsettings/AppSettings.tsx b/src/components/dashboard/subsettings/AppSettings.tsx index c2c3bf0e7..0d950b534 100644 --- a/src/components/dashboard/subsettings/AppSettings.tsx +++ b/src/components/dashboard/subsettings/AppSettings.tsx @@ -293,7 +293,7 @@ export default function AppSettings({ app, showTitle = false }: Props) { })) } fullWidth - placeholder="https://node2.bundlr.network" + placeholder="https://turbo.ardrive.io" /> removeModal.setOpen(true)}> diff --git a/src/popup.tsx b/src/popup.tsx index d70cb4cd1..8807d513b 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -38,6 +38,9 @@ import QuickSettings from "~routes/popup/settings/quickSettings"; import Wallets from "~routes/popup/settings/wallets"; import Wallet from "~routes/popup/settings/wallets/[address]"; import ExportWallet from "~routes/popup/settings/wallets/[address]/export"; +import Applications from "~routes/popup/settings/apps"; +import AppSettings from "~routes/popup/settings/apps/[url]"; +import AppPermissions from "~routes/popup/settings/apps/[url]/permissions"; export default function Popup() { const theme = useTheme(); @@ -97,6 +100,17 @@ export default function Popup() { )}
+ + + {(params: { url: string }) => ( + + )} + + + {(params: { url: string }) => ( + + )} + {(params: { id: string }) => ( diff --git a/src/routes/popup/settings/apps/[url]/index.tsx b/src/routes/popup/settings/apps/[url]/index.tsx new file mode 100644 index 000000000..59834fe99 --- /dev/null +++ b/src/routes/popup/settings/apps/[url]/index.tsx @@ -0,0 +1,466 @@ +import { InputWithBtn, InputWrapper } from "~components/arlocal/InputWrapper"; +import { defaultAllowance } from "~applications/allowance"; +import { InformationIcon } from "@iconicicons/react"; +import { useEffect, useMemo, useState } from "react"; +import { IconButton } from "~components/IconButton"; +import { removeApp } from "~applications"; +import { + ButtonV2, + InputV2, + ModalV2, + SelectV2, + Spacer, + Text, + TooltipV2, + useInput, + useModal, + useToasts +} from "@arconnect/components"; +import { concatGatewayURL, urlToGateway } from "~gateways/utils"; +import Application from "~applications/application"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import Arweave from "arweave"; +import { defaultGateway, suggestedGateways, testnets } from "~gateways/gateway"; +import HeadV2 from "~components/popup/HeadV2"; +import { useLocation } from "wouter"; + +export default function AppSettings({ url }: Props) { + // app settings + const app = new Application(url); + const [settings, updateSettings] = app.hook(); + const arweave = new Arweave(defaultGateway); + + const [, setLocation] = useLocation(); + + // allowance spent qty + const spent = useMemo(() => { + const val = settings?.allowance?.spent; + + if (!val) return "0"; + return val.toString(); + }, [settings]); + + // allowance limit + const limit = useMemo(() => { + const val = settings?.allowance?.limit; + + if (!val) return arweave.ar.arToWinston("0.1"); + return val.toString(); + }, [settings]); + + // active gateway + const gateway = useMemo(() => { + const val = settings?.gateway; + + if (!val) { + return concatGatewayURL(defaultGateway); + } + + return concatGatewayURL(val); + }, [settings]); + + // is the current gateway a custom one + const isCustom = useMemo(() => { + const gatewayUrls = suggestedGateways + .concat(testnets) + .map((g) => concatGatewayURL(g)); + + return !gatewayUrls.includes(gateway); + }, [gateway]); + + // editing custom gateway + const [editingCustom, setEditingCustom] = useState(false); + + // custom gateway input + const customGatewayInput = useInput(); + + const limitInput = useInput(); + + useEffect(() => { + if (!isCustom || !settings.gateway) return; + + setEditingCustom(true); + customGatewayInput.setState(concatGatewayURL(settings.gateway)); + }, [isCustom, settings?.gateway]); + + // toasts + const { setToast } = useToasts(); + + // remove modal + const removeModal = useModal(); + + if (!settings) return <>; + + return ( + <> + + +
+ + {browser.i18n.getMessage("permissions")} + + setLocation(`/quick-settings/apps/${url}/permissions`) + } + > + {browser.i18n.getMessage("view_all")} + + + + + + {browser.i18n.getMessage("allowance")} + + + + + + + updateSettings((val) => ({ + ...val, + allowance: { + ...defaultAllowance, + ...val.allowance, + enabled: !val.allowance.enabled + } + })) + } + /> + + + + +
+ {browser.i18n.getMessage("limit")} +
+ + updateSettings((val) => ({ + ...val, + allowance: { + ...defaultAllowance, + ...val.allowance, + limit: arweave.ar.arToWinston((e.target as any).value) + } + })) + } + fullWidth + /> +
+ AR +
+
+
+ + + + {browser.i18n.getMessage("spent")}:{" "} + {arweave.ar.winstonToAr(spent)} + {" AR "} + + + + updateSettings((val) => ({ + ...val, + allowance: { + ...defaultAllowance, + ...val.allowance, + spent: "0" + } + })) + } + > + {browser.i18n.getMessage("reset")} + + + + + {browser.i18n.getMessage("gateway")} + { + // @ts-expect-error + if (e.target.value === "custom") { + return setEditingCustom(true); + } + + setEditingCustom(false); + updateSettings((val) => ({ + ...val, + // @ts-expect-error + gateway: urlToGateway(e.target.value) + })); + }} + fullWidth + > + {suggestedGateways.concat(testnets).map((g, i) => { + const url = concatGatewayURL(g); + + return ( + + ); + })} + + + {editingCustom && ( + <> + + + + + + { + updateSettings((val) => ({ + ...val, + gateway: urlToGateway(customGatewayInput.state) + })); + setToast({ + type: "info", + content: browser.i18n.getMessage("setCustomGateway"), + duration: 3000 + }); + }} + > + Save + + + + )} + + {browser.i18n.getMessage("bundlrNode")} + + updateSettings((val) => ({ + ...val, + // @ts-expect-error + bundler: e.target.value + })) + } + fullWidth + placeholder="https://turbo.ardrive.io" + /> + +
+
+ removeModal.setOpen(true)}> + {browser.i18n.getMessage("removeApp")} + + + + updateSettings((val) => ({ + ...val, + blocked: !val.blocked + })) + } + > + {browser.i18n.getMessage(settings.blocked ? "unblock" : "block")} + + + removeModal.setOpen(false)}> + {browser.i18n.getMessage("cancel")} + + { + await removeApp(app.url); + setLocation(`/quick-settings/apps`); + }} + > + {browser.i18n.getMessage("remove")} + + + } + > + + {browser.i18n.getMessage("removeApp")} + + + + {browser.i18n.getMessage("removeAppNote")} + + + +
+
+ + ); +} + +interface Props { + url: string; +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 80px); +`; + +const InfoIcon = styled(InformationIcon)` + color: ${(props) => props.theme.secondaryTextv2}; +`; + +const TitleV1 = styled(Text).attrs({ + heading: true +})` + margin-bottom: 0; + font-size: 1.125rem; +`; + +const TitleV2 = styled(Text).attrs({ + heading: true +})` + margin-bottom: 0.6em; + font-size: 1rem; + font-weight: 600; +`; + +const NumberInputV2 = styled(InputV2)` + /* Chrome, Safari, Edge, Opera */ + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Firefox */ + &[type="number"] { + -moz-appearance: textfield; + } +`; + +const ResetButton = styled.span` + border-bottom: 1px solid rgba(${(props) => props.theme.theme}, 0.8); + margin-left: 0.37rem; + cursor: pointer; +`; + +const CenterText = styled(Text)` + text-align: center; + max-width: 22vw; + margin: 0 auto; + + @media screen and (max-width: 720px) { + max-width: 90vw; + } +`; + +const ToggleSwitch = styled.label<{ height?: string; width?: string }>` + position: relative; + display: inline-block; + width: ${(props) => props.width || "60px"}; + height: ${(props) => props.height || "34px"}; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + + &:before { + position: absolute; + content: ""; + height: calc(${(props) => props.height || "34px"} - 8px); + width: calc(${(props) => props.height || "34px"} - 8px); + left: 4px; + bottom: 4px; + background-color: white; + transition: 0.4s; + } + } + + input:checked + .slider { + background-color: ${(props) => props.theme.primary}; + } + + input:focus + .slider { + box-shadow: 0 0 1px ${(props) => props.theme.primary}; + } + + input:checked + .slider:before { + transform: translateX( + calc( + ${(props) => props.width || "60px"} - + ${(props) => props.height || "34px"} + ) + ); + } + + .slider.round { + border-radius: 34px; + + &:before { + border-radius: 50%; + } + } +`; + +const Flex = styled.div<{ alignItems: string; justifyContent: string }>` + display: flex; + align-items: ${(props) => props.alignItems}; + justify-content: ${(props) => props.justifyContent}; +`; diff --git a/src/routes/popup/settings/apps/[url]/permissions.tsx b/src/routes/popup/settings/apps/[url]/permissions.tsx new file mode 100644 index 000000000..acd876166 --- /dev/null +++ b/src/routes/popup/settings/apps/[url]/permissions.tsx @@ -0,0 +1,89 @@ +import { Checkbox, Spacer, Text } from "@arconnect/components"; +import Application from "~applications/application"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import HeadV2 from "~components/popup/HeadV2"; +import { permissionData, type PermissionType } from "~applications/permissions"; + +export default function AppPermissions({ url }: Props) { + // app settings + const app = new Application(url); + const [settings, updateSettings] = app.hook(); + + if (!settings) return <>; + + return ( + <> + + + {browser.i18n.getMessage("permissions")} + {Object.keys(permissionData).map( + (permissionName: PermissionType, i) => ( +
+ + updateSettings((val) => { + // toggle permission + if (checked && !val.permissions.includes(permissionName)) { + val.permissions.push(permissionName); + } else if (!checked) { + val.permissions = val.permissions.filter( + (p) => p !== permissionName + ); + } + + return val; + }) + } + checked={settings.permissions.includes(permissionName)} + > + {permissionName} + + {browser.i18n.getMessage(permissionData[permissionName])} + + + {i !== Object.keys(permissionData).length - 1 && ( + + )} +
+ ) + )} + +
+ + ); +} + +interface Props { + url: string; +} + +const Wrapper = styled.div` + padding: 0 1rem; +`; + +const Title = styled(Text).attrs({ + heading: true +})` + margin-bottom: 0.6em; + font-size: 1.125rem; +`; + +const PermissionCheckbox = styled(Checkbox)` + align-items: center; +`; + +export const PermissionDescription = styled(Text).attrs({ + noMargin: true +})` + margin-top: 0; + font-size: 0.625rem; +`; + +export const PermissionTitle = styled(Text).attrs({ + noMargin: true, + heading: true +})` + margin-top: 0.2rem; + font-size: 0.875rem; +`; diff --git a/src/routes/popup/settings/apps/index.tsx b/src/routes/popup/settings/apps/index.tsx new file mode 100644 index 000000000..07e6b771f --- /dev/null +++ b/src/routes/popup/settings/apps/index.tsx @@ -0,0 +1,166 @@ +import { Spacer, Text, useInput } from "@arconnect/components"; +import { useEffect, useMemo, useState } from "react"; +import { useStorage } from "@plasmohq/storage/hook"; +import { ExtensionStorage } from "~utils/storage"; +import { useLocation } from "wouter"; +import Application from "~applications/application"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import AppListItem from "~components/dashboard/list/AppListItem"; +import { SettingsList } from "~components/dashboard/list/BaseElement"; +import SearchInput from "~components/dashboard/SearchInput"; +import HeadV2 from "~components/popup/HeadV2"; +import useActiveTab from "~applications/useActiveTab"; +import { getAppURL } from "~utils/format"; + +export default function Applications() { + // connected apps + const [connectedApps] = useStorage( + { + key: "apps", + instance: ExtensionStorage + }, + [] + ); + + // apps + const [apps, setApps] = useState([]); + + useEffect(() => { + (async () => { + if (!connectedApps) return; + const appsWithData: SettingsAppData[] = []; + + for (const app of connectedApps) { + const appObj = new Application(app); + const appData = await appObj.getAppData(); + + appsWithData.push({ + name: appData.name || app, + url: app, + icon: appData.logo + }); + } + + setApps(appsWithData); + })(); + }, [connectedApps]); + + // router + const [, setLocation] = useLocation(); + + // active app + const activeTab = useActiveTab(); + const activeApp = useMemo(() => { + if (!activeTab?.url) { + return undefined; + } + + return new Application(getAppURL(activeTab.url)); + }, [activeTab]); + + // search + const searchInput = useInput(); + + // search filter function + function filterSearchResults(app: SettingsAppData) { + const query = searchInput.state; + + if (query === "" || !query) { + return true; + } + + return ( + app.name.toLowerCase().includes(query.toLowerCase()) || + app.url.toLowerCase().includes(query.toLowerCase()) + ); + } + + return ( + <> + + + + + + + + {apps.filter(filterSearchResults).map((app, i) => ( + + {app.url === activeApp.url ? ( + + + Active + + ) : ( + app.url + )} + + } + icon={app.icon} + active={false} + onClick={() => + setLocation( + "/quick-settings/apps/" + encodeURIComponent(app.url) + ) + } + key={i} + /> + ))} + + {connectedApps && connectedApps.length === 0 && ( + {browser.i18n.getMessage("no_apps_added")} + )} + + + ); +} + +interface SettingsAppData { + name: string; + url: string; + icon?: string; +} + +const Wrapper = styled.div` + position: relative; + padding: 0 1rem; +`; + +const SearchWrapper = styled.div` + position: sticky; + top: 0; + left: 0; + right: 0; + z-index: 20; + background-color: rgb(${(props) => props.theme.cardBackground}); +`; + +const NoAppsText = styled(Text)` + text-align: center; + padding-top: 0.5rem; +`; + +const AppOnline = styled.div` + width: 5px; + height: 5px; + border-radius: 100%; + background-color: ${(props) => props.theme.success}; + border: 1px solid rgb(${(props) => props.theme.background}); +`; + +const ActiveText = styled(Text)` + display: flex; + align-items: center; + font-size: 0.625rem; + color: ${(props) => props.theme.success}; + gap: 4px; +`; From 272aab146cbcadf862b43ddc5df21ff63e29a127 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Mon, 1 Jul 2024 13:25:12 +0545 Subject: [PATCH 05/31] feat: Add tokens quick settings --- assets/_locales/en/messages.json | 10 +- assets/_locales/zh_CN/messages.json | 8 + .../dashboard/subsettings/AddToken.tsx | 22 +- src/popup.tsx | 10 + .../popup/settings/tokens/[id]/index.tsx | 186 +++++++++++ src/routes/popup/settings/tokens/index.tsx | 310 ++++++++++++++++++ src/routes/popup/settings/tokens/new.tsx | 23 ++ 7 files changed, 562 insertions(+), 7 deletions(-) create mode 100644 src/routes/popup/settings/tokens/[id]/index.tsx create mode 100644 src/routes/popup/settings/tokens/index.tsx create mode 100644 src/routes/popup/settings/tokens/new.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 56bcedf3e..0b42da3bd 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -495,6 +495,10 @@ "message": "Search for a wallet...", "description": "Search input placeholder for wallets" }, + "search_tokens": { + "message": "Search for a token...", + "description": "Search input placeholder for tokens" + }, "export_keyfile": { "message": "Export keyfile", "description": "Export keyfile button" @@ -1132,7 +1136,7 @@ "description": "Hover text for drag icon" }, "token_type": { - "message": "Token type", + "message": "Token Type", "description": "Token type label" }, "token_type_asset": { @@ -1816,6 +1820,10 @@ "message": "ao.computer", "description": "ao computer url" }, + "ao_tokens": { + "message": "AO Tokens", + "description": "Ao Tokens" + }, "ao_announcement_text": { "message": "Look out for new updates around ao in the future. To learn more visit", "description": "ao announcement description text" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 93dc8443e..0c24c2253 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -495,6 +495,10 @@ "message": "搜索钱包...", "description": "Search input placeholder for wallets" }, + "search_tokens": { + "message": "搜索令牌...", + "description": "Search input placeholder for tokens" + }, "export_keyfile": { "message": "导出密钥文件", "description": "Export keyfile button" @@ -1814,6 +1818,10 @@ "message": "ao.computer", "description": "ao computer url" }, + "ao_tokens": { + "message": "AO代币", + "description": "Ao Tokens" + }, "ao_announcement_text": { "message": "关注未来的 ao 更新。了解更多访问", "description": "ao announcement description text" diff --git a/src/components/dashboard/subsettings/AddToken.tsx b/src/components/dashboard/subsettings/AddToken.tsx index 49cc07fc7..8cb248f0b 100644 --- a/src/components/dashboard/subsettings/AddToken.tsx +++ b/src/components/dashboard/subsettings/AddToken.tsx @@ -9,7 +9,7 @@ import { } from "@arconnect/components"; import browser from "webextension-polyfill"; import { useEffect, useState } from "react"; -import { useAo, type TokenInfo, useAoTokens } from "~tokens/aoTokens/ao"; +import { type TokenInfo } from "~tokens/aoTokens/ao"; import { Token } from "ao-tokens"; import styled from "styled-components"; import { isAddress } from "~utils/assertions"; @@ -21,7 +21,11 @@ import type { TokenState, TokenType } from "~tokens/token"; import { concatGatewayURL } from "~gateways/utils"; import { useGateway } from "~gateways/wayfinder"; -export default function AddToken() { +interface AddTokenProps { + isQuickSetting?: boolean; +} + +export default function AddToken({ isQuickSetting }: AddTokenProps) { const targetInput = useInput(); const gateway = useGateway({ startBlock: 0 }); const [tokenType, setTokenType] = useState("asset"); @@ -30,7 +34,6 @@ export default function AddToken() { const [loading, setLoading] = useState(false); const [warp, setWarp] = useState(null); const tokens = useTokens(); - const ao = useAo(); const { setToast } = useToasts(); const onImportToken = async () => { @@ -114,9 +117,14 @@ export default function AddToken() { return (
- - {browser.i18n.getMessage("import_token")} + {!isQuickSetting && ( + <> + + {browser.i18n.getMessage("import_token")} + + )} { // @ts-expect-error setTokenType(e.target.value); @@ -158,6 +167,7 @@ export default function AddToken() { )} + + + {(params: { id: string }) => ( + + )} + + {(params: { id: string }) => ( diff --git a/src/routes/popup/settings/tokens/[id]/index.tsx b/src/routes/popup/settings/tokens/[id]/index.tsx new file mode 100644 index 000000000..5f96eb139 --- /dev/null +++ b/src/routes/popup/settings/tokens/[id]/index.tsx @@ -0,0 +1,186 @@ +import { + ButtonV2, + SelectV2, + Spacer, + Text, + TooltipV2, + useToasts +} from "@arconnect/components"; +import type { Token, TokenType } from "~tokens/token"; +import { useStorage } from "@plasmohq/storage/hook"; +import { ExtensionStorage } from "~utils/storage"; +import { TrashIcon } from "@iconicicons/react"; +import { removeToken } from "~tokens"; +import { useMemo } from "react"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import copy from "copy-to-clipboard"; +import { formatAddress } from "~utils/format"; +import { CopyButton } from "~components/dashboard/subsettings/WalletSettings"; +import HeadV2 from "~components/popup/HeadV2"; +import { useLocation } from "wouter"; + +export default function TokenSettings({ id }: Props) { + // tokens + const [tokens, setTokens] = useStorage( + { + key: "tokens", + instance: ExtensionStorage + }, + [] + ); + + // ao tokens + const [aoTokens] = useStorage( + { + key: "ao_tokens", + instance: ExtensionStorage + }, + [] + ); + + const { setToast } = useToasts(); + + const [, setLocation] = useLocation(); + + const { token, isAoToken } = useMemo(() => { + const aoToken = aoTokens.find((ao) => ao.processId === id); + if (aoToken) { + return { + token: { + ...aoToken, + id: aoToken.processId, + name: aoToken.Name, + ticker: aoToken.Ticker + // Map additional AO token properties as needed + }, + isAoToken: true + }; + } + const regularToken = tokens.find((t) => t.id === id); + return { + token: regularToken, + isAoToken: false + }; + }, [tokens, aoTokens, id]); + + // update token type + function updateType(type: TokenType) { + setTokens((allTokens) => { + const tokenIndex = allTokens.findIndex((t) => t.id === id); + if (tokenIndex !== -1) { + allTokens[tokenIndex].type = type; + } + return [...allTokens]; + }); + } + + if (!token) return null; + + return ( + <> + + +
+ + + Symbol: + {token.ticker} + + + + Address: + {formatAddress(token.id, 8)} + + + { + copy(token.id); + setToast({ + type: "info", + content: browser.i18n.getMessage("copied_address", [ + formatAddress(token.id, 8) + ]), + duration: 2200 + }); + }} + /> + + + {!isAoToken && ( + { + // @ts-expect-error + updateType(e.target.value as TokenType); + }} + fullWidth + > + + + + )} +
+ { + await removeToken(id); + setLocation(`/quick-settings/tokens`); + }} + style={{ backgroundColor: "#8C1A1A" }} + > + + {browser.i18n.getMessage("remove_token")} + +
+ + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 80px); +`; + +const TokenAddress = styled(Text).attrs({ + margin: true +})` + margin-top: 12px; + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.37rem; +`; + +interface Props { + id: string; +} + +const Property = styled.div` + display: flex; + gap: 4px; +`; + +const BasePropertyText = styled(Text).attrs({ + noMargin: true +})` + font-size: 1rem; + font-weight: 500; +`; + +const PropertyName = styled(BasePropertyText)``; + +const PropertyValue = styled(BasePropertyText)` + color: rgb(${(props) => props.theme.primaryText}); +`; diff --git a/src/routes/popup/settings/tokens/index.tsx b/src/routes/popup/settings/tokens/index.tsx new file mode 100644 index 000000000..fec204274 --- /dev/null +++ b/src/routes/popup/settings/tokens/index.tsx @@ -0,0 +1,310 @@ +import { useStorage } from "@plasmohq/storage/hook"; +import { ExtensionStorage } from "~utils/storage"; +import { useLocation } from "wouter"; +import { useEffect, useMemo, useState } from "react"; +import type { Token, TokenType } from "~tokens/token"; +import styled from "styled-components"; +import browser from "webextension-polyfill"; +import { ButtonV2, Spacer, useInput } from "@arconnect/components"; +import { type TokenInfoWithBalance } from "~tokens/aoTokens/ao"; +import HeadV2 from "~components/popup/HeadV2"; +import { DREContract, DRENode } from "@arconnect/warp-dre"; +import { loadTokenLogo } from "~tokens/token"; +import { formatAddress } from "~utils/format"; +import { getDreForToken } from "~tokens"; +import { useTheme } from "~utils/theme"; +import * as viewblock from "~lib/viewblock"; +import { useGateway } from "~gateways/wayfinder"; +import { concatGatewayURL } from "~gateways/utils"; +import aoLogo from "url:/assets/ecosystem/ao-logo.svg"; +import arLogoDark from "url:/assets/ar/logo_dark.png"; +import { getUserAvatar } from "~lib/avatar"; +import SearchInput from "~components/dashboard/SearchInput"; + +export default function Tokens() { + // tokens + const [tokens] = useStorage( + { + key: "tokens", + instance: ExtensionStorage + }, + [] + ); + + const [aoTokens] = useStorage( + { + key: "ao_tokens", + instance: ExtensionStorage + }, + [] + ); + + const enhancedAoTokens = useMemo(() => { + return aoTokens.map((token) => ({ + id: token.processId, + defaultLogo: token.Logo, + balance: "0", + ticker: token.Ticker, + type: "asset" as TokenType, + name: token.Name + })); + }, [aoTokens]); + + const [aoSettingsState, setaoSettingsState] = useState(true); + + useEffect(() => { + (async () => { + const currentSetting = await ExtensionStorage.get( + "setting_ao_support" + ); + setaoSettingsState(currentSetting); + })(); + }, []); + + // router + const [, setLocation] = useLocation(); + + // search + const searchInput = useInput(); + + // search filter function + function filterSearchResults(token: Token) { + const query = searchInput.state; + + if (query === "" || !query) { + return true; + } + + return ( + token.name.toLowerCase().includes(query.toLowerCase()) || + token.ticker.toLowerCase().includes(query.toLowerCase()) + ); + } + + const addToken = () => { + setLocation("/quick-settings/tokens/new"); + }; + + const handleTokenClick = (token: { + id: any; + defaultLogo?: string; + balance?: string; + ticker?: string; + type?: TokenType; + name?: string; + }) => { + setLocation(`/quick-settings/tokens/${token.id}`); + }; + + return ( + <> + + +
+ + +
+ + {tokens.filter(filterSearchResults).map((token) => ( + + ))} + + {enhancedAoTokens.length > 0 && aoSettingsState && ( + <> + + {enhancedAoTokens.filter(filterSearchResults).map((token) => ( +
handleTokenClick(token)} key={token.id}> + +
+ ))} + + )} +
+
+ + + {browser.i18n.getMessage("import_token")} + + +
+ + ); +} + +function TokenListItem({ token, ao, onClick }: Props) { + // format address + const formattedAddress = useMemo( + () => formatAddress(token.id, 8), + [token.id] + ); + + // display theme + const theme = useTheme(); + + // token logo + const [image, setImage] = useState(viewblock.getTokenLogo(token.id)); + + // gateway + const gateway = useGateway({ startBlock: 0 }); + + useEffect(() => { + (async () => { + try { + // if it is a collectible, we don't need to determinate the logo + if (token.type === "collectible") { + return setImage( + `${concatGatewayURL(token.gateway || gateway)}/${token.id}` + ); + } + if (ao) { + if (token.defaultLogo) { + const logo = await getUserAvatar(token.defaultLogo); + return setImage(logo); + } else { + return setImage(arLogoDark); + } + } + + // query community logo using Warp DRE + const node = new DRENode(await getDreForToken(token.id)); + const contract = new DREContract(token.id, node); + const result = await contract.query<[string]>( + "$.settings.[?(@[0] === 'communityLogo')][1]" + ); + + setImage(await loadTokenLogo(token.id, result[0], theme)); + } catch { + setImage(viewblock.getTokenLogo(token.id)); + } + })(); + }, [token, theme, gateway, ao]); + + // router + const [, setLocation] = useLocation(); + + const handleClick = () => { + if (onClick) { + onClick(); + } else { + setLocation(`/quick-settings/tokens/${token.id}`); + } + }; + + return ( + + +
+ {token.name} + + {formattedAddress} + {ao && ao logo} + {!ao && {token.type}} + +
+
+ ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 70px); +`; + +const Label = styled.p` + font-size: 0.7rem; + font-weight: 600; + color: rgb(${(props) => props.theme.primaryText}); + margin: 0; + margin-bottom: 0.8em; +`; + +const ActionBar = styled.div` + position: sticky; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 0; + background-color: rgb(${(props) => props.theme.background}); +`; + +const Image = styled.img` + width: 12px; + padding: 0 8px; + border: 1px solid rgb(${(props) => props.theme.cardBorder}); + border-radius: 2px; +`; + +const DescriptionWrapper = styled.div` + display: flex; + gap: 8px; + font-size: 0.625rem; + color: ${(props) => props.theme.secondaryTextv2}; +`; + +const TitleWrapper = styled.div` + font-size: 1rem; + font-weight: 600; +`; + +const ListItem = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; + padding: 0.625rem; + border-radius: 10px; + overflow: hidden; + cursor: pointer; + + &:hover { + background-color: ${(props) => props.theme.secondaryItemHover}; + } +`; + +const TokenLogo = styled.img.attrs({ + alt: "token-logo", + draggable: false +})` + width: 2rem; + height: 2rem; +`; + +const TokenType = styled.span` + padding: 0.08rem 0.2rem; + background-color: rgb(${(props) => props.theme.theme}); + color: #fff; + font-weight: 500; + font-size: 0.5rem; + text-transform: uppercase; + margin-left: 0.45rem; + width: max-content; + border-radius: 5px; +`; + +interface Props { + token: Token; + ao?: boolean; + active: boolean; + onClick?: () => void; +} diff --git a/src/routes/popup/settings/tokens/new.tsx b/src/routes/popup/settings/tokens/new.tsx new file mode 100644 index 000000000..6c1fdc933 --- /dev/null +++ b/src/routes/popup/settings/tokens/new.tsx @@ -0,0 +1,23 @@ +import HeadV2 from "~components/popup/HeadV2"; +import browser from "webextension-polyfill"; +import AddToken from "~components/dashboard/subsettings/AddToken"; +import styled from "styled-components"; + +export default function NewToken() { + return ( + <> + + + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 80px); +`; From 66feb993b16f14dc2e13af9b8f26627486656e48 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Mon, 1 Jul 2024 18:53:46 +0545 Subject: [PATCH 06/31] feat: Add contacts quick settings --- assets/_locales/en/messages.json | 20 ++ assets/_locales/zh_CN/messages.json | 20 ++ src/components/dashboard/Contacts.tsx | 30 ++- .../dashboard/list/ContactListItem.tsx | 28 +-- .../dashboard/subsettings/AddContact.tsx | 59 +++--- .../dashboard/subsettings/ContactSettings.tsx | 187 +++++++++++------- src/popup.tsx | 13 ++ .../settings/contacts/[address]/index.tsx | 27 +++ src/routes/popup/settings/contacts/index.tsx | 23 +++ src/routes/popup/settings/contacts/new.tsx | 23 +++ 10 files changed, 323 insertions(+), 107 deletions(-) create mode 100644 src/routes/popup/settings/contacts/[address]/index.tsx create mode 100644 src/routes/popup/settings/contacts/index.tsx create mode 100644 src/routes/popup/settings/contacts/new.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 0b42da3bd..117fee6af 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1001,6 +1001,10 @@ "message": "Contacts", "description": "Contacts setting title" }, + "setting_contact": { + "message": "Contact", + "description": "Contact setting title" + }, "setting_contacts_description": { "message": "Add/edit contacts", "description": "Contacts setting description" @@ -1677,6 +1681,10 @@ "message": "Add contact", "description": "Add a contact button text" }, + "new": { + "message": "New", + "description": "Add a new button text" + }, "send_transaction": { "message": "Send transaction", "description": "Active contact send button text" @@ -1693,6 +1701,10 @@ "message": "Create contact?", "description": "Create contact question" }, + "new_contact": { + "message": "New Contact", + "description": "New contact title" + }, "add_new_contact": { "message": "Add a new contact", "description": "Add a new contact title" @@ -1749,6 +1761,10 @@ "message": "Remove contact", "description": "Remove contact button text" }, + "edit_contact": { + "message": "Edit contact", + "description": "Edit contact button text" + }, "remove_contact_question": { "message": "Are you sure you want to remove this contact?", "description": "Remove contact modal text" @@ -1757,6 +1773,10 @@ "message": "Save new contact", "description": "Save new contact button text" }, + "save_contact": { + "message": "Save contact", + "description": "Save contact button text" + }, "address_in_use": { "message": "Address is already in use.", "description": "Contacts error message if address is already in use"}, diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 0c24c2253..d154e5960 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -996,6 +996,10 @@ "setting_contacts": { "message": "联系人", "description": "Contacts setting title" + }, + "setting_contact": { + "message": "接触", + "description": "Contact setting title" }, "setting_contacts_description": { "message": "添加/编辑联系人", @@ -1674,6 +1678,10 @@ "message": "添加联系人", "description": "Add a contact button text" }, + "new": { + "message": "新的", + "description": "Add a new button text" + }, "send_transaction": { "message": "发送交易", "description": "Active contact send button text" @@ -1690,6 +1698,10 @@ "message": "创建联系人?", "description": "Create contact question" }, + "new_contact": { + "message": "新联系人", + "description": "New contact title" + }, "add_new_contact": { "message": "添加新联系人", "description": "Add a new contact title" @@ -1746,6 +1758,10 @@ "message": "移除联系人", "description": "Remove contact button text" }, + "edit_contact": { + "message": "更改联系资料", + "description": "Edit contact button text" + }, "remove_contact_question": { "message": "您确定要移除此联系人吗?", "description": "Remove contact modal text" @@ -1754,6 +1770,10 @@ "message": "保存新联系人", "description": "Save new contact button text" }, + "save_contact": { + "message": "保存联系方式", + "description": "Save contact button text" + }, "address_in_use": { "message": "地址已在使用中。", "description": "Contacts error message if address is already in use" diff --git a/src/components/dashboard/Contacts.tsx b/src/components/dashboard/Contacts.tsx index 25d7d1dc9..9a755ee4a 100644 --- a/src/components/dashboard/Contacts.tsx +++ b/src/components/dashboard/Contacts.tsx @@ -1,4 +1,4 @@ -import { ButtonV2, Spacer, useInput } from "@arconnect/components"; +import { ButtonV2, Spacer, Text, useInput } from "@arconnect/components"; import React, { useState, useEffect, useMemo } from "react"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; @@ -12,8 +12,13 @@ import { formatAddress } from "~utils/format"; import { multiSort } from "~utils/multi_sort"; import { enrichContact } from "~contacts/hooks"; import { EventType, trackEvent } from "~utils/analytics"; +import type { Contacts } from "~components/Recipient"; -export default function Contacts() { +interface ContactsProps { + isQuickSetting?: boolean; +} + +export default function Contacts({ isQuickSetting }: ContactsProps) { // contacts const [storedContacts, setStoredContacts] = useStorage( { @@ -31,7 +36,7 @@ export default function Contacts() { useEffect(() => { async function fetchContacts() { - const storedContacts = await ExtensionStorage.get("contacts"); + const storedContacts = await ExtensionStorage.get("contacts"); if (storedContacts) { const namedContacts = storedContacts.filter((contact) => contact.name); @@ -100,7 +105,11 @@ export default function Contacts() { // Update the URL when a contact is clicked const handleContactClick = (contactAddress: string) => { - setLocation(`/contacts/${encodeURIComponent(contactAddress)}`); + setLocation( + `/${isQuickSetting ? "quick-settings/" : ""}contacts/${encodeURIComponent( + contactAddress + )}` + ); }; const searchInput = useInput(); @@ -121,22 +130,24 @@ export default function Contacts() { const addContact = () => { trackEvent(EventType.ADD_CONTACT, { fromContactSettings: true }); - setLocation("/contacts/new"); + setLocation(`/${isQuickSetting ? "quick-settings/" : ""}contacts/new`); }; return ( - {browser.i18n.getMessage("add_contact")} + {browser.i18n.getMessage(isQuickSetting ? "new" : "add_contact")} + {browser.i18n.getMessage("your_contacts")} {Object.entries(groupedContacts).map(([letter, contacts]) => { const filteredContacts = contacts.filter(filterSearchResults); @@ -153,6 +164,7 @@ export default function Contacts() { {/* Check if contact has name */} {contact.name && ( `rgb(${props.theme.primaryText})`}; + font-size: 1.25rem; +`; diff --git a/src/components/dashboard/list/ContactListItem.tsx b/src/components/dashboard/list/ContactListItem.tsx index c11112342..ef36c7c71 100644 --- a/src/components/dashboard/list/ContactListItem.tsx +++ b/src/components/dashboard/list/ContactListItem.tsx @@ -11,7 +11,7 @@ export default function ContactListItem({ ...props }: Props & HTMLProps) { return ( - + {/* @ts-ignore */} {!profileIcon && } @@ -25,14 +25,14 @@ interface Props { address: string; profileIcon: string; active: boolean; + small?: boolean; } -const ContactWrapper = styled.div<{ active: boolean }>` +const ContactWrapper = styled.div<{ active: boolean; small?: boolean }>` display: flex; flex-direction: column; justify-content: center; - padding: ${(props) => (props.active ? "0px 12.8px" : "0px 12.8px")}; - border-radius: 20px; + border-radius: ${(props) => (props.small ? "10px" : "20px")}; background-color: rgba( ${(props) => props.theme.theme}, ${(props) => @@ -43,15 +43,17 @@ const ContactWrapper = styled.div<{ active: boolean }>` &:hover { background-color: rgba( ${(props) => - props.theme.theme + - ", " + - (props.active - ? props.theme.displayTheme === "light" - ? ".24" - : ".14" - : props.theme.displayTheme === "light" - ? ".14" - : ".04")} + props.small + ? "43, 40, 56, 1" + : props.theme.theme + + ", " + + (props.active + ? props.theme.displayTheme === "light" + ? ".24" + : ".14" + : props.theme.displayTheme === "light" + ? ".14" + : ".04")} ); } `; diff --git a/src/components/dashboard/subsettings/AddContact.tsx b/src/components/dashboard/subsettings/AddContact.tsx index 0348edae6..3a5293e9e 100644 --- a/src/components/dashboard/subsettings/AddContact.tsx +++ b/src/components/dashboard/subsettings/AddContact.tsx @@ -35,7 +35,11 @@ import copy from "copy-to-clipboard"; import { gql } from "~gateways/api"; import { useTheme } from "~utils/theme"; -export default function AddContact() { +interface AddContactProps { + isQuickSetting?: boolean; +} + +export default function AddContact({ isQuickSetting }: AddContactProps) { // contacts const [storedContacts, setStoredContacts] = useStorage( { @@ -218,7 +222,9 @@ export default function AddContact() { avatarId: "" }); - setLocation(`/contacts/${contact.address}`); + setLocation( + `/${isQuickSetting ? "quick-settings/" : ""}contacts/${contact.address}` + ); } catch (error) { console.error("Error updating contacts:", error); } @@ -238,7 +244,7 @@ export default function AddContact() { }); removeContactModal.setOpen(false); - setLocation("/contacts"); + setLocation(`/${isQuickSetting ? "quick-settings/" : ""}contacts`); }; const areFieldsEmpty = () => { @@ -248,19 +254,23 @@ export default function AddContact() { return (
-
- -
- {browser.i18n.getMessage("add_new_contact")} -
-
- {browser.i18n.getMessage("contact_avatar")} + {!isQuickSetting && ( +
+ +
+ {browser.i18n.getMessage("add_new_contact")} +
+
+ )} + + {browser.i18n.getMessage("contact_avatar")} + {contact.avatarId && contact.profileIcon && ( - + )} {!contact.avatarId && !contact.profileIcon && ( - + {generateProfileIcon(contact.name, contact.address)} )} @@ -275,18 +285,18 @@ export default function AddContact() { onChange={handleAvatarUpload} /> - {browser.i18n.getMessage("name")} + {browser.i18n.getMessage("name")}* - + {browser.i18n.getMessage("arweave_account_address")}* @@ -294,7 +304,7 @@ export default function AddContact() { type="text" list="addressOptions" fullWidth - small + small={isQuickSetting} name="address" placeholder={ contact.address @@ -312,11 +322,13 @@ export default function AddContact() { ))} - {browser.i18n.getMessage("ArNS_address")} + + {browser.i18n.getMessage("ArNS_address")} + @@ -335,8 +347,9 @@ export default function AddContact() { ))} - {browser.i18n.getMessage("notes")} + {browser.i18n.getMessage("notes")} setContact({ ...contact, notes: e.target.value })} @@ -349,7 +362,9 @@ export default function AddContact() { onClick={saveNewContact} disabled={areFieldsEmpty()} > - {browser.i18n.getMessage("save_new_contact")} + {browser.i18n.getMessage( + isQuickSetting ? "save_contact" : "save_new_contact" + )} ` + height: ${(props) => (props.small ? "130px;" : "235px;")}; `; diff --git a/src/components/dashboard/subsettings/ContactSettings.tsx b/src/components/dashboard/subsettings/ContactSettings.tsx index b08f9ac07..2d11cd277 100644 --- a/src/components/dashboard/subsettings/ContactSettings.tsx +++ b/src/components/dashboard/subsettings/ContactSettings.tsx @@ -25,8 +25,9 @@ import styled from "styled-components"; import { svgie } from "~utils/svgies"; import { useLocation } from "wouter"; import copy from "copy-to-clipboard"; +import { formatAddress } from "~utils/format"; -export default function ContactSettings({ address }: Props) { +export default function ContactSettings({ address, isQuickSetting }: Props) { // contacts const [storedContacts, setStoredContacts] = useStorage( { @@ -193,7 +194,9 @@ export default function ContactSettings({ address }: Props) { if (editable) { return ( <> - {browser.i18n.getMessage("ArNS_address")} + + {browser.i18n.getMessage("ArNS_address")} + - {browser.i18n.getMessage("ArNS_address")} - + + {browser.i18n.getMessage("ArNS_address")} + + {browser.i18n.getMessage("arweave_url") + contact.ArNSAddress} browser.tabs.create({ url: `https://${contact.ArNSAddress}.arweave.ar` @@ -255,7 +261,7 @@ export default function ContactSettings({ address }: Props) { } removeContactModal.setOpen(false); - setLocation("/contacts"); + setLocation(`/${isQuickSetting ? "quick-settings/" : ""}contacts`); }; const copyAddress: MouseEventHandler = (e) => { @@ -286,30 +292,34 @@ export default function ContactSettings({ address }: Props) { return (
-
- -
- {browser.i18n.getMessage("contact_info")} - -
-
- {browser.i18n.getMessage("contact_avatar")} + {!isQuickSetting && ( +
+ +
+ {browser.i18n.getMessage("contact_info")} + +
+
+ )} + + {browser.i18n.getMessage("contact_avatar")} + {contact.avatarId && !avatarLoading ? ( - + ) : ( avatarLoading && contact.avatarId && ( - + ) )} {!contact.profileIcon && svgieAvatar && ( - + )} {!contact.profileIcon && !svgieAvatar && ( - + {generateProfileIcon(contact.name, contact.address)} )} @@ -328,7 +338,11 @@ export default function ContactSettings({ address }: Props) { ) : null} - {contact.name && {browser.i18n.getMessage("name")}} + {contact.name && ( + + {browser.i18n.getMessage("name")} + + )} {editable ? ( ) : ( - {contact.name} + {contact.name} )} - + {browser.i18n.getMessage("arweave_account_address")} {editable && "*"} @@ -357,6 +371,7 @@ export default function ContactSettings({ address }: Props) { position="top" > @@ -378,15 +393,30 @@ export default function ContactSettings({ address }: Props) { /> ) : ( -
{contact.address}
+
+ {isQuickSetting + ? formatAddress(contact.address, 8) + : contact.address} +
)} {<>{renderArNSAddress()}} - {browser.i18n.getMessage("notes")} + + {browser.i18n.getMessage("notes")} + setContact({ ...contact, notes: e.target.value })} - style={{ height: editable ? "235px" : "269px" }} + style={{ + height: editable + ? isQuickSetting + ? "78px" + : "235px" + : isQuickSetting + ? "78px" + : "269px" + }} />
{editable && ( @@ -408,39 +438,54 @@ export default function ContactSettings({ address }: Props) { {browser.i18n.getMessage("remove_contact")}
- - removeContactModal.setOpen(false)} - > - {browser.i18n.getMessage("no")} - - - {browser.i18n.getMessage("yes")} - - - } - > - - {browser.i18n.getMessage("remove_contact")} - - - - {browser.i18n.getMessage("remove_contact_question")} - - - )} + {isQuickSetting && !editable && ( +
+ + {browser.i18n.getMessage("edit_contact")} + + removeContactModal.setOpen(true)} + displayTheme={theme} + > + {browser.i18n.getMessage("remove_contact")} + +
+ )} + + removeContactModal.setOpen(false)} + > + {browser.i18n.getMessage("no")} + + + {browser.i18n.getMessage("yes")} + + + } + > + + {browser.i18n.getMessage("remove_contact")} + + + + {browser.i18n.getMessage("remove_contact_question")} + + + ); } -const Action = styled(CopyIcon)` +const Action = styled(CopyIcon)<{ small?: boolean }>` font-size: 1.25rem; width: 1em; height: 1em; @@ -462,9 +507,11 @@ const LoadingSpin = styled(Loading)` width: 23px; `; -const Link = styled(Share04)` +const Link = styled(Share04)<{ small?: boolean }>` margin-left: 10px; cursor: pointer; + height: ${(props) => (props.small ? "0.875em" : "1em")}; + width: ${(props) => (props.small ? "0.875em" : "1em")}; `; export const Wrapper = styled.div` @@ -499,12 +546,13 @@ export const CenterText = styled(Text)` const Address = styled(Text).attrs({ heading: true -})` +})<{ small?: boolean }>` margin-bottom: 20px; font-weight: 500; display: flex; align-items: center; word-break: break-all; + ${(props) => props.small && "font-size: 1rem;"} `; const AddressWrapper = styled.div` @@ -540,9 +588,9 @@ export const RemoveContact = styled(ButtonV2)<{ displayTheme: DisplayTheme }>` } `; -export const AutoContactPic = styled.div` - width: 100px; - height: 100px; +export const AutoContactPic = styled.div<{ small?: boolean }>` + width: ${(props) => (props.small ? "64px" : "100px")}; + height: ${(props) => (props.small ? "64px" : "100px")}; border-radius: 100%; margin-bottom: 10px; display: flex; @@ -553,9 +601,9 @@ export const AutoContactPic = styled.div` background-color: #ab9aff26; `; -export const ContactPic = styled.img` - width: 100px; - height: 100px; +export const ContactPic = styled.img<{ small?: boolean }>` + width: ${(props) => (props.small ? "64px" : "100px")}; + height: ${(props) => (props.small ? "64px" : "100px")}; border-radius: 100%; margin-bottom: 10px; `; @@ -565,7 +613,7 @@ export const InputWrapper = styled.div` `; export const SelectInput = styled(SelectV2)` - height: 53px; + ${(props) => !props.small && "height: 53px;"} padding: 10px 20px 10px 20px; color: #b9b9b9; font-size: 16px; @@ -576,7 +624,7 @@ export const SelectInput = styled(SelectV2)` `; export const ContactInput = styled(InputV2)` - height: 33px; + ${(props) => !props.small && "height: 33px;"} padding: 10px 20px 10px 20px; color: #b9b9b9; font-size: 16px; @@ -588,16 +636,17 @@ export const ContactInput = styled(InputV2)` const ContactInfo = styled(Text).attrs({ heading: true -})` +})<{ small?: boolean }>` margin-bottom: 20px; font-weight: 500; + ${(props) => props.small && "font-size: 1rem;"} display: flex; align-items: center; `; -export const ContactNotes = styled.textarea` +export const ContactNotes = styled.textarea<{ small?: boolean }>` display: flex; - width: 96%; + width: ${(props) => (props.small ? "92%" : "96%")}; border-radius: 15px; border: 1.5px solid #ab9aff26; padding: 12px; @@ -614,9 +663,14 @@ export const ContactNotes = styled.textarea` color: #b9b9b9; `; -export const SubTitle = styled(Text)` - font-size: 16px; - color: #aeadcd; +export const SubTitle = styled(Text)<{ small?: boolean; color?: string }>` + font-size: ${(props) => (props.small ? "14px" : "16px")}; + color: rgb( + ${(props) => + props.color === "primary" + ? props.theme.primaryText + : props.theme.secondaryText} + ); margin-bottom: 4px; `; @@ -629,4 +683,5 @@ export const Title = styled(Text).attrs({ interface Props { address: string; + isQuickSetting?: boolean; } diff --git a/src/popup.tsx b/src/popup.tsx index f952564ab..e55123c03 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -44,6 +44,9 @@ import AppPermissions from "~routes/popup/settings/apps/[url]/permissions"; import { default as QuickTokens } from "~routes/popup/settings/tokens"; import TokenSettings from "~routes/popup/settings/tokens/[id]"; import NewToken from "~routes/popup/settings/tokens/new"; +import Contacts from "~routes/popup/settings/contacts"; +import ContactSettings from "~routes/popup/settings/contacts/[address]"; +import NewContact from "~routes/popup/settings/contacts/new"; export default function Popup() { const theme = useTheme(); @@ -121,6 +124,16 @@ export default function Popup() { )} + + + {(params: { address: string }) => ( + + )} + + {(params: { id: string }) => ( diff --git a/src/routes/popup/settings/contacts/[address]/index.tsx b/src/routes/popup/settings/contacts/[address]/index.tsx new file mode 100644 index 000000000..e55f0e832 --- /dev/null +++ b/src/routes/popup/settings/contacts/[address]/index.tsx @@ -0,0 +1,27 @@ +import HeadV2 from "~components/popup/HeadV2"; +import browser from "webextension-polyfill"; +import { default as ContactSettingsComponent } from "~components/dashboard/subsettings/ContactSettings"; +import styled from "styled-components"; + +interface ContactSettingsProps { + address: string; +} + +export default function ContactSettings({ address }: ContactSettingsProps) { + return ( + <> + + + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 80px); +`; diff --git a/src/routes/popup/settings/contacts/index.tsx b/src/routes/popup/settings/contacts/index.tsx new file mode 100644 index 000000000..646526cf9 --- /dev/null +++ b/src/routes/popup/settings/contacts/index.tsx @@ -0,0 +1,23 @@ +import HeadV2 from "~components/popup/HeadV2"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import { default as ContactsComponent } from "~components/dashboard/Contacts"; + +export default function Contacts() { + return ( + <> + + + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: 100%; +`; diff --git a/src/routes/popup/settings/contacts/new.tsx b/src/routes/popup/settings/contacts/new.tsx new file mode 100644 index 000000000..c5bab13ff --- /dev/null +++ b/src/routes/popup/settings/contacts/new.tsx @@ -0,0 +1,23 @@ +import HeadV2 from "~components/popup/HeadV2"; +import browser from "webextension-polyfill"; +import AddContact from "~components/dashboard/subsettings/AddContact"; +import styled from "styled-components"; + +export default function NewContact() { + return ( + <> + + + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; + height: calc(100vh - 80px); +`; From 09bc496ef3bc775ab07e524ca2f61569a82b4c25 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Mon, 1 Jul 2024 23:06:05 +0545 Subject: [PATCH 07/31] refactor: integrate Ar.io ao process --- assets/_locales/en/messages.json | 4 + assets/_locales/zh_CN/messages.json | 4 + package.json | 3 +- src/components/Recipient.tsx | 10 +- .../dashboard/subsettings/AddContact.tsx | 21 +- .../dashboard/subsettings/ContactSettings.tsx | 21 +- src/gateways/cache.ts | 12 +- src/gateways/gateway.ts | 3 - src/gateways/types.ts | 4 +- src/lib/ao.ts | 199 ++++++++++++++++++ src/lib/arns.ts | 153 +++++++++++--- yarn.lock | 39 +++- 12 files changed, 417 insertions(+), 56 deletions(-) create mode 100644 src/lib/ao.ts diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 619a7a38a..14bf8a203 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1689,6 +1689,10 @@ "message": "No ArNS address found", "description": "No ArNS address found text" }, + "searching_ArNS_addresses": { + "message": "Searching ArNS addresses...", + "description": "Search ArNS addressess text" + }, "arweave_url": { "message": "ar://", "description": "Arweave address for ArNS names" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 9d01a8931..2f5df218b 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1686,6 +1686,10 @@ "message": "未找到 ArNS 地址", "description": "No ArNS address found text" }, + "searching_ArNS_addresses": { + "message": "正在搜索 ArNS 地址...", + "description": "Search ArNS addressess text" + }, "arweave_url": { "message": "ar://", "description": "Arweave address for ArNS names" diff --git a/package.json b/package.json index df5e90c94..2a83be034 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@keystonehq/arweave-keyring": "^0.1.1-alpha.0", "@keystonehq/bc-ur-registry-arweave": "^0.1.1-alpha.0", "@ngraveio/bc-ur": "^1.1.6", - "@permaweb/aoconnect": "^0.0.43", + "@permaweb/aoconnect": "^0.0.55", "@plasmohq/storage": "^1.7.2", "@segment/analytics-next": "^1.53.2", "@untitled-ui/icons-react": "^0.1.1", @@ -75,6 +75,7 @@ "mitt": "^3.0.0", "nanoid": "^4.0.0", "path-to-regexp": "^6.2.1", + "plimit-lit": "^3.0.1", "pretty-bytes": "^6.0.0", "qrcode.react": "^3.1.0", "react": "18.2.0", diff --git a/src/components/Recipient.tsx b/src/components/Recipient.tsx index 95e7ec988..11b970e82 100644 --- a/src/components/Recipient.tsx +++ b/src/components/Recipient.tsx @@ -55,6 +55,7 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { const [show, setShow] = useState(true); const { lastRecipients, storedContacts } = useContacts(activeAddress); + const [loading, setLoading] = useState(false); const { setToast } = useToasts(); const possibleTargets = useMemo(() => { @@ -102,8 +103,9 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { } if (targetInput.state.startsWith("ar://")) search = targetInput.state.substring(5); + setLoading(true); const result = await searchArNSName(search); - if (!result.success) { + if (result.success) { onClick({ address: result.record.owner }); onClose(); setToast({ @@ -119,7 +121,10 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { content: browser.i18n.getMessage("check_address"), duration: 2400 }); - } catch {} + } catch { + } finally { + setLoading(false); + } }; const filteredAndGroupedContacts = useMemo(() => { @@ -167,6 +172,7 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { /> { submit(); }} diff --git a/src/components/dashboard/subsettings/AddContact.tsx b/src/components/dashboard/subsettings/AddContact.tsx index 0348edae6..1a87e951a 100644 --- a/src/components/dashboard/subsettings/AddContact.tsx +++ b/src/components/dashboard/subsettings/AddContact.tsx @@ -34,6 +34,7 @@ import { useLocation } from "wouter"; import copy from "copy-to-clipboard"; import { gql } from "~gateways/api"; import { useTheme } from "~utils/theme"; +import { isAddressFormat } from "~utils/format"; export default function AddContact() { // contacts @@ -55,6 +56,7 @@ export default function AddContact() { const { setToast } = useToasts(); const [location] = useLocation(); const address = location.split("=")[1]; + const [loading, setLoading] = useState(false); const [contact, setContact] = useState({ name: "", @@ -138,15 +140,20 @@ export default function AddContact() { async function fetchArnsAddresses(ownerAddress) { try { + setLoading(true); const arnsNames = await getAllArNSNames(ownerAddress); - setArnsResults(arnsNames.records || []); + setArnsResults(arnsNames || []); } catch (error) { console.error("Error fetching ArNS addresses:", error); + } finally { + setLoading(false); } } useEffect(() => { - fetchArnsAddresses(contact.address); + if (contact.address && isAddressFormat(contact.address)) { + fetchArnsAddresses(contact.address); + } (async () => { if (!activeAddress) return; @@ -324,13 +331,15 @@ export default function AddContact() { } > - {Object.entries(arnsResults).map(([contractTxId]) => ( - ))} diff --git a/src/components/dashboard/subsettings/ContactSettings.tsx b/src/components/dashboard/subsettings/ContactSettings.tsx index b08f9ac07..51fe2c53f 100644 --- a/src/components/dashboard/subsettings/ContactSettings.tsx +++ b/src/components/dashboard/subsettings/ContactSettings.tsx @@ -25,6 +25,7 @@ import styled from "styled-components"; import { svgie } from "~utils/svgies"; import { useLocation } from "wouter"; import copy from "copy-to-clipboard"; +import { isAddressFormat } from "~utils/format"; export default function ContactSettings({ address }: Props) { // contacts @@ -53,6 +54,7 @@ export default function ContactSettings({ address }: Props) { const [avatarLoading, setAvatarLoading] = useState(false); const [copied, setCopied] = useState(false); const [originalContact, setOriginalContact] = useState(null); + const [loading, setLoading] = useState(false); const svgieAvatar = useMemo(() => { if (!contact.address || contact.avatarId) { @@ -66,7 +68,9 @@ export default function ContactSettings({ address }: Props) { if (loadedContact) { setContact(loadedContact); setContactIndex(storedContacts.indexOf(loadedContact)); - fetchArnsAddresses(loadedContact.address); + if (loadedContact.address && isAddressFormat(loadedContact.address)) { + fetchArnsAddresses(loadedContact.address); + } } else { setContact({ name: "", @@ -82,10 +86,13 @@ export default function ContactSettings({ address }: Props) { async function fetchArnsAddresses(ownerAddress) { try { + setLoading(true); const arnsNames = await getAllArNSNames(ownerAddress); - setArnsResults(arnsNames.records || []); + setArnsResults(arnsNames || []); } catch (error) { console.error("Error fetching ArNS addresses:", error); + } finally { + setLoading(false); } } @@ -205,13 +212,15 @@ export default function ContactSettings({ address }: Props) { } > - {Object.entries(arnsResults).map(([contractTxId]) => ( - ))} diff --git a/src/gateways/cache.ts b/src/gateways/cache.ts index 48010174b..266837712 100644 --- a/src/gateways/cache.ts +++ b/src/gateways/cache.ts @@ -1,8 +1,9 @@ import { extractGarItems, pingUpdater } from "~lib/wayfinder"; import browser, { type Alarms } from "webextension-polyfill"; -import { defaultGARCacheURL } from "./gateway"; -import type { ProcessedData } from "./types"; +import type { GatewayAddressRegistryCache, ProcessedData } from "./types"; import { ExtensionStorage } from "~utils/storage"; +import { AOProcess } from "~lib/ao"; +import { AO_ARNS_PROCESS } from "~lib/arns"; /** Cache storage name */ const CACHE_STORAGE_NAME = "gateways"; @@ -53,8 +54,11 @@ export async function handleGatewayUpdate(alarm?: Alarms.Alarm) { try { // fetch cache - const data = await (await fetch(defaultGARCacheURL)).json(); - const garItems = extractGarItems(data); + const ArIO = new AOProcess({ processId: AO_ARNS_PROCESS }); + const gateways = (await ArIO.read({ + tags: [{ name: "Action", value: "Gateways" }] + })) as GatewayAddressRegistryCache["gateways"]; + const garItems = extractGarItems({ gateways }); // healtcheck await pingUpdater(garItems, (newData) => { diff --git a/src/gateways/gateway.ts b/src/gateways/gateway.ts index 1078af973..183b6f755 100644 --- a/src/gateways/gateway.ts +++ b/src/gateways/gateway.ts @@ -4,9 +4,6 @@ export interface Gateway { protocol: string; } -export const defaultGARCacheURL = - "https://dev.arns.app/v1/contract/bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U/gateways"; - /** * Well-known gateways */ diff --git a/src/gateways/types.ts b/src/gateways/types.ts index 268cf8e7f..0ed45d016 100644 --- a/src/gateways/types.ts +++ b/src/gateways/types.ts @@ -24,9 +24,9 @@ export interface GatewayAddressRegistryItem } export interface GatewayAddressRegistryCache { - contractTxId: string; + // contractTxId: string; gateways: Record; - evaluationOptions: {}; + // evaluationOptions: {}; } interface GatewayVault { diff --git a/src/lib/ao.ts b/src/lib/ao.ts new file mode 100644 index 000000000..b80a2f91d --- /dev/null +++ b/src/lib/ao.ts @@ -0,0 +1,199 @@ +import { connect } from "@permaweb/aoconnect"; +import { ArweaveSigner, createData } from "arbundles"; +import type { JWKInterface } from "arweave/web/lib/wallet"; +import { defaultConfig } from "~tokens/aoTokens/config"; + +export const joinUrl = ({ url, path }: { url: string; path: string }) => { + if (!path) return url; + + // Create a URL object + const urlObj = new URL(url); + + // Remove leading slash from path if present + const normalizedPath = path.startsWith("/") ? path.slice(1) : path; + + // Ensure the URL object's pathname ends with a slash if it's not empty + urlObj.pathname = urlObj.pathname.replace(/\/?$/, "/"); + + // Join the normalized path + urlObj.pathname += normalizedPath; + + return urlObj.toString(); +}; + +export class AOProcess { + private processId: string; + private ao: { + result: any; + results: any; + message: any; + spawn: any; + monitor: any; + unmonitor: any; + dryrun: any; + assign: any; + }; + + constructor({ + processId, + connectionConfig + }: { + processId: string; + connectionConfig?: { + CU_URL: string; + MU_URL: string; + GATEWAY_URL: string; + GRAPHQL_URL: string; + }; + }) { + this.processId = processId; + this.ao = connect({ + GRAPHQL_URL: + connectionConfig?.GATEWAY_URL ?? + joinUrl({ url: defaultConfig.GATEWAY_URL, path: "graphql" }), + CU_URL: connectionConfig?.GATEWAY_URL ?? defaultConfig.CU_URL, + MU_URL: connectionConfig?.MU_URL ?? defaultConfig.MU_URL, + GATEWAY_URL: connectionConfig?.GATEWAY_URL ?? defaultConfig.GATEWAY_URL + }); + } + + async createAoSigner( + wallet: JWKInterface + ): Promise< + (args: { + data: string | Buffer; + tags?: { name: string; value: string }[]; + target?: string; + anchor?: string; + }) => Promise<{ id: string; raw: ArrayBuffer }> + > { + const aoSigner = async ({ data, tags, target, anchor }) => { + const signer = new ArweaveSigner(wallet); + const dataItem = createData(data, signer, { tags, target, anchor }); + + await dataItem.sign(signer); + + return { + id: dataItem.id, + raw: dataItem.getRaw() + }; + }; + + return aoSigner; + } + + async read({ + tags, + retries = 3 + }: { + tags?: Array<{ name: string; value: string }>; + retries?: number; + }): Promise { + let attempts = 0; + let lastError: Error | undefined; + while (attempts < retries) { + try { + console.debug(`Evaluating read interaction on contract`, { + tags + }); + // map tags to inputs + const result = await this.ao.dryrun({ + process: this.processId, + tags + }); + + if (result.Error !== undefined) { + throw new Error(result.Error); + } + + if (result.Messages.length === 0) { + throw new Error("Process does not support provided action."); + } + + console.debug(`Read interaction result`, { + result: result.Messages[0].Data + }); + + const data: K = JSON.parse(result.Messages[0].Data); + return data; + } catch (e) { + attempts++; + console.debug(`Read attempt ${attempts} failed`, { + error: e, + tags + }); + lastError = e; + // exponential backoff + await new Promise((resolve) => + setTimeout(resolve, 2 ** attempts * 1000) + ); + } + } + throw lastError; + } + + async send({ + tags, + data, + wallet + }: { + tags: Array<{ name: string; value: string }>; + data?: I; + wallet: JWKInterface; + }): Promise<{ id: string; result?: K }> { + console.debug(`Evaluating send interaction on contract`, { + tags, + data, + processId: this.processId + }); + + // append ar-io-sdk tags + + const messageId = await this.ao.message({ + process: this.processId, + tags: [...tags], + data: JSON.stringify(data), + signer: await this.createAoSigner(wallet) + }); + + console.debug(`Sent message to process`, { + messageId, + processId: this.processId + }); + + // check the result of the send interaction + const output = await this.ao.result({ + message: messageId, + process: this.processId + }); + + console.debug("Message result", { + output, + messageId, + processId: this.processId + }); + + // check if there are any Messages in the output + if (output.Messages.length === 0) { + return { id: messageId }; + } + + const tagsOutput = output.Messages[0].Tags; + const error = tagsOutput.find((tag) => tag.name === "Error"); + // if there's an Error tag, throw an error related to it + if (error) { + const result = output.Messages[0].Data; + throw new Error(`${error.Value}: ${result}`); + } + + const resultData: K = JSON.parse(output.Messages[0].Data); + + console.debug("Message result data", { + resultData, + messageId, + processId: this.processId + }); + + return { id: messageId, result: resultData }; + } +} diff --git a/src/lib/arns.ts b/src/lib/arns.ts index 8c665bc0c..3534ad564 100644 --- a/src/lib/arns.ts +++ b/src/lib/arns.ts @@ -1,41 +1,144 @@ -async function getANTsContractTxIds(owner) { - const response = await fetch( - `https://api.arns.app/v1/wallet/${owner}/contracts?type=ant` - ); - const data = await response.json(); - return data.contractTxIds; +import { pLimit } from "plimit-lit"; +import { AOProcess } from "./ao"; + +export const AO_ARNS_PROCESS = "agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA"; + +export type ProcessId = string; +export type WalletAddress = string; +export type RegistrationType = "lease" | "permabuy"; + +export type AoArNSBaseNameData = { + processId: ProcessId; + startTimestamp: number; + type: RegistrationType; + undernameLimit: number; + purchasePrice: number; +}; + +export type ANTRecord = { + transactionId: string; + ttlSeconds: number; +}; + +export type AoANTState = { + Name: string; + Ticker: string; + Denomination: number; + Owner: WalletAddress; + Controllers: WalletAddress[]; + Records: Record; + Balances: Record; + Logo: string; + TotalSupply: number; + Initialized: boolean; +}; + +export type AoArNSLeaseData = AoArNSBaseNameData & { + type: "lease"; + endTimestamp: number; // At what unix time (seconds since epoch) the lease ends +}; + +export type AoArNSPermabuyData = AoArNSBaseNameData & { + type: "permabuy"; +}; +export type AoArNSNameData = AoArNSPermabuyData | AoArNSLeaseData; +export type ANTInfo = { + Name: string; + Ticker: string; + Denomination: number; + Owner: string; +}; + +export async function getArNSRecord( + name: string +): Promise { + const ArIO = new AOProcess({ processId: AO_ARNS_PROCESS }); + const record = await ArIO.read({ + tags: [ + { name: "Action", value: "Record" }, + { name: "Name", value: name } + ] + }); + return record; } -const REGISTRY = "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U"; -export async function getAllArNSNames(owner) { - const contractTxIds = await getANTsContractTxIds(owner); - if (contractTxIds.length === 0) return []; +export async function getArNSRecords(): Promise< + Record +> { + const ArIO = new AOProcess({ processId: AO_ARNS_PROCESS }); + const record = await ArIO.read>({ + tags: [{ name: "Action", value: "Records" }] + }); + return record; +} - const url = [ - "https://api.arns.app/v1/contract/bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U/records?", - ...contractTxIds.map((txId) => `contractTxId=${txId}`) - ].join("&"); +export async function getANTInfo(processId: string): Promise { + const ant = new AOProcess({ processId }); + const tags = [{ name: "Action", value: "Info" }]; + const info = await ant.read({ tags }); + return info; +} - const response = await fetch(url); - const data = await response.json(); - return data; +export async function getANTState( + processId: string, + retries = 3 +): Promise { + const ant = new AOProcess({ processId }); + const tags = [{ name: "Action", value: "State" }]; + const res = await ant.read({ tags, retries }); + return res; } +export const getAllArNSNames = async ( + address: WalletAddress +): Promise => { + if (!address) return []; + + const throttle = pLimit(50); + + const arnsRecords = await getArNSRecords().then((records) => + Object.values(records).filter((record) => record.processId !== undefined) + ); + + // check the contract owner and controllers + const results = await Promise.all( + arnsRecords.map(async (record) => + throttle(async () => { + try { + const state = await getANTState(record.processId, 1); + + if (state.Owner === address || state.Controllers.includes(address)) { + return state.Name; + } + return; + } catch (err) { + return; + } + }) + ) + ); + + if (results.length === 0) { + return []; + } + + return Array.from(new Set(results.filter((result) => result))); +}; + export async function searchArNSName(name: string) { name = name.toLowerCase(); - const response = await fetch( - `https://api.arns.app/v1/contract/${REGISTRY}/records/${name}` - ); - if (response.status === 404) { + const record = await getArNSRecord(name); + + if (record?.processId) { + const info = await getANTInfo(record?.processId); + return { success: true, - record: null, - message: `${name} is not registered` + record: { ...record, owner: info.Owner } }; } return { success: false, - record: await response.json(), - message: `${name} is already registered` + record: null }; } diff --git a/yarn.lock b/yarn.lock index 4932c9c8f..99d7b4673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2590,24 +2590,25 @@ "@parcel/utils" "2.9.3" nullthrows "^1.1.1" -"@permaweb/ao-scheduler-utils@~0.0.7": +"@permaweb/ao-scheduler-utils@~0.0.16": version "0.0.19" - resolved "https://registry.yarnpkg.com/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.19.tgz#69d35c19583624ace3f500f53b4b4d73fca883e1" + resolved "https://registry.npmjs.org/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.19.tgz#69d35c19583624ace3f500f53b4b4d73fca883e1" integrity sha512-xwIe9FqQ1UZxEYWvSGJDONz0xr4vDq2Ny1NeRUiO0dKYoonShN+oI1ULgrHocKOjOPNEgRX70vMCKGLe+3x70A== dependencies: lru-cache "^10.2.2" ramda "^0.30.0" zod "^3.23.5" -"@permaweb/aoconnect@^0.0.43": - version "0.0.43" - resolved "https://registry.yarnpkg.com/@permaweb/aoconnect/-/aoconnect-0.0.43.tgz#765e058c22d8405e25f12117b3af87bc16bca58e" - integrity sha512-nGPhJw8i6HxTHEg2yU12S7dvU3OwPZ7tF7Od4tRZgefbbgVM+LmIJbqvsX6GW4gjl1zYbcOZGymo78BfXabyTQ== +"@permaweb/aoconnect@^0.0.55": + version "0.0.55" + resolved "https://registry.npmjs.org/@permaweb/aoconnect/-/aoconnect-0.0.55.tgz#d856a078d3702154ac58541d09478d25ed3acf2c" + integrity sha512-W2GtLZedVseuDkCKk4CmM9SFmi0DdrMKqvhMBm9xo65z+Mzr/t1TEjMJKRNzEA2qh5IdwM43sWJ5fmbBYLg6TQ== dependencies: - "@permaweb/ao-scheduler-utils" "~0.0.7" + "@permaweb/ao-scheduler-utils" "~0.0.16" buffer "^6.0.3" debug "^4.3.4" hyper-async "^1.1.2" + mnemonist "^0.39.8" ramda "^0.29.1" warp-arbundles "^1.0.4" zod "^3.22.4" @@ -8898,6 +8899,13 @@ mnemonic-id@3.2.7: resolved "https://registry.yarnpkg.com/mnemonic-id/-/mnemonic-id-3.2.7.tgz#f7d77d8b39e009ad068117cbafc458a6c6f8cddf" integrity sha512-kysx9gAGbvrzuFYxKkcRjnsg/NK61ovJOV4F1cHTRl9T5leg+bo6WI0pWIvOFh1Z/yDL0cjA5R3EEGPPLDv/XA== +mnemonist@^0.39.8: + version "0.39.8" + resolved "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz#9078cd8386081afd986cca34b52b5d84ea7a4d38" + integrity sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ== + dependencies: + obliterator "^2.0.1" + module-error@^1.0.1, module-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" @@ -9369,6 +9377,11 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" +obliterator@^2.0.1: + version "2.0.4" + resolved "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" + integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -9812,6 +9825,13 @@ plasmo@0.86.3: tempy "3.1.0" typescript "5.2.2" +plimit-lit@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/plimit-lit/-/plimit-lit-3.0.1.tgz#45a2aee1a7249aa9c2eafc67b6a27bc927e3aa39" + integrity sha512-EyTTdP5LMX6WbHihG8R9w6DS+c3pyMpeKooOFuGDCyuVBogQjYNtoYwKLRD6hM1+VkHzGcfIuyLoWi6l5JA3iA== + dependencies: + queue-lit "^3.0.0" + popmotion@11.0.5: version "11.0.5" resolved "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz#8e3e014421a0ffa30ecd722564fd2558954e1f7d" @@ -10081,6 +10101,11 @@ querystring-es3@^0.2.1: resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== +queue-lit@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/queue-lit/-/queue-lit-3.0.0.tgz#5062f815e49c28759a2dc12124ab1723d563b932" + integrity sha512-iyVL2X5G58kICVGLW/nseYmdHxBoAp2Gav16H23NPtIllyEJ+UheHlYZqBjO+lJHRYoZRSrX7chH8tMrH9MB/A== + queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" From e4772f2d47db2308b17e2cf1c7f32aa8ec3cf7f2 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Mon, 1 Jul 2024 23:20:55 +0545 Subject: [PATCH 08/31] refactor: Update AOProcess send --- src/lib/ao.ts | 151 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 46 deletions(-) diff --git a/src/lib/ao.ts b/src/lib/ao.ts index b80a2f91d..cfecb8e63 100644 --- a/src/lib/ao.ts +++ b/src/lib/ao.ts @@ -21,6 +21,23 @@ export const joinUrl = ({ url, path }: { url: string; path: string }) => { return urlObj.toString(); }; +export function safeDecode(data: any): R { + try { + return JSON.parse(data); + } catch (e) { + return data as R; + } +} + +export class BaseError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class WriteInteractionError extends BaseError {} + export class AOProcess { private processId: string; private ao: { @@ -57,7 +74,7 @@ export class AOProcess { }); } - async createAoSigner( + static async createAoSigner( wallet: JWKInterface ): Promise< (args: { @@ -135,65 +152,107 @@ export class AOProcess { async send({ tags, data, - wallet + wallet, + retries = 3 }: { tags: Array<{ name: string; value: string }>; data?: I; wallet: JWKInterface; + retries?: number; }): Promise<{ id: string; result?: K }> { - console.debug(`Evaluating send interaction on contract`, { - tags, - data, - processId: this.processId - }); + // main purpose of retries is to handle network errors/new process delays + let attempts = 0; + let lastError: Error | undefined; + while (attempts < retries) { + try { + console.debug(`Evaluating send interaction on contract`, { + tags, + data, + processId: this.processId + }); - // append ar-io-sdk tags + // TODO: do a read as a dry run to check if the process supports the action - const messageId = await this.ao.message({ - process: this.processId, - tags: [...tags], - data: JSON.stringify(data), - signer: await this.createAoSigner(wallet) - }); + const messageId = await this.ao.message({ + process: this.processId, + // TODO: any other default tags we want to add? + tags: [...tags], + data: typeof data !== "string" ? JSON.stringify(data) : data, + signer: await AOProcess.createAoSigner(wallet) + }); - console.debug(`Sent message to process`, { - messageId, - processId: this.processId - }); + console.debug(`Sent message to process`, { + messageId, + processId: this.processId + }); - // check the result of the send interaction - const output = await this.ao.result({ - message: messageId, - process: this.processId - }); + // check the result of the send interaction + const output = await this.ao.result({ + message: messageId, + process: this.processId + }); - console.debug("Message result", { - output, - messageId, - processId: this.processId - }); + console.debug("Message result", { + output, + messageId, + processId: this.processId + }); - // check if there are any Messages in the output - if (output.Messages.length === 0) { - return { id: messageId }; - } + // check if there are any Messages in the output + if (output.Messages?.length === 0 || output.Messages === undefined) { + return { id: messageId }; + } - const tagsOutput = output.Messages[0].Tags; - const error = tagsOutput.find((tag) => tag.name === "Error"); - // if there's an Error tag, throw an error related to it - if (error) { - const result = output.Messages[0].Data; - throw new Error(`${error.Value}: ${result}`); - } + const tagsOutput = output.Messages[0].Tags; + const error = tagsOutput.find((tag) => tag.name === "Error"); + // if there's an Error tag, throw an error related to it + if (error) { + const result = output.Messages[0].Data; + throw new WriteInteractionError(`${error.Value}: ${result}`); + } - const resultData: K = JSON.parse(output.Messages[0].Data); + if (output.Messages.length === 0) { + throw new Error( + `Process ${this.processId} does not support provided action.` + ); + } - console.debug("Message result data", { - resultData, - messageId, - processId: this.processId - }); + if (output.Messages[0].Data === undefined) { + return { id: messageId }; + } + + const resultData: K = safeDecode(output.Messages[0].Data); - return { id: messageId, result: resultData }; + console.debug("Message result data", { + resultData, + messageId, + processId: this.processId + }); + + return { id: messageId, result: resultData }; + } catch (error) { + console.error("Error sending message to process", { + error: error.message, + processId: this.processId, + tags + }); + // throw on write interaction errors. No point retrying write interactions, waste of gas. + if (error.message.includes("500")) { + console.debug("Retrying send interaction", { + attempts, + retries, + error: error.message, + processId: this.processId + }); + // exponential backoff + await new Promise((resolve) => + setTimeout(resolve, 2 ** attempts * 2000) + ); + attempts++; + lastError = error; + } else throw error; + } + } + throw lastError; } } From 6b618894039e34397ff2021919ad3f36977d9169 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 2 Jul 2024 13:10:22 +0545 Subject: [PATCH 09/31] feat: Add notifications quick settings --- assets/_locales/en/messages.json | 8 + assets/_locales/zh_CN/messages.json | 8 + src/popup.tsx | 5 + .../popup/settings/apps/[url]/index.tsx | 91 ++-------- .../popup/settings/notifications/index.tsx | 155 ++++++++++++++++++ 5 files changed, 190 insertions(+), 77 deletions(-) create mode 100644 src/routes/popup/settings/notifications/index.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 117fee6af..13c276e62 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1537,6 +1537,14 @@ "message": "Receive notifications instantly when a transaction processes.", "description": "Enable notifications paragraph" }, + "toggle_notifications": { + "message": "Toggle notifications", + "description": "Toggle notifications text" + }, + "toggle_notifications_decription": { + "message": "Toggle to receive alerts for new transactions in your wallet.", + "description": "Toggle notifications description text" + }, "get_started":{ "message": "Get Started", "description": "Get started title" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index d154e5960..42a3a62e0 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1534,6 +1534,14 @@ "message": "当交易处理时立即接收通知。", "description": "Enable notifications paragraph" }, + "toggle_notifications": { + "message": "切换通知", + "description": "Toggle notifications text" + }, + "toggle_notifications_decription": { + "message": "切换以接收钱包中新交易的提醒。", + "description": "Toggle notifications description text" + }, "get_started": { "message": "开始", "description": "Get started title" diff --git a/src/popup.tsx b/src/popup.tsx index e55123c03..034d0f33a 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -47,6 +47,7 @@ import NewToken from "~routes/popup/settings/tokens/new"; import Contacts from "~routes/popup/settings/contacts"; import ContactSettings from "~routes/popup/settings/contacts/[address]"; import NewContact from "~routes/popup/settings/contacts/new"; +import NotificationSettings from "~routes/popup/settings/notifications"; export default function Popup() { const theme = useTheme(); @@ -134,6 +135,10 @@ export default function Popup() { path="/quick-settings/contacts/new" component={NewContact} /> + {(params: { id: string }) => ( diff --git a/src/routes/popup/settings/apps/[url]/index.tsx b/src/routes/popup/settings/apps/[url]/index.tsx index 59834fe99..94f6ebfb9 100644 --- a/src/routes/popup/settings/apps/[url]/index.tsx +++ b/src/routes/popup/settings/apps/[url]/index.tsx @@ -24,6 +24,7 @@ import Arweave from "arweave"; import { defaultGateway, suggestedGateways, testnets } from "~gateways/gateway"; import HeadV2 from "~components/popup/HeadV2"; import { useLocation } from "wouter"; +import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; export default function AppSettings({ url }: Props) { // app settings @@ -118,23 +119,19 @@ export default function AppSettings({ url }: Props) { - - - updateSettings((val) => ({ - ...val, - allowance: { - ...defaultAllowance, - ...val.allowance, - enabled: !val.allowance.enabled - } - })) - } - /> - - + { + updateSettings((val) => ({ + ...val, + allowance: { + ...defaultAllowance, + ...val.allowance, + enabled: !val.allowance.enabled + } + })); + }} + />
@@ -399,66 +396,6 @@ const CenterText = styled(Text)` } `; -const ToggleSwitch = styled.label<{ height?: string; width?: string }>` - position: relative; - display: inline-block; - width: ${(props) => props.width || "60px"}; - height: ${(props) => props.height || "34px"}; - - input { - opacity: 0; - width: 0; - height: 0; - } - - .slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - transition: 0.4s; - - &:before { - position: absolute; - content: ""; - height: calc(${(props) => props.height || "34px"} - 8px); - width: calc(${(props) => props.height || "34px"} - 8px); - left: 4px; - bottom: 4px; - background-color: white; - transition: 0.4s; - } - } - - input:checked + .slider { - background-color: ${(props) => props.theme.primary}; - } - - input:focus + .slider { - box-shadow: 0 0 1px ${(props) => props.theme.primary}; - } - - input:checked + .slider:before { - transform: translateX( - calc( - ${(props) => props.width || "60px"} - - ${(props) => props.height || "34px"} - ) - ); - } - - .slider.round { - border-radius: 34px; - - &:before { - border-radius: 50%; - } - } -`; - const Flex = styled.div<{ alignItems: string; justifyContent: string }>` display: flex; align-items: ${(props) => props.alignItems}; diff --git a/src/routes/popup/settings/notifications/index.tsx b/src/routes/popup/settings/notifications/index.tsx new file mode 100644 index 000000000..d9829a264 --- /dev/null +++ b/src/routes/popup/settings/notifications/index.tsx @@ -0,0 +1,155 @@ +import { useStorage } from "@plasmohq/storage/hook"; +import styled from "styled-components"; +import { ExtensionStorage } from "~utils/storage"; +import { Spacer, Text, TooltipV2 } from "@arconnect/components"; +import browser from "webextension-polyfill"; +import { + RadioWrapper, + RadioItem, + Radio, + RadioInner +} from "~components/dashboard/Setting"; +import HeadV2 from "~components/popup/HeadV2"; +import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; +import { InformationIcon } from "@iconicicons/react"; + +export default function NotificationSettings() { + const [notificationSettings, setNotificationSettings] = useStorage( + { + key: "setting_notifications", + instance: ExtensionStorage + }, + false + ); + const [notificationCustomizeSettings, setNotificationCustomizeSettings] = + useStorage( + { + key: "setting_notifications_customize", + instance: ExtensionStorage + }, + ["default"] + ); + + const toggleNotificationSetting = () => { + setNotificationSettings(!notificationSettings); + }; + + const handleRadioChange = (setting) => { + setNotificationCustomizeSettings([setting]); + }; + + return ( + <> + + + + + + {browser.i18n.getMessage("toggle_notifications")} + + + {browser.i18n.getMessage("toggle_notifications_decription")} +
+ } + position="bottom" + > + + + + + + + + {/* AR AND AO TRANSFER NOTIFICATIONS */} + handleRadioChange("default")} + > + + {notificationCustomizeSettings && + notificationCustomizeSettings.includes("default") && ( + + )} + + + Enable Arweave and ao Transaction Notifications + + + {/* JUST AR TRANSFER NOTIFICATIONS */} + handleRadioChange("arTransferNotifications")} + > + + {notificationCustomizeSettings && + notificationCustomizeSettings.includes( + "arTransferNotifications" + ) && } + + + Enable Arweave Transaction Notifications + + + {/* ALL NOTIFICATIONS */} + handleRadioChange("allTxns")} + > + + {notificationCustomizeSettings && + notificationCustomizeSettings.includes("allTxns") && ( + + )} + + + Enable all Arweave and ao Notifications + + + + + + ); +} + +const Wrapper = styled.div` + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 1rem; +`; + +const ToggleSwitchWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Title = styled(Text)` + color: rgb(${(props) => props.theme.primaryText}); +`; + +const TitleWrapper = styled.div` + display: flex; + gap: 4px; +`; + +const RadioText = styled(Text)` + font-size: 0.75rem; + color: rgb(${(props) => props.theme.primaryText}); +`; + +const InfoIcon = styled(InformationIcon)` + color: ${(props) => props.theme.secondaryTextv2}; +`; From cbe9134b63520ba6b8609257c674a40a4d6704d1 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 2 Jul 2024 14:11:29 +0545 Subject: [PATCH 10/31] fix: Update notifications settings checkbox --- src/components/Checkbox.tsx | 68 +++++++++++++++++++ src/components/popup/WalletHeader.tsx | 8 --- .../popup/settings/notifications/index.tsx | 46 +++++++------ 3 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 src/components/Checkbox.tsx diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx new file mode 100644 index 000000000..0de61402c --- /dev/null +++ b/src/components/Checkbox.tsx @@ -0,0 +1,68 @@ +import type { Dispatch, SetStateAction } from "react"; +import styled from "styled-components"; + +const Round = styled.div` + position: relative; +`; + +const CheckboxInput = styled.input.attrs({ type: "checkbox" })` + visibility: hidden; + + &:checked + label { + background-color: #8e7bea; + border-color: #8e7bea; + } + + &:checked + label:after { + opacity: 1; + } +`; + +const Label = styled.label<{ size: number }>` + background-color: transparent; + border: 1.875px solid #8e7bea; + border-radius: 50%; + cursor: pointer; + height: ${(props) => props.size}px; + left: 0; + position: absolute; + top: 0; + width: ${(props) => props.size}px; + + &:after { + border: 1.5px solid #fff; + border-top: none; + border-right: none; + content: ""; + height: ${(props) => props.size / 3.4}px; + left: ${(props) => props.size / 7}px; + opacity: 0; + position: absolute; + top: ${(props) => props.size / 4}px; + transform: rotate(-45deg); + width: ${(props) => props.size / 1.7}px; + } +`; + +export const Checkbox = ({ + checked, + setChecked, + size = 28 +}: { + checked: boolean; + setChecked: Dispatch>; + size?: number; +}) => { + const handleChange = () => { + setChecked(!checked); + }; + + return ( + + + + + ); +}; + +export default Checkbox; diff --git a/src/components/popup/WalletHeader.tsx b/src/components/popup/WalletHeader.tsx index 3018ffe61..3828ea932 100644 --- a/src/components/popup/WalletHeader.tsx +++ b/src/components/popup/WalletHeader.tsx @@ -435,14 +435,6 @@ export default function WalletHeader() { push("/subscriptions"); } }, - { - icon: , - title: "Settings", - route: () => - browser.tabs.create({ - url: browser.runtime.getURL("tabs/dashboard.html") - }) - }, { icon: , title: "Settings", diff --git a/src/routes/popup/settings/notifications/index.tsx b/src/routes/popup/settings/notifications/index.tsx index d9829a264..cbbb413a4 100644 --- a/src/routes/popup/settings/notifications/index.tsx +++ b/src/routes/popup/settings/notifications/index.tsx @@ -6,12 +6,12 @@ import browser from "webextension-polyfill"; import { RadioWrapper, RadioItem, - Radio, RadioInner } from "~components/dashboard/Setting"; import HeadV2 from "~components/popup/HeadV2"; import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; import { InformationIcon } from "@iconicicons/react"; +import Checkbox from "~components/Checkbox"; export default function NotificationSettings() { const [notificationSettings, setNotificationSettings] = useStorage( @@ -76,12 +76,14 @@ export default function NotificationSettings() { style={{ padding: 0 }} onClick={() => handleRadioChange("default")} > - - {notificationCustomizeSettings && - notificationCustomizeSettings.includes("default") && ( - - )} - + handleRadioChange("default")} + /> Enable Arweave and ao Transaction Notifications @@ -89,14 +91,18 @@ export default function NotificationSettings() { {/* JUST AR TRANSFER NOTIFICATIONS */} handleRadioChange("arTransferNotifications")} + onClick={(_) => handleRadioChange("arTransferNotifications")} > - - {notificationCustomizeSettings && + } - + ) + } + setChecked={(_) => handleRadioChange("arTransferNotifications")} + /> Enable Arweave Transaction Notifications @@ -104,14 +110,16 @@ export default function NotificationSettings() { {/* ALL NOTIFICATIONS */} handleRadioChange("allTxns")} + onClick={(_) => handleRadioChange("allTxns")} > - - {notificationCustomizeSettings && - notificationCustomizeSettings.includes("allTxns") && ( - - )} - + handleRadioChange("allTxns")} + /> Enable all Arweave and ao Notifications From 2ef577a88295a2975a6a2209dadad212f6a835bd Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 2 Jul 2024 15:51:04 +0545 Subject: [PATCH 11/31] refactor: Update settings to show general/advanced with search --- assets/_locales/en/messages.json | 20 ++++ assets/_locales/zh_CN/messages.json | 20 ++++ src/routes/dashboard/index.tsx | 157 ++++++++++++++++++++++------ 3 files changed, 163 insertions(+), 34 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 13c276e62..881a64d6b 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -29,6 +29,26 @@ "message": "Accept", "description": "Accept terms" }, + "search": { + "message": "Search", + "description": "Search text" + }, + "general": { + "message": "General", + "description": "General text" + }, + "advanced": { + "message": "Advanced", + "description": "Advanced text" + }, + "show": { + "message": "Show", + "description": "Show text" + }, + "hide": { + "message": "Hide", + "description": "Hide text" + }, "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 42a3a62e0..8c23f0d29 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -29,6 +29,26 @@ "message": "接受", "description": "Accept terms" }, + "search": { + "message": "搜索", + "description": "Search text" + }, + "general": { + "message": "普通", + "description": "General text" + }, + "advanced": { + "message": "高级", + "description": "Advanced text" + }, + "show": { + "message": "显示", + "description": "Show text" + }, + "hide": { + "message": "隐藏", + "description": "Hide text" + }, "add_wallet": { "message": "添加钱包", "description": "Add a wallet text" diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 9a33f3f0e..88afeb134 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -1,4 +1,4 @@ -import { Card, Spacer, Text } from "@arconnect/components"; +import { Card, Spacer, Text, useInput } from "@arconnect/components"; import SettingListItem, { type Props as SettingItemData } from "~components/dashboard/list/SettingListItem"; @@ -6,7 +6,7 @@ import { setting_element_padding, SettingsList } from "~components/dashboard/list/BaseElement"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useLocation } from "wouter"; import { GridIcon, @@ -15,7 +15,12 @@ import { WalletIcon, BellIcon } from "@iconicicons/react"; -import { Coins04, Users01 } from "@untitled-ui/icons-react"; +import { + Coins04, + Users01, + ChevronUp, + ChevronDown +} from "@untitled-ui/icons-react"; import WalletSettings from "~components/dashboard/subsettings/WalletSettings"; import TokenSettings from "~components/dashboard/subsettings/TokenSettings"; import AppSettings from "~components/dashboard/subsettings/AppSettings"; @@ -32,16 +37,22 @@ import About from "~components/dashboard/About"; import Reset from "~components/dashboard/Reset"; import browser from "webextension-polyfill"; import styled from "styled-components"; -import settings from "~settings"; +import settings, { getSetting } from "~settings"; import { PageType, trackPage } from "~utils/analytics"; import SignSettings from "~components/dashboard/SignSettings"; import AddToken from "~components/dashboard/subsettings/AddToken"; import NotificationSettings from "~components/dashboard/NotificationSettings"; +import SearchInput from "~components/dashboard/SearchInput"; export default function Settings({ params }: Props) { // router location const [, setLocation] = useLocation(); + const [showAdvanced, setShowAdvanced] = useState(false); + + // search + const searchInput = useInput(); + // active setting val const activeSetting = useMemo(() => params.setting, [params.setting]); @@ -67,6 +78,21 @@ export default function Settings({ params }: Props) { return new Application(decodeURIComponent(activeSubSetting)); }, [activeSubSetting]); + // search filter function + function filterSearchResults(setting: Omit) { + const query = searchInput.state; + + if (query === "" || !query) { + return true; + } + + return ( + setting.name.toLowerCase().includes(query.toLowerCase()) || + setting.displayName.toLowerCase().includes(query.toLowerCase()) || + setting.description.toLowerCase().includes(query.toLowerCase()) + ); + } + // redirect to the first setting // if none is selected useEffect(() => { @@ -85,17 +111,49 @@ export default function Settings({ params }: Props) { {browser.i18n.getMessage("settings")} + + + {browser.i18n.getMessage("general")} + - {allSettings.map((setting, i) => ( + {basicSettings.filter(filterSearchResults).map((setting, i) => ( setLocation("/" + setting.name)} - key={i} + key={`basic-settings-${i}`} /> ))} + + {browser.i18n.getMessage("advanced")} +
setShowAdvanced((prev) => !prev)} + style={{ display: "flex", cursor: "pointer" }} + > + + {browser.i18n.getMessage(showAdvanced ? "hide" : "show")} + + +
+
+ {showAdvanced && + advancedSettings + .filter(filterSearchResults) + .map((setting, i) => ( + setLocation("/" + setting.name)} + key={`advanced-settings-${i}`} + /> + ))}
props.theme.secondaryText}); + transition: all 0.23s ease-in-out; + + &:hover { + opacity: 0.85; + } + + &:active { + transform: scale(0.92); + } +`; + const isMac = () => { const userAgent = navigator.userAgent; @@ -262,14 +343,7 @@ interface Props { }; } -const allSettings: Omit[] = [ - { - name: "apps", - displayName: "setting_apps", - description: "setting_apps_description", - icon: GridIcon, - component: Applications - }, +const basicSettings: Omit[] = [ { name: "wallets", displayName: "setting_wallets", @@ -277,6 +351,13 @@ const allSettings: Omit[] = [ icon: WalletIcon, component: Wallets }, + { + name: "apps", + displayName: "setting_apps", + description: "setting_apps_description", + icon: GridIcon, + component: Applications + }, { name: "tokens", displayName: "setting_tokens", @@ -291,13 +372,6 @@ const allSettings: Omit[] = [ icon: Users01, component: Contacts }, - { - name: "sign_settings", - displayName: "setting_sign_settings", - description: "setting_sign_notification_description", - icon: BellIcon, - component: SignSettings - }, { name: "notifications", displayName: "setting_notifications", @@ -305,26 +379,39 @@ const allSettings: Omit[] = [ icon: BellIcon, component: NotificationSettings }, - ...settings.map((setting) => ({ - name: setting.name, - displayName: setting.displayName, - description: setting.description, - icon: setting.icon - })), - // TODO - /*{ - name: "config", - displayName: "setting_config", - description: "setting_config_description", - icon: DownloadIcon - },*/ + getSetting("display_theme") as Omit, { name: "about", displayName: "setting_about", description: "setting_about_description", icon: InformationIcon, component: About + } +]; + +const advancedSettings: Omit[] = [ + { + name: "sign_settings", + displayName: "setting_sign_settings", + description: "setting_sign_notification_description", + icon: BellIcon, + component: SignSettings }, + ...settings + .filter((setting) => setting.name !== "display_theme") + .map((setting) => ({ + name: setting.name, + displayName: setting.displayName, + description: setting.description, + icon: setting.icon + })), + // TODO + /*{ + name: "config", + displayName: "setting_config", + description: "setting_config_description", + icon: DownloadIcon + },*/ { name: "reset", displayName: "setting_reset", @@ -333,3 +420,5 @@ const allSettings: Omit[] = [ component: Reset } ]; + +const allSettings = [...basicSettings, ...advancedSettings]; From af535a4edbf694fa27677650a21656439ad47164 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 2 Jul 2024 17:02:15 +0545 Subject: [PATCH 12/31] fix: Update settings search filter function --- src/routes/dashboard/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 88afeb134..bf517385c 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -88,8 +88,14 @@ export default function Settings({ params }: Props) { return ( setting.name.toLowerCase().includes(query.toLowerCase()) || - setting.displayName.toLowerCase().includes(query.toLowerCase()) || - setting.description.toLowerCase().includes(query.toLowerCase()) + browser.i18n + .getMessage(setting.displayName) + .toLowerCase() + .includes(query.toLowerCase()) || + browser.i18n + .getMessage(setting.description) + .toLowerCase() + .includes(query.toLowerCase()) ); } From c8234905015db069446e4b7f492029755dae505a Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 2 Jul 2024 21:16:20 +0545 Subject: [PATCH 13/31] fix: Update checkbox --- src/components/Checkbox.tsx | 65 +++++++++++++------ .../popup/settings/apps/[url]/index.tsx | 34 ++++++++-- .../popup/settings/apps/[url]/permissions.tsx | 56 +++++++++------- .../popup/settings/notifications/index.tsx | 6 +- 4 files changed, 109 insertions(+), 52 deletions(-) diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 0de61402c..22fcbfbd6 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,7 +1,46 @@ -import type { Dispatch, SetStateAction } from "react"; +import { useEffect, useMemo, useState, type HTMLProps } from "react"; import styled from "styled-components"; -const Round = styled.div` +export const Checkbox = ({ + checked, + onChange, + id, + size = 28 +}: CheckboxProps & Omit, "onChange">) => { + const [state, setState] = useState(checked); + const effectiveId = useMemo(() => id || generateUniqueId(), []); + + async function toggle() { + let newVal = state; + + setState((val) => { + newVal = !val; + return newVal; + }); + + if (onChange) { + await onChange(newVal); + } + } + + useEffect(() => setState(checked), [checked]); + + return ( + + + + + ); +}; + +interface CheckboxProps { + checked?: boolean; + onChange?: (checked: boolean) => void; + id?: string; + size?: number; +} + +const CheckboxWrapper = styled.div` position: relative; `; @@ -44,25 +83,9 @@ const Label = styled.label<{ size: number }>` } `; -export const Checkbox = ({ - checked, - setChecked, - size = 28 -}: { - checked: boolean; - setChecked: Dispatch>; - size?: number; -}) => { - const handleChange = () => { - setChecked(!checked); - }; - - return ( - - - - - ); +// Function to generate a unique ID +const generateUniqueId = (): string => { + return `checkbox-${Date.now()}-${Math.floor(Math.random() * 1000)}`; }; export default Checkbox; diff --git a/src/routes/popup/settings/apps/[url]/index.tsx b/src/routes/popup/settings/apps/[url]/index.tsx index 94f6ebfb9..c80da57c4 100644 --- a/src/routes/popup/settings/apps/[url]/index.tsx +++ b/src/routes/popup/settings/apps/[url]/index.tsx @@ -105,7 +105,17 @@ export default function AppSettings({ url }: Props) { setLocation(`/quick-settings/apps/${url}/permissions`) } > - {browser.i18n.getMessage("view_all")} + + {browser.i18n.getMessage("view_all")} + @@ -113,7 +123,11 @@ export default function AppSettings({ url }: Props) { {browser.i18n.getMessage("allowance")} + {browser.i18n.getMessage("allowanceTip")} +
+ } position="top" > @@ -181,7 +195,7 @@ export default function AppSettings({ url }: Props) { @@ -195,7 +209,17 @@ export default function AppSettings({ url }: Props) { })) } > - {browser.i18n.getMessage("reset")} + + {browser.i18n.getMessage("reset")} + @@ -381,7 +405,7 @@ const NumberInputV2 = styled(InputV2)` `; const ResetButton = styled.span` - border-bottom: 1px solid rgba(${(props) => props.theme.theme}, 0.8); + border-bottom: 1px solid rgba(${(props) => props.theme.primaryText}, 0.8); margin-left: 0.37rem; cursor: pointer; `; diff --git a/src/routes/popup/settings/apps/[url]/permissions.tsx b/src/routes/popup/settings/apps/[url]/permissions.tsx index acd876166..10314927b 100644 --- a/src/routes/popup/settings/apps/[url]/permissions.tsx +++ b/src/routes/popup/settings/apps/[url]/permissions.tsx @@ -1,9 +1,10 @@ -import { Checkbox, Spacer, Text } from "@arconnect/components"; +import { Spacer, Text } from "@arconnect/components"; import Application from "~applications/application"; import browser from "webextension-polyfill"; import styled from "styled-components"; import HeadV2 from "~components/popup/HeadV2"; import { permissionData, type PermissionType } from "~applications/permissions"; +import Checkbox from "~components/Checkbox"; export default function AppPermissions({ url }: Props) { // app settings @@ -20,28 +21,35 @@ export default function AppPermissions({ url }: Props) { {Object.keys(permissionData).map( (permissionName: PermissionType, i) => (
- - updateSettings((val) => { - // toggle permission - if (checked && !val.permissions.includes(permissionName)) { - val.permissions.push(permissionName); - } else if (!checked) { - val.permissions = val.permissions.filter( - (p) => p !== permissionName - ); - } + + + updateSettings((val) => { + // toggle permission + if ( + checked && + !val.permissions.includes(permissionName) + ) { + val.permissions.push(permissionName); + } else if (!checked) { + val.permissions = val.permissions.filter( + (p) => p !== permissionName + ); + } - return val; - }) - } - checked={settings.permissions.includes(permissionName)} - > - {permissionName} - - {browser.i18n.getMessage(permissionData[permissionName])} - - + return val; + }) + } + checked={settings.permissions.includes(permissionName)} + /> +
+ {permissionName} + + {browser.i18n.getMessage(permissionData[permissionName])} + +
+ {i !== Object.keys(permissionData).length - 1 && ( )} @@ -69,8 +77,10 @@ const Title = styled(Text).attrs({ font-size: 1.125rem; `; -const PermissionCheckbox = styled(Checkbox)` +const Permission = styled.div` + display: flex; align-items: center; + gap: 8px; `; export const PermissionDescription = styled(Text).attrs({ diff --git a/src/routes/popup/settings/notifications/index.tsx b/src/routes/popup/settings/notifications/index.tsx index cbbb413a4..d8fd39e9a 100644 --- a/src/routes/popup/settings/notifications/index.tsx +++ b/src/routes/popup/settings/notifications/index.tsx @@ -82,7 +82,7 @@ export default function NotificationSettings() { notificationCustomizeSettings && notificationCustomizeSettings.includes("default") } - setChecked={(_) => handleRadioChange("default")} + onChange={(_) => handleRadioChange("default")} /> Enable Arweave and ao Transaction Notifications @@ -101,7 +101,7 @@ export default function NotificationSettings() { "arTransferNotifications" ) } - setChecked={(_) => handleRadioChange("arTransferNotifications")} + onChange={(_) => handleRadioChange("arTransferNotifications")} /> Enable Arweave Transaction Notifications @@ -118,7 +118,7 @@ export default function NotificationSettings() { notificationCustomizeSettings && notificationCustomizeSettings.includes("allTxns") } - setChecked={(_) => handleRadioChange("allTxns")} + onChange={(_) => handleRadioChange("allTxns")} /> Enable all Arweave and ao Notifications From 17861dc1bcb17107cf1c57335f9a4665e3aa5a2b Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 3 Jul 2024 11:22:09 +0545 Subject: [PATCH 14/31] refactor: Update all settings page --- src/routes/dashboard/index.tsx | 44 ++++++++++++++++------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index bf517385c..b15f92b19 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -2,10 +2,7 @@ import { Card, Spacer, Text, useInput } from "@arconnect/components"; import SettingListItem, { type Props as SettingItemData } from "~components/dashboard/list/SettingListItem"; -import { - setting_element_padding, - SettingsList -} from "~components/dashboard/list/BaseElement"; +import { SettingsList } from "~components/dashboard/list/BaseElement"; import { useEffect, useMemo, useState } from "react"; import { useLocation } from "wouter"; import { @@ -113,7 +110,7 @@ export default function Settings({ params }: Props) { return ( - + {browser.i18n.getMessage("settings")} @@ -162,14 +159,7 @@ export default function Settings({ params }: Props) { ))} - + {allSettings && @@ -198,7 +188,7 @@ export default function Settings({ params }: Props) { return ; })())} - + {!!activeAppSetting && ( { return userAgent.includes("Mac") && !userAgent.includes("Windows"); }; -const Panel = styled(Card)<{ normalPadding?: boolean }>` +const Panel = styled(Card)<{ + normalPadding?: boolean; + showRightBorder?: boolean; +}>` position: relative; - padding: 0.5rem ${(props) => (!props.normalPadding ? "0.35rem" : "0.95rem")}; + border-radius: 0; + border: 0; + ${(props) => props.showRightBorder && `border-right: 1.5px solid #8e7bea;`} + padding: ${(props) => (props.normalPadding ? "1.5rem 1rem" : "1.5rem")}; overflow-y: auto; - height: calc(100% - 0.35rem * 2); ${!isMac() ? ` @@ -304,6 +297,11 @@ const Panel = styled(Card)<{ normalPadding?: boolean }>` @media screen and (max-width: 900px) { width: calc(50% - 2.5rem); height: 55vh; + flex-grow: 1; + + &:nth-child(2) { + border: 0; + } &:last-child { width: 100%; @@ -314,6 +312,7 @@ const Panel = styled(Card)<{ normalPadding?: boolean }>` @media screen and (max-width: 645px) { width: 100%; height: 55vh; + border: 0; &:last-child { height: auto; @@ -324,15 +323,12 @@ const Panel = styled(Card)<{ normalPadding?: boolean }>` const SettingsTitle = styled(Text).attrs({ title: true, noMargin: true -})` - padding: 0 ${setting_element_padding}; -`; +})``; const MidSettingsTitle = styled(Text).attrs({ title: true, noMargin: true })` - padding: 0 ${setting_element_padding}; font-weight: 600; text-transform: capitalize; `; From d818034a9ebf448208132c44537178a15dd9e798 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 3 Jul 2024 12:37:10 +0545 Subject: [PATCH 15/31] fix: Update contact hover color --- src/components/dashboard/list/ContactListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/list/ContactListItem.tsx b/src/components/dashboard/list/ContactListItem.tsx index ef36c7c71..3a613adc7 100644 --- a/src/components/dashboard/list/ContactListItem.tsx +++ b/src/components/dashboard/list/ContactListItem.tsx @@ -43,7 +43,7 @@ const ContactWrapper = styled.div<{ active: boolean; small?: boolean }>` &:hover { background-color: rgba( ${(props) => - props.small + props.small && props.theme.displayTheme === "dark" ? "43, 40, 56, 1" : props.theme.theme + ", " + From 0f0a669a735396083f39673424c20a738e3a9dea Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 3 Jul 2024 14:32:44 +0545 Subject: [PATCH 16/31] fix: Add correct back functions --- .../popup/settings/apps/[url]/index.tsx | 8 ++- .../popup/settings/apps/[url]/permissions.tsx | 7 +- src/routes/popup/settings/apps/index.tsx | 5 +- .../settings/contacts/[address]/index.tsx | 10 ++- src/routes/popup/settings/contacts/index.tsx | 8 ++- src/routes/popup/settings/contacts/new.tsx | 8 ++- .../popup/settings/notifications/index.tsx | 14 ++-- src/routes/popup/settings/quickSettings.tsx | 67 +++++++++---------- .../popup/settings/tokens/[id]/index.tsx | 5 +- src/routes/popup/settings/tokens/index.tsx | 5 +- src/routes/popup/settings/tokens/new.tsx | 8 ++- .../settings/wallets/[address]/export.tsx | 8 ++- .../settings/wallets/[address]/index.tsx | 5 +- src/routes/popup/settings/wallets/index.tsx | 5 +- 14 files changed, 106 insertions(+), 57 deletions(-) diff --git a/src/routes/popup/settings/apps/[url]/index.tsx b/src/routes/popup/settings/apps/[url]/index.tsx index c80da57c4..fe4d7e3f4 100644 --- a/src/routes/popup/settings/apps/[url]/index.tsx +++ b/src/routes/popup/settings/apps/[url]/index.tsx @@ -95,7 +95,10 @@ export default function AppSettings({ url }: Props) { return ( <> - + setLocation("/quick-settings/apps")} + />
@@ -380,6 +383,7 @@ const TitleV1 = styled(Text).attrs({ })` margin-bottom: 0; font-size: 1.125rem; + font-weight: 500; `; const TitleV2 = styled(Text).attrs({ @@ -387,7 +391,7 @@ const TitleV2 = styled(Text).attrs({ })` margin-bottom: 0.6em; font-size: 1rem; - font-weight: 600; + font-weight: 500; `; const NumberInputV2 = styled(InputV2)` diff --git a/src/routes/popup/settings/apps/[url]/permissions.tsx b/src/routes/popup/settings/apps/[url]/permissions.tsx index 10314927b..2f9ea8304 100644 --- a/src/routes/popup/settings/apps/[url]/permissions.tsx +++ b/src/routes/popup/settings/apps/[url]/permissions.tsx @@ -5,17 +5,22 @@ import styled from "styled-components"; import HeadV2 from "~components/popup/HeadV2"; import { permissionData, type PermissionType } from "~applications/permissions"; import Checkbox from "~components/Checkbox"; +import { useLocation } from "wouter"; export default function AppPermissions({ url }: Props) { // app settings const app = new Application(url); const [settings, updateSettings] = app.hook(); + const [, setLocation] = useLocation(); if (!settings) return <>; return ( <> - + setLocation(`/quick-settings/apps/${url}`)} + /> {browser.i18n.getMessage("permissions")} {Object.keys(permissionData).map( diff --git a/src/routes/popup/settings/apps/index.tsx b/src/routes/popup/settings/apps/index.tsx index 07e6b771f..6eec41e51 100644 --- a/src/routes/popup/settings/apps/index.tsx +++ b/src/routes/popup/settings/apps/index.tsx @@ -78,7 +78,10 @@ export default function Applications() { return ( <> - + setLocation("/quick-settings")} + /> - + setLocation("/quick-settings/contacts")} + /> @@ -23,5 +29,5 @@ const Wrapper = styled.div` flex-direction: column; justify-content: space-between; padding: 0 1rem; - height: calc(100vh - 80px); + height: calc(100vh - 70px); `; diff --git a/src/routes/popup/settings/contacts/index.tsx b/src/routes/popup/settings/contacts/index.tsx index 646526cf9..0d9d829f3 100644 --- a/src/routes/popup/settings/contacts/index.tsx +++ b/src/routes/popup/settings/contacts/index.tsx @@ -2,11 +2,17 @@ import HeadV2 from "~components/popup/HeadV2"; import browser from "webextension-polyfill"; import styled from "styled-components"; import { default as ContactsComponent } from "~components/dashboard/Contacts"; +import { useLocation } from "wouter"; export default function Contacts() { + const [, setLocation] = useLocation(); + return ( <> - + setLocation("/quick-settings")} + /> diff --git a/src/routes/popup/settings/contacts/new.tsx b/src/routes/popup/settings/contacts/new.tsx index c5bab13ff..647724e7d 100644 --- a/src/routes/popup/settings/contacts/new.tsx +++ b/src/routes/popup/settings/contacts/new.tsx @@ -2,11 +2,17 @@ import HeadV2 from "~components/popup/HeadV2"; import browser from "webextension-polyfill"; import AddContact from "~components/dashboard/subsettings/AddContact"; import styled from "styled-components"; +import { useLocation } from "wouter"; export default function NewContact() { + const [, setLocation] = useLocation(); + return ( <> - + setLocation("/quick-settings/contacts")} + /> diff --git a/src/routes/popup/settings/notifications/index.tsx b/src/routes/popup/settings/notifications/index.tsx index d8fd39e9a..c1e2ba18c 100644 --- a/src/routes/popup/settings/notifications/index.tsx +++ b/src/routes/popup/settings/notifications/index.tsx @@ -3,17 +3,16 @@ import styled from "styled-components"; import { ExtensionStorage } from "~utils/storage"; import { Spacer, Text, TooltipV2 } from "@arconnect/components"; import browser from "webextension-polyfill"; -import { - RadioWrapper, - RadioItem, - RadioInner -} from "~components/dashboard/Setting"; +import { RadioWrapper, RadioItem } from "~components/dashboard/Setting"; import HeadV2 from "~components/popup/HeadV2"; import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; import { InformationIcon } from "@iconicicons/react"; import Checkbox from "~components/Checkbox"; +import { useLocation } from "wouter"; export default function NotificationSettings() { + const [, setLocation] = useLocation(); + const [notificationSettings, setNotificationSettings] = useStorage( { key: "setting_notifications", @@ -40,7 +39,10 @@ export default function NotificationSettings() { return ( <> - + setLocation("/quick-settings")} + /> diff --git a/src/routes/popup/settings/quickSettings.tsx b/src/routes/popup/settings/quickSettings.tsx index eb6b0de3d..0e0cfbbaa 100644 --- a/src/routes/popup/settings/quickSettings.tsx +++ b/src/routes/popup/settings/quickSettings.tsx @@ -7,11 +7,6 @@ import { LinkExternal02, Coins04 } from "@untitled-ui/icons-react"; -import Wallets from "~components/dashboard/Wallets"; -import Applications from "~components/dashboard/Applications"; -import Contacts from "~components/dashboard/Contacts"; -import NotificationSettings from "~components/dashboard/NotificationSettings"; -import Tokens from "../tokens"; import { useLocation } from "wouter"; import { useMemo } from "react"; import { ListItem, ListItemIcon } from "@arconnect/components"; @@ -19,27 +14,6 @@ import type { Icon } from "~settings/setting"; import type { HTMLProps, ReactNode } from "react"; import styled from "styled-components"; -interface Props { - params: { - setting?: string; - subsetting?: string; - }; -} - -interface Setting extends SettingItemData { - name: string; - component?: (...args: any[]) => JSX.Element; - externalLink?: string; -} - -type SettingItemData = { - icon: Icon; - displayName: string; - description: string; - active: boolean; - isExternalLink?: boolean; -}; - export default function QuickSettings({ params }: Props) { // router location const [, setLocation] = useLocation(); @@ -49,7 +23,10 @@ export default function QuickSettings({ params }: Props) { return ( <> - + setLocation("/")} + /> {allSettings.map((setting, i) => ( JSX.Element; + externalLink?: string; +} + +type SettingItemData = { + icon: Icon; + displayName: string; + description: string; + active: boolean; + isExternalLink?: boolean; +}; + const ExternalLinkIcon = styled(LinkExternal02)` height: 1rem; width: 1rem; @@ -128,36 +126,31 @@ const allSettings: Omit[] = [ name: "wallets", displayName: "setting_wallets", description: "setting_wallets_description", - icon: WalletIcon, - component: Wallets + icon: WalletIcon }, { name: "apps", displayName: "setting_apps", description: "setting_apps_description", - icon: GridIcon, - component: Applications + icon: GridIcon }, { name: "tokens", displayName: "setting_tokens", description: "setting_tokens_description", - icon: Coins04, - component: Tokens + icon: Coins04 }, { name: "contacts", displayName: "setting_contacts", description: "setting_contacts_description", - icon: Users01, - component: Contacts + icon: Users01 }, { name: "notifications", displayName: "setting_notifications", description: "setting_notifications_description", - icon: BellIcon, - component: NotificationSettings + icon: BellIcon }, { name: "All Settings", diff --git a/src/routes/popup/settings/tokens/[id]/index.tsx b/src/routes/popup/settings/tokens/[id]/index.tsx index 5f96eb139..351225292 100644 --- a/src/routes/popup/settings/tokens/[id]/index.tsx +++ b/src/routes/popup/settings/tokens/[id]/index.tsx @@ -79,7 +79,10 @@ export default function TokenSettings({ id }: Props) { return ( <> - + setLocation("/quick-settings/tokens")} + />
diff --git a/src/routes/popup/settings/tokens/index.tsx b/src/routes/popup/settings/tokens/index.tsx index fec204274..020306e98 100644 --- a/src/routes/popup/settings/tokens/index.tsx +++ b/src/routes/popup/settings/tokens/index.tsx @@ -98,7 +98,10 @@ export default function Tokens() { return ( <> - + setLocation("/quick-settings")} + />
- + setLocation("/quick-settings/tokens")} + /> diff --git a/src/routes/popup/settings/wallets/[address]/export.tsx b/src/routes/popup/settings/wallets/[address]/export.tsx index 8b73aa7f1..1653dbb0a 100644 --- a/src/routes/popup/settings/wallets/[address]/export.tsx +++ b/src/routes/popup/settings/wallets/[address]/export.tsx @@ -15,8 +15,11 @@ import { downloadFile } from "~utils/file"; import browser from "webextension-polyfill"; import styled from "styled-components"; import HeadV2 from "~components/popup/HeadV2"; +import { useLocation } from "wouter"; export default function ExportWallet({ address }: Props) { + const [, setLocation] = useLocation(); + // wallets const [wallets] = useStorage( { @@ -79,7 +82,10 @@ export default function ExportWallet({ address }: Props) { return ( <> - + setLocation(`/quick-settings/wallets/${address}`)} + /> {browser.i18n.getMessage("export_keyfile_description")} diff --git a/src/routes/popup/settings/wallets/[address]/index.tsx b/src/routes/popup/settings/wallets/[address]/index.tsx index 96b6c0b63..5b2c4ca6d 100644 --- a/src/routes/popup/settings/wallets/[address]/index.tsx +++ b/src/routes/popup/settings/wallets/[address]/index.tsx @@ -124,7 +124,10 @@ export default function Wallet({ address }: Props) { return ( <> - + setLocation("/quick-settings/wallets")} + />
diff --git a/src/routes/popup/settings/wallets/index.tsx b/src/routes/popup/settings/wallets/index.tsx index 965a7c83d..d1226ff65 100644 --- a/src/routes/popup/settings/wallets/index.tsx +++ b/src/routes/popup/settings/wallets/index.tsx @@ -81,7 +81,10 @@ export default function Wallets() { return ( <> - + setLocation("/quick-settings")} + />
Date: Wed, 3 Jul 2024 15:47:01 +0545 Subject: [PATCH 17/31] refactor: Remove AOCred from default tokens list --- src/tokens/aoTokens/ao.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index c97725026..198d979fe 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -39,13 +39,6 @@ export const defaultAoTokens: TokenInfo[] = [ Logo: "AdFxCN1eEPboxNpCNL23WZRNhIhiamOeS-TUwx_Nr3Q", processId: "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ" }, - { - Name: "AOCRED", - Ticker: "testnet-AOCRED", - Denomination: 3, - Logo: "eIOOJiqtJucxvB4k8a-sEKcKpKTh9qQgOV3Au7jlGYc", - processId: "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc" - }, { Name: "Astro USD (Test)", Ticker: "USDA-TST", From 6e6505e530cb4f7e0a7a6d7381bea76ef65b43e2 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 3 Jul 2024 19:48:39 +0545 Subject: [PATCH 18/31] fix: Endless spinner issue when loading AR balance on dashboard --- src/components/popup/home/Balance.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/popup/home/Balance.tsx b/src/components/popup/home/Balance.tsx index 90ee69b4e..cfbc08318 100644 --- a/src/components/popup/home/Balance.tsx +++ b/src/components/popup/home/Balance.tsx @@ -124,7 +124,7 @@ export default function Balance() { } useEffect(() => { - if (!balance.isEqualTo(historicalBalance[0])) { + if (parseFloat(balance.toString()) !== historicalBalance[0]) { setLoading(true); } else { setLoading(false); From a29077ea09ae09307987627f5b53ce6c5b89bd8c Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 3 Jul 2024 10:17:37 -0700 Subject: [PATCH 19/31] feat: wip explore page for ecosystem apps --- assets/ecosystem/alex.svg | 5 + assets/ecosystem/arns.svg | 1 + assets/ecosystem/artbycity.png | Bin 0 -> 140607 bytes assets/ecosystem/bazar.png | Bin 0 -> 9528 bytes assets/ecosystem/echo.svg | 13 ++ assets/ecosystem/permafacts.svg | 67 ++++++ assets/ecosystem/permapages.svg | 1 + src/routes/popup/explore.tsx | 400 +++++++------------------------- src/utils/apps.ts | 299 ++++++++++++++++++++++++ 9 files changed, 468 insertions(+), 318 deletions(-) create mode 100644 assets/ecosystem/alex.svg create mode 100644 assets/ecosystem/arns.svg create mode 100644 assets/ecosystem/artbycity.png create mode 100644 assets/ecosystem/bazar.png create mode 100644 assets/ecosystem/echo.svg create mode 100644 assets/ecosystem/permafacts.svg create mode 100644 assets/ecosystem/permapages.svg create mode 100644 src/utils/apps.ts diff --git a/assets/ecosystem/alex.svg b/assets/ecosystem/alex.svg new file mode 100644 index 000000000..178f66bd0 --- /dev/null +++ b/assets/ecosystem/alex.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/ecosystem/arns.svg b/assets/ecosystem/arns.svg new file mode 100644 index 000000000..b1496ff17 --- /dev/null +++ b/assets/ecosystem/arns.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ecosystem/artbycity.png b/assets/ecosystem/artbycity.png new file mode 100644 index 0000000000000000000000000000000000000000..80fd46e3d3e96c89fa9e7e3824c8d1ba4a5ef448 GIT binary patch literal 140607 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 literal 0 HcmV?d00001 diff --git a/assets/ecosystem/bazar.png b/assets/ecosystem/bazar.png new file mode 100644 index 0000000000000000000000000000000000000000..77cd93045015a78dc3eab89c88fa2445933c7011 GIT binary patch literal 9528 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 HOWgkfFcBwC literal 0 HcmV?d00001 diff --git a/assets/ecosystem/echo.svg b/assets/ecosystem/echo.svg new file mode 100644 index 000000000..7dac18c84 --- /dev/null +++ b/assets/ecosystem/echo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ecosystem/permafacts.svg b/assets/ecosystem/permafacts.svg new file mode 100644 index 000000000..3791788e3 --- /dev/null +++ b/assets/ecosystem/permafacts.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + ermafacts + + diff --git a/assets/ecosystem/permapages.svg b/assets/ecosystem/permapages.svg new file mode 100644 index 000000000..ef55dbfb7 --- /dev/null +++ b/assets/ecosystem/permapages.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/routes/popup/explore.tsx b/src/routes/popup/explore.tsx index 05d65d42d..e9dc1eb12 100644 --- a/src/routes/popup/explore.tsx +++ b/src/routes/popup/explore.tsx @@ -1,126 +1,15 @@ -import Article, { - type ArticleInterface, - LoadingArticle -} from "~components/popup/Article"; -import { Section, Spacer, Text } from "@arconnect/components"; -import { getMarketChart, getArPrice } from "~lib/coingecko"; -import { AnimatePresence, motion } from "framer-motion"; -import { parseCoverImageFromContent } from "~lib/ans"; import { useEffect, useState } from "react"; -import PeriodPicker from "~components/popup/asset/PeriodPicker"; -import viewblockLogo from "url:/assets/ecosystem/viewblock.png"; -import metaweaveLogo from "url:/assets/ecosystem/metaweave.png"; -import permaswapLogo from "url:/assets/ecosystem/permaswap.svg"; -import PriceChart from "~components/popup/asset/PriceChart"; -import arDriveLogo from "url:/assets/ecosystem/ardrive.svg"; -import aftrLogo from "url:/assets/ecosystem/aftrmarket.png"; -import getPermawebNewsFeed from "~lib/permaweb_news"; -import AppIcon from "~components/popup/home/AppIcon"; -import Skeleton from "~components/Skeleton"; import browser from "webextension-polyfill"; -import useSetting from "~settings/hook"; -import styled from "styled-components"; import { PageType, trackPage } from "~utils/analytics"; import HeadV2 from "~components/popup/HeadV2"; -import logo from "url:/assets/icon512.png"; -import { SendButton } from "./send"; -import { useHistory } from "~utils/hash_router"; +import styled from "styled-components"; +import { InputV2 } from "@arconnect/components"; +import { SearchIcon } from "@iconicicons/react"; +import AppIcon from "~components/popup/home/AppIcon"; +import { ShareIcon } from "@iconicicons/react"; +import { apps } from "~utils/apps"; export default function Explore() { - // ar price period - const [period, setPeriod] = useState("Day"); - - // currency setting - const [currency] = useSetting("currency"); - - // load ar price history - const [priceData, setPriceData] = useState([]); - - useEffect(() => { - (async () => { - const days = { - Day: "1", - Week: "7", - Month: "31", - Year: "365", - All: "max" - }; - const { prices } = await getMarketChart(currency, days[period]); - - setPriceData(prices.map(([, price]) => price)); - })(); - }, [period, currency]); - - // load latest ar price - const [latestPrice, setLatestPrice] = useState(0); - const [push] = useHistory(); - - useEffect(() => { - (async () => { - const price = await getArPrice(currency); - - setLatestPrice(price); - })(); - }, [currency]); - - // active featured news page - const [featuredPage, setFeaturedPage] = useState(0); - - // const for navigation button - const numberOfFeatured = 3; - - useEffect(() => { - const id = setTimeout( - () => - setFeaturedPage((v) => { - if (v > 1) return 0; - else return v + 1; - }), - 4000 - ); - - return () => clearInterval(id); - }, [featuredPage]); - - // parse permaweb.news RSS - const [feed, setFeed] = useState(); - const [error, setIsError] = useState(false); - - useEffect(() => { - (async () => { - // get feed - try { - const permawebNews = await getPermawebNewsFeed(); - - // TODO: add other sources - - // construct feed - const unsortedFeed: ArticleInterface[] = permawebNews.map( - (article) => ({ - source: "permaweb.news", - title: article.title, - date: article.pubDate, - link: article.link, - content: article.contentSnippet, - cover: parseCoverImageFromContent(article.content) - }) - ); - - setFeed( - unsortedFeed.sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() - ) - ); - if (unsortedFeed.length < 1) { - setIsError(true); - } - } catch (err) { - console.log("err fetching articles", err); - } - })(); - }, []); - - //segment useEffect(() => { trackPage(PageType.EXPLORE); }, []); @@ -128,240 +17,115 @@ export default function Explore() { return ( <> -
- - - browser.tabs.create({ url: "https://app.ardrive.io" }) - } - > - {"ArDrive"} - - browser.tabs.create({ url: "https://aftr.market" })} - > - {"AFTR"} - - - browser.tabs.create({ url: "https://viewblock.io/arweave" }) - } - > - {"Viewblock"} - - - browser.tabs.create({ url: "https://metaweave.xyz" }) - } - > - {"Metaweave"} - - - browser.tabs.create({ url: "https://permaswap.network" }) - } - > - {"Permaswap"} - - -
- - {!error ? ( - <> - - - {(feed && ( - - browser.tabs.create({ - url: feed[featuredPage]?.link - }) - } - > - {feed[featuredPage]?.title || ""} - - )) || ( - - - - - - )} - - - {feed && ( - - {Array.from({ length: numberOfFeatured }).map((_, index) => ( - setFeaturedPage(index)} - /> - ))} - - )} - - - {feed && - feed.slice(4).map((article, i) =>
)} - {!feed && - Array(6) - .fill("") - .map((_, i) => )} - - ) : ( - - -

This page is having a problem

-

Error: unable to establish a connection to permaweb.news

-

Please try again another time.

-

- ArConnect status: Online -

- push("/")}>Back to home -
- )} + + } + placeholder="Search for a dApp" + /> +
+ {apps.map((app, index) => ( + + + + + + + + + <AppTitle>{app.name}</AppTitle> + <Pill>{app.category}</Pill> + + {app.description} + + + + + + ))} +
+
); } -const Logo = styled.img.attrs({ - draggable: false, - alt: "ArConnect", - src: logo -})` - width: 75px; +const IconWrapper = styled.div` + width: 16px; `; -const ErrorSection = styled.div` - align-items: center; - justify-content: center; - padding: 28px 15px; +const Description = styled.div` display: flex; flex-direction: column; - gap: 18px; - - h2 { - font-weight: 600; - font-size: 18px; - margin: 0; - } + gap: 4px; +`; - p { - font-size: 12px; - font-weight: 500; - color: #a3a3a3; - white-space: nowrap; - margin: 0; - } +const Title = styled.div` + display: flex; + gap: 4px; + align-items: center; `; -const FeaturedArticles = styled.div` - position: relative; +const Wrapper = styled.div` + padding: 18px 1rem 72px; display: flex; - overflow: hidden; - height: 125px; - transition: transform 0.07s ease-in-out; + flex-direction: column; + gap: 18px; +`; - &:active { - transform: scale(0.98); - } +const AppTitle = styled.h3` + margin: 0; + font-size: 1rem; + font-weight: 500; + color: ${(props) => props.theme.primaryTextv2}; `; -const FeaturedArticle = styled(motion.div).attrs({ - initial: { x: 1000, opacity: 0 }, - animate: { x: 0, opacity: 1 }, - exit: { x: -1000, opacity: 0 }, - transition: { - x: { type: "spring", stiffness: 300, damping: 30 }, - opacity: { duration: 0.2 } - } -})<{ background: string }>` - position: absolute; - top: 0; - bottom: 0; - width: 100%; - background-image: url(${(props) => props.background}); - background-size: cover; - background-position: center; - cursor: pointer; +const Pill = styled.div` + color: ${(props) => props.theme.primaryTextv2}; + background-color: ${(props) => props.theme.backgroundSecondary}; + padding: 3px 8px; + border-radius: 50px; + border: 1px solid ${(props) => props.theme.inputField}; + + font-size: 10px; `; -const ArticleTitle = styled(Text).attrs({ - noMargin: true -})` +const AppDescription = styled.p` + margin: 0; display: -webkit-box; - position: absolute; - font-size: 0.85rem; - font-weight: 600; - left: 0.5rem; - bottom: 0.5rem; - padding: 0.1rem 0.35rem; - border-radius: 4px; - // 100 % - padding * 2 + left + right - max-width: calc(100% - 0.35rem * 2 - 0.5rem * 2); - color: #fff; - background-color: rgba(0, 0, 0, 0.45); - backdrop-filter: blur(5px); - line-clamp: 2; - box-orient: vertical; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; + margin: 0; + font-size: 10px; + color: ${(props) => props.theme.secondaryTextv2}; `; -const NavigationWrapper = styled.div` +const LogoWrapper = styled.div` display: flex; - justify-content: center; - padding-top: 1rem; - gap: 0.25rem; -`; - -const NavigationButton = styled.button<{ featured: boolean }>` - width: 8px; - height: 8px; - background-color: ${(props) => - props.featured - ? props.theme.displayTheme === "light" - ? "#000" - : "#fff" - : "#999999"}; - border-radius: 50%; - border: none; - padding: 0; - cursor: pointer; + align-items: center; + height: 100$; `; -const FeaturedSkeleton = styled(Skeleton)` - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - width: auto; - height: auto; - - ${ArticleTitle} { - padding: 0; - } +const Logo = styled.img` + height: 25px; + width: 25px; `; -const Shortcuts = styled.div` +const AppWrapper = styled.div` + padding: 10px; + gap: 12px; display: flex; - align-items: center; - justify-content: space-between; `; -const AppShortcut = styled(AppIcon)` +const AppShortcut = styled(AppIcon)<{ bgColor?: string }>` transition: all 0.125s ease-in-out; + color: ${(props) => + props.bgColor ? props.bgColor : props.theme.primaryTextv2}; + + width: 32px; + height: 32px; &:hover { opacity: 0.9; diff --git a/src/utils/apps.ts b/src/utils/apps.ts new file mode 100644 index 000000000..9dd21f77a --- /dev/null +++ b/src/utils/apps.ts @@ -0,0 +1,299 @@ +import alexLogo from "url:/assets/ecosystem/alex.svg"; +import ardriveLogo from "url:/assets/ecosystem/ardrive.svg"; +import aftrmarketLogo from "url:/assets/ecosystem/aftrmarket.png"; +import arwikiLogo from "url:/assets/ecosystem/arwiki.png"; +import bazarLogo from "url:/assets/ecosystem/bazar.png"; +import protocollandLogo from "url:/assets/ecosystem/protocolland.svg"; +import permaswapLogo from "url:/assets/ecosystem/permaswap.svg"; +import pianityLogo from "url:/assets/ecosystem/pianity.png"; +import barkLogo from "url:/assets/ecosystem/bark.png"; +import ansLogo from "url:/assets/ecosystem/ans-logo.svg"; +import arnsLogo from "url:/assets/ecosystem/arns.svg"; +import astroLogo from "url:/assets/ecosystem/astro.png"; +import artByCityLogo from "url:/assets/ecosystem/artbycity.png"; +import permapagesLogo from "url:/assets/ecosystem/permapages.svg"; +import echoLogo from "url:/assets/ecosystem/echo.svg"; +import permaFacts from "url:/assets/ecosystem/permafacts.svg"; +import arLogoLight from "url:/assets/ar/logo_light.png"; + +export interface App { + name: string; + category: string; + description: string; + assets: { + logo: string; + thumbnail: string; + bgColor?: string; + }; + links: { + website: string; + twitter?: string; + discord?: string; + github?: string; + }; +} + +export const apps: App[] = [ + { + name: "ArDrive", + category: "Storage", + description: + "ArDrive offers never-ending storage of your most valuable files. Pay once and save your memories forever.", + assets: { + logo: ardriveLogo, + thumbnail: permaswapLogo, + bgColor: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://ardrive.io", + twitter: "https://twitter.com/ardriveapp", + discord: "https://discord.com/invite/ya4hf2H", + github: "https://github.com/ardriveapp" + } + }, + { + name: "Permaswap", + category: "Finance", + description: + "Permaswap is an engineering innovation to refactor AMM. Inspired by Arweave’s SmartWeave, we’ve proposed the SCP theory. By exploring SCP, we’re certain that the approach to building decentralized applications is not limited to the on-chain VM model and that the future of Dapp development will be diverse. The Permaswap Network will prove with a new architecture that decentralization should break the impossible triangle and provide users with a perfect experience.", + assets: { + logo: permaswapLogo, + thumbnail: permaswapLogo, + bgColor: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://permaswap.network/", + twitter: "https://twitter.com/permaswap", + discord: "https://discord.gg/WM5MQZGVPF", + github: "https://github.com/permaswap" + } + }, + { + name: "Astro", + category: "Finance", + description: + "Astro USD (USDA) is the very first stablecoin in the Arweave (and AO Computer) ecosystem.", + assets: { + logo: astroLogo, + thumbnail: "/apps/astro/thumbnail.png", + bgColor: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://astrousd.com", + twitter: "https://twitter.com/AstroUSD", + discord: "https://discord.gg/NpNRtNE6PN" + } + }, + { + name: "Bark", + category: "Finance", + description: + "Bark is the AO Computer's first decentralized Finance. It supports AMM trading pairs and extreme scalability.", + assets: { + logo: barkLogo, + thumbnail: "/apps/bark/thumbnail.png", + bgColor: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://bark.arweave.dev" + } + }, + { + name: "Protocol.Land", + category: "Development", + description: + "Code collaboration, reimagined. Protocol.Land is a decentralized, source controlled, code collaboration where you own your code.", + assets: { + logo: protocollandLogo, + thumbnail: "/apps/protocolland/thumbnail.png", + bgColor: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://protocol.land", + twitter: "https://twitter.com/ProtocolLand", + discord: "https://discord.com/invite/GqxX2vtwRj", + github: "https://github.com/labscommunity/protocol-land" + } + }, + { + name: "Arweave Name System", + category: "Social", + description: + "The Arweave Name System (ArNS) works similarly to traditional Domain Name Services - but with ArNS, the registry is decentralized, permanent, and stored on Arweave. It’s a simple way to name and help you - and your users - find your data, apps, or websites on Arweave.", + assets: { + logo: arnsLogo, + thumbnail: "/apps/arns/thumbnail.jpeg" + }, + links: { + website: "https://arns.app", + twitter: "https://twitter.com/ar_io_network", + discord: "https://discord.com/invite/HGG52EtTc2", + github: "https://github.com/ar-io" + } + }, + { + name: "Art By City", + category: "NFTs", + description: + "Art By City is a chain-agnostic Web3 art and creative content protocol built on Arweave. The protocol is governed by the Art By City DAO. The Art By City DAO is a Profit Sharing Community or PSC. The Art By City community governs development of the Art By City protocol, dApps, and tools artists will need to take control of their Web3 experience.", + assets: { + logo: artByCityLogo, + thumbnail: "/apps/artbycity/thumbnail.png" + }, + links: { + website: "https://artby.city", + twitter: "https://twitter.com/artbycity", + discord: "https://discord.gg/w4Yhc95b8p", + github: "https://github.com/art-by-city" + } + }, + { + name: "ECHO", + category: "Social", + description: + "ECHO is the first decentralized social engagement protocol based on Arweave. Its goal is to provide the fundamental infrastructure of Web3 social by introducing the first comment widget that can be deployed on any Web3 website with permanent data storage, so that users can speak up for themselves in a decentralized, permissionless, and censorship-resistant environment. ", + assets: { + logo: echoLogo, + thumbnail: "/apps/echo/thumbnail.png" + }, + links: { + website: "https://0xecho.com", + twitter: "https://twitter.com/0x_ECHO", + discord: "https://discord.gg/KFxyaw9Wdj", + github: "https://github.com/0x-echo" + } + }, + { + name: "Permafacts", + category: "Social", + description: + "A provably neutral publishing platform, built on top of the #FactsProtocol, aimed at dis-intermediating the truth. Publish assertions, and take your position in the Fact Marketplace!", + assets: { + logo: permaFacts, + thumbnail: "/apps/permafacts/thumbnail.png" + }, + links: { + website: "https://permafacts.arweave.dev", + twitter: "https://twitter.com/permafacts", + discord: "https://discord.gg/uGg8VAvqU7", + github: "https://github.com/facts-laboratory" + } + }, + { + name: "Pianity", + category: "NFTs", + description: + "Pianity is a music NFT platform – built on environmentally-conscious Arweave technology — where musicians and their community gather to create, sell, buy and collect songs in limited editions. Pianity’s pioneering approach, which includes free listening for all, enables deeper connections between artists and their audience.", + assets: { + logo: pianityLogo, + thumbnail: "/apps/pianity/thumbnail.png" + }, + links: { + website: "https://pianity.com", + twitter: "https://twitter.com/pianitynft", + discord: "https://discord.gg/pianity" + } + }, + { + name: "Arweave Name Service (ANS)", + category: "Social", + description: + "ans.gg is a popular name service built on top of the Arweave blockchain. Buy your domain once, own forever.", + assets: { + logo: ansLogo, + thumbnail: "/apps/ans/thumbnail.png" + }, + links: { + website: "https://ans.gg", + twitter: "https://twitter.com/ArweaveANS", + discord: "https://discord.gg/decentland" + } + }, + { + name: "Permapages", + category: "Social", + description: + "Create and manage your own permanent web3 profile and permaweb pages built on Arweave.", + assets: { + logo: permapagesLogo, + thumbnail: "/apps/permapages/thumbnail.png" + }, + links: { + website: "https://permapages.app", + twitter: "https://twitter.com/permapages" + } + }, + { + name: "ArCode Studio", + category: "Development", + description: + "ArCode Studio is an online IDE for smartweave contracts. As ArCode works on the browser all the files are saved in cache memory and removed when the cache is cleared.", + assets: { + logo: arLogoLight, + thumbnail: "/apps/arcode/thumbnail.jpeg" + }, + links: { + website: "https://arcode.ar-io.dev", + github: "https://github.com/luckyr13/arcode" + } + }, + { + name: "Arwiki", + category: "Social", + description: + "As MediaWiki is the software that powers Wikipedia, ArWiki is the software that powers the Arweave Wiki. However, ArWiki is a Web3 platform -- it is completely decentralized, and is hosted on and served from the Arweave permaweb itself.", + assets: { + logo: arwikiLogo, + thumbnail: "/apps/arwiki/thumbnail.jpeg" + }, + links: { + website: "https://arwiki.wik", + twitter: "https://x.com/TheArWiki" + } + }, + { + name: "Alex", + category: "Storage", + description: + "A decentralized archival platform that preserves human history and culture digitally.", + assets: { + logo: alexLogo, + thumbnail: "/apps/alex/thumbnail.png" + }, + links: { + website: "https://alex.arweave.dev/", + twitter: "https://twitter.com/thealexarchive", + discord: "http://discord.gg/2uZsWuTNvN" + } + }, + { + name: "Bazar", + category: "NFTs", + description: + "The first fully decentralized atomic asset exchange built on the permaweb. Through the power of the Universal Content Marketplace (UCM) protocol and the Universal Data License (UDL) content creators can trade digital assets with real world rights.", + assets: { + logo: bazarLogo, + thumbnail: "/apps/bazar/thumbnail.gif" + }, + links: { + website: "https://bazar.arweave.dev", + twitter: "https://twitter.com/OurBazAR" + } + }, + { + name: "AFTR Market", + category: "Finance", + description: + "AFTR Market provides asset management and governance on-chain for Arweave assets.", + assets: { + logo: aftrmarketLogo, + thumbnail: "/apps/aftr/thumbnail.png", + bgColor: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://www.aftr.market/", + twitter: "https://twitter.com/AftrMarket", + discord: "https://discord.gg/YEy8VpuNXR" + } + } +]; From cd51e1c0abc8599d1c4032acc767e849c5f5b940 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 3 Jul 2024 16:03:09 -0700 Subject: [PATCH 20/31] feat: completed explore page --- src/components/popup/Navigation.tsx | 66 ++++++------- src/routes/popup/explore.tsx | 50 ++++++++-- src/utils/apps.ts | 145 +++++++++++++++------------- 3 files changed, 151 insertions(+), 110 deletions(-) diff --git a/src/components/popup/Navigation.tsx b/src/components/popup/Navigation.tsx index 85b86c3df..9823eb1da 100644 --- a/src/components/popup/Navigation.tsx +++ b/src/components/popup/Navigation.tsx @@ -44,41 +44,41 @@ export const NavigationBar = () => { const theme = useTheme(); const [push] = useHistory(); const [location] = useLocation(); - const shouldShowNavigationBar = buttons.some((button) => { - if (button.title === "Send") { - return location.startsWith(button.route); - } else { - return location === button.route; - } - }); + const shouldShowNavigationBar = + location !== "/explore" && + buttons.some((button) => { + if (button.title === "Send") { + return location.startsWith(button.route); + } else { + return location === button.route; + } + }); + + if (!shouldShowNavigationBar) { + return null; + } return ( - <> - {shouldShowNavigationBar && ( - - <> - - {buttons.map((button, index) => { - const active = button.route === location; - return ( - push(button.route)} - > - - {button.icon} - -
{browser.i18n.getMessage(button.title)}
-
- ); - })} -
- -
- )} - + + + {buttons.map((button, index) => { + const active = button.route === location; + return ( + push(button.route)} + > + + {button.icon} + +
{browser.i18n.getMessage(button.title)}
+
+ ); + })} +
+
); }; diff --git a/src/routes/popup/explore.tsx b/src/routes/popup/explore.tsx index e9dc1eb12..79ac47e99 100644 --- a/src/routes/popup/explore.tsx +++ b/src/routes/popup/explore.tsx @@ -3,32 +3,51 @@ import browser from "webextension-polyfill"; import { PageType, trackPage } from "~utils/analytics"; import HeadV2 from "~components/popup/HeadV2"; import styled from "styled-components"; -import { InputV2 } from "@arconnect/components"; +import { InputV2, useInput } from "@arconnect/components"; import { SearchIcon } from "@iconicicons/react"; import AppIcon from "~components/popup/home/AppIcon"; import { ShareIcon } from "@iconicicons/react"; -import { apps } from "~utils/apps"; +import { apps, type App } from "~utils/apps"; +import { useTheme } from "~utils/theme"; export default function Explore() { + const [filteredApps, setFilteredApps] = useState(apps); + const searchInput = useInput(); + const theme = useTheme(); + useEffect(() => { trackPage(PageType.EXPLORE); }, []); + useEffect(() => { + setFilteredApps(filterApps(apps, searchInput.state)); + }, [searchInput.state]); + return ( <> } placeholder="Search for a dApp" />
- {apps.map((app, index) => ( + {filteredApps.map((app, index) => ( - + { + browser.tabs.create({ url: app.links.website }); + }} + > @@ -40,7 +59,14 @@ export default function Explore() { {app.description} - + { + browser.tabs.create({ url: app.links.website }); + }} + /> ))} @@ -50,6 +76,15 @@ export default function Explore() { ); } +const filterApps = (apps: App[], searchTerm: string = ""): App[] => { + return apps.filter( + (app: App) => + app.name.toLowerCase().includes(searchTerm.toLowerCase()) || + app.description.toLowerCase().includes(searchTerm.toLowerCase()) || + app.category.toLowerCase().includes(searchTerm.toLowerCase()) + ); +}; + const IconWrapper = styled.div` width: 16px; `; @@ -67,7 +102,7 @@ const Title = styled.div` `; const Wrapper = styled.div` - padding: 18px 1rem 72px; + padding: 18px 1rem; display: flex; flex-direction: column; gap: 18px; @@ -121,8 +156,7 @@ const AppWrapper = styled.div` const AppShortcut = styled(AppIcon)<{ bgColor?: string }>` transition: all 0.125s ease-in-out; - color: ${(props) => - props.bgColor ? props.bgColor : props.theme.primaryTextv2}; + color: ${(props) => (props.bgColor ? props.bgColor : props.theme.background)}; width: 32px; height: 32px; diff --git a/src/utils/apps.ts b/src/utils/apps.ts index 9dd21f77a..644a5f8d3 100644 --- a/src/utils/apps.ts +++ b/src/utils/apps.ts @@ -23,7 +23,8 @@ export interface App { assets: { logo: string; thumbnail: string; - bgColor?: string; + lightBackground?: string; + darkBackground?: string; }; links: { website: string; @@ -35,48 +36,48 @@ export interface App { export const apps: App[] = [ { - name: "ArDrive", - category: "Storage", + name: "Bark", + category: "Exchange", description: - "ArDrive offers never-ending storage of your most valuable files. Pay once and save your memories forever.", + "Bark is the AO Computer's first decentralized Finance. It supports AMM trading pairs and extreme scalability.", assets: { - logo: ardriveLogo, - thumbnail: permaswapLogo, - bgColor: "rgba(19, 28, 37, 1)" + logo: barkLogo, + thumbnail: "/apps/bark/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { - website: "https://ardrive.io", - twitter: "https://twitter.com/ardriveapp", - discord: "https://discord.com/invite/ya4hf2H", - github: "https://github.com/ardriveapp" + website: "https://bark.arweave.dev" } }, { - name: "Permaswap", - category: "Finance", + name: "Protocol.Land", + category: "Storage", description: - "Permaswap is an engineering innovation to refactor AMM. Inspired by Arweave’s SmartWeave, we’ve proposed the SCP theory. By exploring SCP, we’re certain that the approach to building decentralized applications is not limited to the on-chain VM model and that the future of Dapp development will be diverse. The Permaswap Network will prove with a new architecture that decentralization should break the impossible triangle and provide users with a perfect experience.", + "Code collaboration, reimagined. Protocol.Land is a decentralized, source controlled, code collaboration where you own your code.", assets: { - logo: permaswapLogo, - thumbnail: permaswapLogo, - bgColor: "rgba(19, 28, 37, 1)" + logo: protocollandLogo, + thumbnail: "/apps/protocolland/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { - website: "https://permaswap.network/", - twitter: "https://twitter.com/permaswap", - discord: "https://discord.gg/WM5MQZGVPF", - github: "https://github.com/permaswap" + website: "https://protocol.land", + twitter: "https://twitter.com/ProtocolLand", + discord: "https://discord.com/invite/GqxX2vtwRj", + github: "https://github.com/labscommunity/protocol-land" } }, { name: "Astro", - category: "Finance", + category: "De-fi", description: "Astro USD (USDA) is the very first stablecoin in the Arweave (and AO Computer) ecosystem.", assets: { logo: astroLogo, thumbnail: "/apps/astro/thumbnail.png", - bgColor: "rgba(19, 28, 37, 1)" + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { website: "https://astrousd.com", @@ -85,41 +86,39 @@ export const apps: App[] = [ } }, { - name: "Bark", - category: "Finance", + name: "AFTR Market", + category: "Developer Tooling", description: - "Bark is the AO Computer's first decentralized Finance. It supports AMM trading pairs and extreme scalability.", + "AFTR Market provides asset management and governance on-chain for Arweave assets.", assets: { - logo: barkLogo, - thumbnail: "/apps/bark/thumbnail.png", - bgColor: "rgba(19, 28, 37, 1)" + logo: aftrmarketLogo, + thumbnail: "/apps/aftr/thumbnail.png" }, links: { - website: "https://bark.arweave.dev" + website: "https://www.aftr.market/", + twitter: "https://twitter.com/AftrMarket", + discord: "https://discord.gg/YEy8VpuNXR" } }, { - name: "Protocol.Land", - category: "Development", + name: "Bazar", + category: "Exchange", description: - "Code collaboration, reimagined. Protocol.Land is a decentralized, source controlled, code collaboration where you own your code.", + "The first fully decentralized atomic asset exchange built on the permaweb. Through the power of the Universal Content Marketplace (UCM) protocol and the Universal Data License (UDL) content creators can trade digital assets with real world rights.", assets: { - logo: protocollandLogo, - thumbnail: "/apps/protocolland/thumbnail.png", - bgColor: "rgba(19, 28, 37, 1)" + logo: bazarLogo, + thumbnail: "/apps/bazar/thumbnail.gif" }, links: { - website: "https://protocol.land", - twitter: "https://twitter.com/ProtocolLand", - discord: "https://discord.com/invite/GqxX2vtwRj", - github: "https://github.com/labscommunity/protocol-land" + website: "https://bazar.arweave.dev", + twitter: "https://twitter.com/OurBazAR" } }, { name: "Arweave Name System", category: "Social", description: - "The Arweave Name System (ArNS) works similarly to traditional Domain Name Services - but with ArNS, the registry is decentralized, permanent, and stored on Arweave. It’s a simple way to name and help you - and your users - find your data, apps, or websites on Arweave.", + "The Arweave Name System (ArNS) works similarly to traditional Domain Name Services - but with ArNS, the registry is decentralized, permanent, and stored on Arweave. It's a simple way to name and help you - and your users - find your data, apps, or websites on Arweave.", assets: { logo: arnsLogo, thumbnail: "/apps/arns/thumbnail.jpeg" @@ -133,12 +132,14 @@ export const apps: App[] = [ }, { name: "Art By City", - category: "NFTs", + category: "Publishing", description: "Art By City is a chain-agnostic Web3 art and creative content protocol built on Arweave. The protocol is governed by the Art By City DAO. The Art By City DAO is a Profit Sharing Community or PSC. The Art By City community governs development of the Art By City protocol, dApps, and tools artists will need to take control of their Web3 experience.", assets: { logo: artByCityLogo, - thumbnail: "/apps/artbycity/thumbnail.png" + thumbnail: "/apps/artbycity/thumbnail.png", + lightBackground: "rgba(255, 255, 255, 1)", + darkBackground: "rgba(255, 255, 255, 1)" }, links: { website: "https://artby.city", @@ -165,7 +166,7 @@ export const apps: App[] = [ }, { name: "Permafacts", - category: "Social", + category: "Publishing", description: "A provably neutral publishing platform, built on top of the #FactsProtocol, aimed at dis-intermediating the truth. Publish assertions, and take your position in the Fact Marketplace!", assets: { @@ -183,7 +184,7 @@ export const apps: App[] = [ name: "Pianity", category: "NFTs", description: - "Pianity is a music NFT platform – built on environmentally-conscious Arweave technology — where musicians and their community gather to create, sell, buy and collect songs in limited editions. Pianity’s pioneering approach, which includes free listening for all, enables deeper connections between artists and their audience.", + "Pianity is a music NFT platform – built on environmentally-conscious Arweave technology — where musicians and their community gather to create, sell, buy and collect songs in limited editions. Pianity's pioneering approach, which includes free listening for all, enables deeper connections between artists and their audience.", assets: { logo: pianityLogo, thumbnail: "/apps/pianity/thumbnail.png" @@ -211,7 +212,7 @@ export const apps: App[] = [ }, { name: "Permapages", - category: "Social", + category: "Publishing", description: "Create and manage your own permanent web3 profile and permaweb pages built on Arweave.", assets: { @@ -223,6 +224,24 @@ export const apps: App[] = [ twitter: "https://twitter.com/permapages" } }, + { + name: "ArDrive", + category: "Storage", + description: + "ArDrive offers never-ending storage of your most valuable files. Pay once and save your memories forever.", + assets: { + logo: ardriveLogo, + thumbnail: permaswapLogo, + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" + }, + links: { + website: "https://ardrive.io", + twitter: "https://twitter.com/ardriveapp", + discord: "https://discord.com/invite/ya4hf2H", + github: "https://github.com/ardriveapp" + } + }, { name: "ArCode Studio", category: "Development", @@ -267,33 +286,21 @@ export const apps: App[] = [ } }, { - name: "Bazar", - category: "NFTs", - description: - "The first fully decentralized atomic asset exchange built on the permaweb. Through the power of the Universal Content Marketplace (UCM) protocol and the Universal Data License (UDL) content creators can trade digital assets with real world rights.", - assets: { - logo: bazarLogo, - thumbnail: "/apps/bazar/thumbnail.gif" - }, - links: { - website: "https://bazar.arweave.dev", - twitter: "https://twitter.com/OurBazAR" - } - }, - { - name: "AFTR Market", - category: "Finance", + name: "Permaswap", + category: "Exchange", description: - "AFTR Market provides asset management and governance on-chain for Arweave assets.", + "Permaswap is an engineering innovation to refactor AMM. Inspired by Arweave's SmartWeave, we've proposed the SCP theory. By exploring SCP, we're certain that the approach to building decentralized applications is not limited to the on-chain VM model and that the future of Dapp development will be diverse. The Permaswap Network will prove with a new architecture that decentralization should break the impossible triangle and provide users with a perfect experience.", assets: { - logo: aftrmarketLogo, - thumbnail: "/apps/aftr/thumbnail.png", - bgColor: "rgba(19, 28, 37, 1)" + logo: permaswapLogo, + thumbnail: permaswapLogo, + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { - website: "https://www.aftr.market/", - twitter: "https://twitter.com/AftrMarket", - discord: "https://discord.gg/YEy8VpuNXR" + website: "https://permaswap.network/", + twitter: "https://twitter.com/permaswap", + discord: "https://discord.gg/WM5MQZGVPF", + github: "https://github.com/permaswap" } } ]; From 01ae3039d45f4d6a30750b09e29bf16cb45445e9 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 12:13:01 +0545 Subject: [PATCH 21/31] fix: decode url for quick app settings --- src/routes/popup/settings/apps/[url]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/popup/settings/apps/[url]/index.tsx b/src/routes/popup/settings/apps/[url]/index.tsx index fe4d7e3f4..c8c38a755 100644 --- a/src/routes/popup/settings/apps/[url]/index.tsx +++ b/src/routes/popup/settings/apps/[url]/index.tsx @@ -28,7 +28,7 @@ import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; export default function AppSettings({ url }: Props) { // app settings - const app = new Application(url); + const app = new Application(decodeURIComponent(url)); const [settings, updateSettings] = app.hook(); const arweave = new Arweave(defaultGateway); From c4db4ba97e71a796a9c98343d6146831f5321601 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 12:23:24 +0545 Subject: [PATCH 22/31] fix: all settings border color --- src/routes/dashboard/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index b15f92b19..2cea2464e 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -279,7 +279,6 @@ const Panel = styled(Card)<{ }>` position: relative; border-radius: 0; - border: 0; ${(props) => props.showRightBorder && `border-right: 1.5px solid #8e7bea;`} padding: ${(props) => (props.normalPadding ? "1.5rem 1rem" : "1.5rem")}; overflow-y: auto; @@ -300,7 +299,7 @@ const Panel = styled(Card)<{ flex-grow: 1; &:nth-child(2) { - border: 0; + border-right: 1px solid rgb(${(props) => props.theme.cardBorder}); } &:last-child { @@ -312,7 +311,7 @@ const Panel = styled(Card)<{ @media screen and (max-width: 645px) { width: 100%; height: 55vh; - border: 0; + border-right: 1px solid rgb(${(props) => props.theme.cardBorder}); &:last-child { height: auto; From 49641a2321b52733e2dba493d2d082178c058aa3 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 12:37:40 +0545 Subject: [PATCH 23/31] fix: Update search input background color --- src/components/dashboard/Contacts.tsx | 7 ++++--- src/routes/popup/settings/apps/index.tsx | 2 +- src/routes/popup/settings/wallets/index.tsx | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/dashboard/Contacts.tsx b/src/components/dashboard/Contacts.tsx index 9a755ee4a..1bc1c1e91 100644 --- a/src/components/dashboard/Contacts.tsx +++ b/src/components/dashboard/Contacts.tsx @@ -135,7 +135,7 @@ export default function Contacts({ isQuickSetting }: ContactsProps) { return ( - + ` position: sticky; display: grid; gap: 8px; @@ -218,7 +218,8 @@ const SearchWrapper = styled.div` right: 0; z-index: 20; grid-template-columns: auto auto; - background-color: rgb(${(props) => props.theme.cardBackground}); + ${(props) => + !props.small && `background-color: rgb(${props.theme.cardBackground})`} `; const AddContactButton = styled(ButtonV2)` diff --git a/src/routes/popup/settings/apps/index.tsx b/src/routes/popup/settings/apps/index.tsx index 6eec41e51..53d4b0814 100644 --- a/src/routes/popup/settings/apps/index.tsx +++ b/src/routes/popup/settings/apps/index.tsx @@ -122,6 +122,7 @@ export default function Applications() { {connectedApps && connectedApps.length === 0 && ( {browser.i18n.getMessage("no_apps_added")} )} + ); @@ -144,7 +145,6 @@ const SearchWrapper = styled.div` left: 0; right: 0; z-index: 20; - background-color: rgb(${(props) => props.theme.cardBackground}); `; const NoAppsText = styled(Text)` diff --git a/src/routes/popup/settings/wallets/index.tsx b/src/routes/popup/settings/wallets/index.tsx index d1226ff65..9600e358c 100644 --- a/src/routes/popup/settings/wallets/index.tsx +++ b/src/routes/popup/settings/wallets/index.tsx @@ -88,6 +88,7 @@ export default function Wallets() {
From 76040cc88a7be689a88f1615faef8385ccf65b8f Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 13:21:02 +0545 Subject: [PATCH 24/31] refactor: Add qr generate button & page for wallets --- assets/_locales/en/messages.json | 4 + assets/_locales/zh_CN/messages.json | 4 + src/popup.tsx | 8 +- src/routes/popup/receive.tsx | 49 ++++-- .../settings/wallets/[address]/index.tsx | 140 ++++++------------ .../popup/settings/wallets/[address]/qr.tsx | 26 ++++ 6 files changed, 121 insertions(+), 110 deletions(-) create mode 100644 src/routes/popup/settings/wallets/[address]/qr.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 881a64d6b..163edbc78 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1303,6 +1303,10 @@ "message": "Scan the QR code below with your hardware wallet", "description": "Request to scan signature QR" }, + "generate_qr_code": { + "message": "Generate QR code", + "description": "Generate QR code button" + }, "viewblock": { "message": "Viewblock", "description": "view block" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 8c23f0d29..a2bf5b9db 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1299,6 +1299,10 @@ "message": "使用您的硬件钱包扫描下面的二维码", "description": "Request to scan signature QR" }, + "generate_qr_code": { + "message": "生成二维码", + "description": "Generate QR code button" + }, "viewblock": { "message": "Viewblock", "description": "view block" diff --git a/src/popup.tsx b/src/popup.tsx index 034d0f33a..75fa9afe1 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -48,6 +48,7 @@ import Contacts from "~routes/popup/settings/contacts"; import ContactSettings from "~routes/popup/settings/contacts/[address]"; import NewContact from "~routes/popup/settings/contacts/new"; import NotificationSettings from "~routes/popup/settings/notifications"; +import GenerateQR from "~routes/popup/settings/wallets/[address]/qr"; export default function Popup() { const theme = useTheme(); @@ -83,7 +84,7 @@ export default function Popup() { )} - + {() => } {(params: { id?: string }) => } @@ -107,6 +108,11 @@ export default function Popup() { )} + + {(params: { address: string }) => ( + + )} + {(params: { url: string }) => ( diff --git a/src/routes/popup/receive.tsx b/src/routes/popup/receive.tsx index 69a0b945a..081192fac 100644 --- a/src/routes/popup/receive.tsx +++ b/src/routes/popup/receive.tsx @@ -1,10 +1,4 @@ -import { - ButtonV2, - Section, - TooltipV2, - useToasts, - type DisplayTheme -} from "@arconnect/components"; +import { ButtonV2, Section, TooltipV2, useToasts } from "@arconnect/components"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; import { CheckIcon, CopyIcon } from "@iconicicons/react"; @@ -13,14 +7,20 @@ import { QRCodeSVG } from "qrcode.react"; import browser from "webextension-polyfill"; import styled from "styled-components"; import copy from "copy-to-clipboard"; -import { useEffect, type MouseEventHandler, useState } from "react"; +import { useEffect, type MouseEventHandler, useState, useMemo } from "react"; import { PageType, trackPage } from "~utils/analytics"; import HeadV2 from "~components/popup/HeadV2"; import { Degraded, WarningWrapper } from "./send"; import { WarningIcon } from "~components/popup/Token"; import { useActiveWallet } from "~wallets/hooks"; +import { useLocation } from "wouter"; -export default function Receive() { +interface ReceiveProps { + walletName?: string; + walletAddress?: string; +} + +export default function Receive({ walletName, walletAddress }: ReceiveProps) { // active address const [activeAddress] = useStorage({ key: "active_address", @@ -28,25 +28,35 @@ export default function Receive() { }); const [copied, setCopied] = useState(false); + const effectiveAddress = useMemo( + () => walletAddress || activeAddress, + [walletAddress, activeAddress] + ); + //segment useEffect(() => { - trackPage(PageType.RECEIVE); + if (!walletName && !walletAddress) { + trackPage(PageType.RECEIVE); + } }, []); const { setToast } = useToasts(); + // location + const [, setLocation] = useLocation(); + const wallet = useActiveWallet(); const keystoneWarning = wallet?.type === "hardware"; const copyAddress: MouseEventHandler = (e) => { e.stopPropagation(); - copy(activeAddress); + copy(effectiveAddress); setCopied(true); setTimeout(() => setCopied(false), 1000); setToast({ type: "success", duration: 2000, - content: `${formatAddress(activeAddress, 3)} ${browser.i18n.getMessage( + content: `${formatAddress(effectiveAddress, 3)} ${browser.i18n.getMessage( "copied_address_2" )}` }); @@ -55,7 +65,16 @@ export default function Receive() { return (
- + { + if (walletName && walletAddress) { + setLocation(`/quick-settings/wallets/${walletAddress}`); + } else { + setLocation("/"); + } + }} + />
{keystoneWarning && ( @@ -75,13 +94,13 @@ export default function Receive() { fgColor="#fff" bgColor="transparent" size={275} - value={activeAddress ?? ""} + value={effectiveAddress ?? ""} />
- {formatAddress(activeAddress ?? "", 6)} + {formatAddress(effectiveAddress ?? "", 6)}
- -
- - {ansLabel || wallet.nickname} - {wallet.type === "hardware" && ( - - - - )} - - - {formatAddress(wallet.address, 8)} +
+ + {ansLabel || wallet.nickname} + {wallet.type === "hardware" && ( - { - copy(wallet.address); - setToast({ - type: "info", - content: browser.i18n.getMessage("copied_address", [ - wallet.nickname, - formatAddress(wallet.address, 3) - ]), - duration: 2200 - }); - }} + - -
- - + )} + + + {formatAddress(wallet.address, 8)} + + { + copy(wallet.address); + setToast({ + type: "info", + content: browser.i18n.getMessage("copied_address", [ + wallet.nickname, + formatAddress(wallet.address, 3) + ]), + duration: 2200 + }); + }} + /> + + +
+ {browser.i18n.getMessage("edit_wallet_name")} {!!ansLabel && ( {browser.i18n.getMessage("cannot_edit_with_ans")} @@ -198,13 +196,22 @@ export default function Wallet({ address }: Props) {
setLocation(`/quick-settings/wallets/${address}/qr`)} + > + {browser.i18n.getMessage("generate_qr_code")} + + + + setLocation(`/quick-settings/wallets/${address}/export`) } disabled={wallet.type === "hardware"} > - {browser.i18n.getMessage("export_keyfile")} + removeModal.setOpen(true)} > - {browser.i18n.getMessage("remove_wallet")} +
- qrModal.setOpen(true)}> - - - - - - - - - ); -} - -const LogoWrapper = styled.div<{ small?: boolean }>` - display: flex; - align-items: center; - justify-content: center; - width: ${(props) => (props.small ? "2.1875rem" : "2.8rem;")}; - height: ${(props) => (props.small ? "2.1875rem" : "2.8rem;")}; - background-color: ${(props) => props.theme.primary}; - cursor: pointer; - border-radius: 11.905px; - - &:hover { - background-color: ${(props) => props.theme.primaryBtnHover}; - } -`; - const CenterText = styled(Text)` text-align: center; `; @@ -344,11 +301,6 @@ const WalletName = styled(Text).attrs({ font-weight: 600; `; -const WalletWrapper = styled.div` - display: flex; - justify-content: space-between; -`; - const HardwareWalletIcon = styled.img.attrs({ draggable: false })` diff --git a/src/routes/popup/settings/wallets/[address]/qr.tsx b/src/routes/popup/settings/wallets/[address]/qr.tsx new file mode 100644 index 000000000..0bd00f905 --- /dev/null +++ b/src/routes/popup/settings/wallets/[address]/qr.tsx @@ -0,0 +1,26 @@ +import { useStorage } from "@plasmohq/storage/hook"; +import { useMemo } from "react"; +import Receive from "~routes/popup/receive"; +import { ExtensionStorage } from "~utils/storage"; +import type { StoredWallet } from "~wallets"; + +export default function GenerateQR({ address }: { address: string }) { + // wallets + const [wallets] = useStorage( + { + key: "wallets", + instance: ExtensionStorage + }, + [] + ); + + // this wallet + const wallet = useMemo( + () => wallets?.find((w) => w.address === address), + [wallets, address] + ); + + if (!wallet) return <>; + + return ; +} From 2bc3d5d3394e6b60d11b6f9fa44683b545023782 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 13:24:51 +0545 Subject: [PATCH 25/31] fix: Return to wallets list after wallet deletion --- src/routes/popup/settings/wallets/[address]/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/popup/settings/wallets/[address]/index.tsx b/src/routes/popup/settings/wallets/[address]/index.tsx index 257151ec4..998aa2fa2 100644 --- a/src/routes/popup/settings/wallets/[address]/index.tsx +++ b/src/routes/popup/settings/wallets/[address]/index.tsx @@ -247,6 +247,7 @@ export default function Wallet({ address }: Props) { ), duration: 2000 }); + setLocation("/quick-settings/wallets"); } catch (e) { console.log("Error removing wallet", e); setToast({ From 0d2b56cdeed4cdef566848dccb5f9f7cb74bd892 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 17:15:48 +0545 Subject: [PATCH 26/31] fix: Hide expand view on expanded view --- src/components/popup/WalletHeader.tsx | 89 ++++++++++++++------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/components/popup/WalletHeader.tsx b/src/components/popup/WalletHeader.tsx index 3828ea932..27743c2ba 100644 --- a/src/components/popup/WalletHeader.tsx +++ b/src/components/popup/WalletHeader.tsx @@ -21,7 +21,6 @@ import { TooltipV2 } from "@arconnect/components"; import { - ArrowUpRightIcon, CheckIcon, ChevronDownIcon, CopyIcon, @@ -210,6 +209,52 @@ export default function WalletHeader() { // copied address const [copied, setCopied] = useState(false); + const menuItems = useMemo(() => { + const items = [ + { + icon: , + title: "setting_contacts", + route: () => + browser.tabs.create({ + url: browser.runtime.getURL("tabs/dashboard.html#/contacts") + }) + }, + { + icon: , + title: "Viewblock", + route: () => + browser.tabs.create({ + url: `https://viewblock.io/arweave/address/${activeAddress}` + }) + }, + { + icon: , + title: "subscriptions", + route: () => { + push("/subscriptions"); + } + }, + { + icon: , + title: "Settings", + route: () => { + push("/quick-settings"); + } + } + ]; + if (!isExpanded && items.length === 4) { + items.push({ + icon: , + title: "expand_view", + route: () => { + window.open(window.location.href.split("#")[0] + "?expanded=true"); + } + }); + } + + return items; + }, [activeAddress, isExpanded]); + return ( 14}> @@ -411,47 +456,7 @@ export default function WalletHeader() { setMenuOpen(false)} - menuItems={[ - { - icon: , - title: "setting_contacts", - route: () => - browser.tabs.create({ - url: browser.runtime.getURL("tabs/dashboard.html#/contacts") - }) - }, - { - icon: , - title: "Viewblock", - route: () => - browser.tabs.create({ - url: `https://viewblock.io/arweave/address/${activeAddress}` - }) - }, - { - icon: , - title: "subscriptions", - route: () => { - push("/subscriptions"); - } - }, - { - icon: , - title: "Settings", - route: () => { - push("/quick-settings"); - } - }, - { - icon: , - title: "expand_view", - route: () => { - window.open( - window.location.href.split("#")[0] + "?expanded=true" - ); - } - } - ]} + menuItems={menuItems} /> ); From b5762a0674d207caab2ce15ae4079778980b6add Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 17:30:07 +0545 Subject: [PATCH 27/31] refactor: Comment ArNS names fetching & select input in contact --- .../dashboard/subsettings/AddContact.tsx | 41 ++++++++++--------- .../dashboard/subsettings/ContactSettings.tsx | 39 +++++++++--------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/components/dashboard/subsettings/AddContact.tsx b/src/components/dashboard/subsettings/AddContact.tsx index 1a87e951a..630485197 100644 --- a/src/components/dashboard/subsettings/AddContact.tsx +++ b/src/components/dashboard/subsettings/AddContact.tsx @@ -28,13 +28,13 @@ import { useEffect, useState } from "react"; import browser from "webextension-polyfill"; import { findGateway } from "~gateways/wayfinder"; import { uploadUserAvatar, getUserAvatar } from "~lib/avatar"; -import { getAllArNSNames } from "~lib/arns"; +// import { getAllArNSNames } from "~lib/arns"; import styled from "styled-components"; import { useLocation } from "wouter"; import copy from "copy-to-clipboard"; import { gql } from "~gateways/api"; import { useTheme } from "~utils/theme"; -import { isAddressFormat } from "~utils/format"; +// import { isAddressFormat } from "~utils/format"; export default function AddContact() { // contacts @@ -56,7 +56,6 @@ export default function AddContact() { const { setToast } = useToasts(); const [location] = useLocation(); const address = location.split("=")[1]; - const [loading, setLoading] = useState(false); const [contact, setContact] = useState({ name: "", @@ -73,7 +72,8 @@ export default function AddContact() { } }, []); - const [arnsResults, setArnsResults] = useState([]); + // const [loading, setLoading] = useState(false); + // const [arnsResults, setArnsResults] = useState([]); const [lastRecipients, setLastRecipients] = useState([]); const generateProfileIcon = (name, address) => { @@ -138,22 +138,23 @@ export default function AddContact() { }); }; - async function fetchArnsAddresses(ownerAddress) { - try { - setLoading(true); - const arnsNames = await getAllArNSNames(ownerAddress); - setArnsResults(arnsNames || []); - } catch (error) { - console.error("Error fetching ArNS addresses:", error); - } finally { - setLoading(false); - } - } + // TODO: Uncomment when getAllArNSNames is optimized + // async function fetchArnsAddresses(ownerAddress) { + // try { + // setLoading(true); + // const arnsNames = await getAllArNSNames(ownerAddress); + // setArnsResults(arnsNames || []); + // } catch (error) { + // console.error("Error fetching ArNS addresses:", error); + // } finally { + // setLoading(false); + // } + // } useEffect(() => { - if (contact.address && isAddressFormat(contact.address)) { - fetchArnsAddresses(contact.address); - } + // if (contact.address && isAddressFormat(contact.address)) { + // fetchArnsAddresses(contact.address); + // } (async () => { if (!activeAddress) return; @@ -319,7 +320,7 @@ export default function AddContact() { ))} - {browser.i18n.getMessage("ArNS_address")} + {/* {browser.i18n.getMessage("ArNS_address")} ))} - + */} {browser.i18n.getMessage("notes")} { const { name, value } = e.target; @@ -200,7 +201,7 @@ export default function ContactSettings({ address }: Props) { if (editable) { return ( <> - {browser.i18n.getMessage("ArNS_address")} + {/* {browser.i18n.getMessage("ArNS_address")} ))} - + */} ); } else if (contact.ArNSAddress) { From afdc639cc8079301ad5ec86e99c154e04603d06d Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 18:31:36 +0545 Subject: [PATCH 28/31] fix: Show correct fiat amount for AR in transactions history --- src/components/popup/home/Transactions.tsx | 56 ++--------------- src/lib/transactions.ts | 62 +++++++++++++++++++ src/routes/popup/transaction/transactions.tsx | 60 +++--------------- 3 files changed, 78 insertions(+), 100 deletions(-) diff --git a/src/components/popup/home/Transactions.tsx b/src/components/popup/home/Transactions.tsx index 83950e855..e4ea44b10 100644 --- a/src/components/popup/home/Transactions.tsx +++ b/src/components/popup/home/Transactions.tsx @@ -6,7 +6,6 @@ import { Loading, Text } from "@arconnect/components"; import { gql } from "~gateways/api"; import styled from "styled-components"; -import { balanceToFractioned, formatFiatBalance } from "~tokens/currency"; import { AO_RECEIVER_QUERY, AO_SENT_QUERY, @@ -20,6 +19,10 @@ import { suggestedGateways } from "~gateways/gateway"; import { Spacer } from "@arconnect/components"; import { Heading, ViewAll, TokenCount } from "../Title"; import { + getFormattedAmount, + getFormattedFiatAmount, + getFullMonthName, + getTransactionDescription, processTransactions, sortFn, type ExtendedTransaction @@ -118,49 +121,6 @@ export default function Transactions() { push(`/transaction/${id}?back=${encodeURIComponent("/transactions")}`); }; - const getFormattedAmount = (transaction: ExtendedTransaction) => { - switch (transaction.transactionType) { - case "sent": - case "received": - return `${parseFloat(transaction.node.quantity.ar).toFixed(3)} AR`; - case "aoSent": - case "aoReceived": - if (transaction.aoInfo) { - return `${balanceToFractioned(transaction.aoInfo.quantity, { - divisibility: transaction.aoInfo.denomination - }).toFixed()} ${transaction.aoInfo.tickerName}`; - } - return ""; - default: - return ""; - } - }; - - const getTransactionDescription = (transaction: ExtendedTransaction) => { - switch (transaction.transactionType) { - case "sent": - return `${browser.i18n.getMessage("sent")} AR`; - case "received": - return `${browser.i18n.getMessage("received")} AR`; - case "aoSent": - return `${browser.i18n.getMessage("sent")} ${ - transaction.aoInfo.tickerName - }`; - case "aoReceived": - return `${browser.i18n.getMessage("received")} ${ - transaction.aoInfo.tickerName - }`; - default: - return ""; - } - }; - - const getFullMonthName = (monthYear: string) => { - const [month, year] = monthYear.split("-").map(Number); - const date = new Date(year, month - 1); - return date.toLocaleString("default", { month: "long" }); - }; - return ( <> @@ -170,7 +130,7 @@ export default function Transactions() { - 0}> + 0 && !loading}> {!loading && (transactions.length > 0 ? ( transactions.map((transaction, index) => ( @@ -192,11 +152,7 @@ export default function Transactions() {
{getFormattedAmount(transaction)}
- {transaction.node.quantity && - formatFiatBalance( - transaction.node.quantity.ar, - currency - )} + {getFormattedFiatAmount(transaction, arPrice, currency)}
diff --git a/src/lib/transactions.ts b/src/lib/transactions.ts index cb2c348a8..47d30e85e 100644 --- a/src/lib/transactions.ts +++ b/src/lib/transactions.ts @@ -6,6 +6,9 @@ import { formatAddress } from "~utils/format"; import { ExtensionStorage } from "~utils/storage"; import { getTokenInfo } from "~tokens/aoTokens/router"; import type { Token } from "~tokens/token"; +import BigNumber from "bignumber.js"; +import browser from "webextension-polyfill"; +import { balanceToFractioned, formatFiatBalance } from "~tokens/currency"; let tokens: TokenInfo[] = null; export let tokenInfoMap = new Map(); @@ -126,3 +129,62 @@ export const processTransactions = async ( return Promise.resolve([]); } }; + +export const getFormattedAmount = (transaction: ExtendedTransaction) => { + switch (transaction.transactionType) { + case "sent": + case "received": + return `${parseFloat(transaction.node.quantity.ar).toFixed(3)} AR`; + case "aoSent": + case "aoReceived": + if (transaction.aoInfo) { + return `${balanceToFractioned(transaction.aoInfo.quantity, { + divisibility: transaction.aoInfo.denomination + }).toFixed()} ${transaction.aoInfo.tickerName}`; + } + return ""; + default: + return ""; + } +}; + +export const getFormattedFiatAmount = ( + transaction: ExtendedTransaction, + arPrice: number, + currency: string +) => { + try { + if (transaction.node.quantity) { + const fiatBalance = BigNumber(transaction.node.quantity.ar).multipliedBy( + arPrice + ); + return formatFiatBalance(fiatBalance, currency); + } + } catch {} + return ""; +}; + +export const getTransactionDescription = (transaction: ExtendedTransaction) => { + switch (transaction.transactionType) { + case "sent": + return `${browser.i18n.getMessage("sent")} AR`; + case "received": + return `${browser.i18n.getMessage("received")} AR`; + case "aoSent": + return `${browser.i18n.getMessage("sent")} ${ + transaction.aoInfo.tickerName + }`; + case "aoReceived": + return `${browser.i18n.getMessage("received")} ${ + transaction.aoInfo.tickerName + }`; + default: + return ""; + } +}; + +export const getFullMonthName = (monthYear: string) => { + const [month, year] = monthYear.split("-").map(Number); + const date = new Date(year, month - 1); + return date.toLocaleString("default", { month: "long" }); +}; diff --git a/src/routes/popup/transaction/transactions.tsx b/src/routes/popup/transaction/transactions.tsx index 3ab4592d9..b476b310c 100644 --- a/src/routes/popup/transaction/transactions.tsx +++ b/src/routes/popup/transaction/transactions.tsx @@ -7,7 +7,6 @@ import { useStorage } from "@plasmohq/storage/hook"; import { gql } from "~gateways/api"; import styled from "styled-components"; import { Empty, TitleMessage } from "../notifications"; -import { balanceToFractioned, formatFiatBalance } from "~tokens/currency"; import { AO_RECEIVER_QUERY_WITH_CURSOR, AO_SENT_QUERY_WITH_CURSOR, @@ -24,7 +23,11 @@ import { sortFn, processTransactions, type GroupedTransactions, - type ExtendedTransaction + type ExtendedTransaction, + getFormattedAmount, + getFormattedFiatAmount, + getFullMonthName, + getTransactionDescription } from "~lib/transactions"; const defaultCursors = ["", "", "", ""]; @@ -190,49 +193,6 @@ export default function Transactions() { push(`/transaction/${id}?back=${encodeURIComponent("/transactions")}`); }; - const getFormattedAmount = (transaction: ExtendedTransaction) => { - switch (transaction.transactionType) { - case "sent": - case "received": - return `${parseFloat(transaction.node.quantity.ar).toFixed(3)} AR`; - case "aoSent": - case "aoReceived": - if (transaction.aoInfo) { - return `${balanceToFractioned(transaction.aoInfo.quantity, { - divisibility: transaction.aoInfo.denomination - }).toFixed()} ${transaction.aoInfo.tickerName}`; - } - return ""; - default: - return ""; - } - }; - - const getTransactionDescription = (transaction: ExtendedTransaction) => { - switch (transaction.transactionType) { - case "sent": - return `${browser.i18n.getMessage("sent")} AR`; - case "received": - return `${browser.i18n.getMessage("received")} AR`; - case "aoSent": - return `${browser.i18n.getMessage("sent")} ${ - transaction.aoInfo.tickerName - }`; - case "aoReceived": - return `${browser.i18n.getMessage("received")} ${ - transaction.aoInfo.tickerName - }`; - default: - return ""; - } - }; - - const getFullMonthName = (monthYear: string) => { - const [month, year] = monthYear.split("-").map(Number); - const date = new Date(year, month - 1); - return date.toLocaleString("default", { month: "long" }); - }; - return ( <> @@ -260,11 +220,11 @@ export default function Transactions() {
{getFormattedAmount(transaction)}
- {transaction.node.quantity && - formatFiatBalance( - transaction.node.quantity.ar, - currency - )} + {getFormattedFiatAmount( + transaction, + arPrice, + currency + )}
From 5259a9421c453e7865a52e9def1b536c44e96c18 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Fri, 5 Jul 2024 08:39:17 -0700 Subject: [PATCH 29/31] chore: added bits check --- src/routes/popup/index.tsx | 22 ++++++++++++- src/utils/analytics.ts | 64 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx index 163642239..72760a5a1 100644 --- a/src/routes/popup/index.tsx +++ b/src/routes/popup/index.tsx @@ -7,7 +7,13 @@ import Balance from "~components/popup/home/Balance"; import { AnnouncementPopup } from "./announcement"; import { getDecryptionKey, isExpired } from "~wallets/auth"; import { useHistory } from "~utils/hash_router"; -import { trackEvent, EventType, trackPage, PageType } from "~utils/analytics"; +import { + trackEvent, + EventType, + trackPage, + PageType, + checkWalletBits +} from "~utils/analytics"; import styled from "styled-components"; import { useTokens } from "~tokens"; import { useAoTokens } from "~tokens/aoTokens/ao"; @@ -95,6 +101,20 @@ export default function Home() { checkExpiration(); }, []); + useEffect(() => { + const checkBits = async () => { + const bits = await checkWalletBits(); + + if (bits === null) { + return; + } else { + await trackEvent(EventType.BITS_LENGTH, { mismatch: bits }); + } + }; + + checkBits(); + }, []); + useEffect(() => { // check whether to show announcement (async () => { diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 31e996ab8..544b84514 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -1,13 +1,16 @@ import { getSetting } from "~settings"; import { ExtensionStorage, TempTransactionStorage } from "./storage"; import { AnalyticsBrowser } from "@segment/analytics-next"; -import { getWallets } from "~wallets"; +import { getWallets, getActiveKeyfile, getActiveAddress } from "~wallets"; import Arweave from "arweave"; import { defaultGateway } from "~gateways/gateway"; import { v4 as uuid } from "uuid"; import browser, { type Alarms } from "webextension-polyfill"; import BigNumber from "bignumber.js"; import axios from "axios"; +import { isLocalWallet } from "./assertions"; +import { ArweaveSigner } from "arbundles"; +import { freeDecryptedWallet } from "~wallets/encryption"; const PUBLIC_SEGMENT_WRITEKEY = "J97E4cvSZqmpeEdiUQNC2IxS1Kw4Cwxm"; @@ -36,7 +39,8 @@ export enum EventType { TX_SENT = "TX_SENT", SUBSCRIBED = "SUBSCRIBED", UNSUBSCRIBED = "UNSUBSCRIBED", - SUBSCRIPTION_PAYMENT = "SUBSCRIPTION_PAYMENT" + SUBSCRIPTION_PAYMENT = "SUBSCRIPTION_PAYMENT", + BITS_LENGTH = "BITS_LENGTH" } export enum PageType { @@ -230,6 +234,62 @@ const setToStartOfNextMonth = (currentDate: Date): Date => { return newDate; }; +/** + * Checks the bit length the active Arweave wallet. + * + * This function verifies the integrity of the currently active wallet by comparing + * the expected length of the public key with its actual length. It uses the ArweaveSigner + * to generate the public key from the wallet's keyfile. + * + * + * @returns {Promise} A promise that resolves to: + * - true if an integrity issue is detected (lengths don't match) + * - false if no integrity issue is found + * - null if the check has already been performed for this address or if an error occurs + * + * @throws {Error} If no wallets are added or if there's an issue accessing the wallet + */ +export const checkWalletBits = async (): Promise => { + const activeAddress = await getActiveAddress(); + if (!activeAddress) { + return null; + } + const hasBeenTracked = await ExtensionStorage.get( + `bits_check_${activeAddress}` + ); + + if (hasBeenTracked) { + return null; + } + + try { + const decryptedWallet = await getActiveKeyfile().catch((e) => { + throw new Error("No wallets added"); + }); + isLocalWallet(decryptedWallet); + + const signer = new ArweaveSigner(decryptedWallet.keyfile); + const owner = signer.publicKey; + const expectedLength = signer.ownerLength; + const actualLength = owner.byteLength; + + const lengthsMatch = expectedLength === actualLength; + + freeDecryptedWallet(decryptedWallet.keyfile); + + await ExtensionStorage.set(`bits_check_${activeAddress}`, true); + + return !lengthsMatch; + } catch (error) { + console.error( + `An error occurred during wallet integrity check: ${ + error instanceof Error ? error.message : String(error) + }` + ); + return null; + } +}; + const GDPR_COUNTRIES_AND_OTHERS = [ "AT", // Austria "BE", // Belgium From da64fe062298e007a19fa5f343b735441def6372 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Fri, 5 Jul 2024 08:42:57 -0700 Subject: [PATCH 30/31] fix: typo --- src/utils/apps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/apps.ts b/src/utils/apps.ts index 644a5f8d3..ac9f14fdd 100644 --- a/src/utils/apps.ts +++ b/src/utils/apps.ts @@ -266,7 +266,7 @@ export const apps: App[] = [ thumbnail: "/apps/arwiki/thumbnail.jpeg" }, links: { - website: "https://arwiki.wik", + website: "https://arwiki.wiki", twitter: "https://x.com/TheArWiki" } }, From 9355344a550f13a1701dcd2d39473bc51d1ce4cc Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 5 Jul 2024 22:23:56 +0545 Subject: [PATCH 31/31] fix: Add spacer to contacts --- src/components/dashboard/Contacts.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/Contacts.tsx b/src/components/dashboard/Contacts.tsx index 1bc1c1e91..cb6ef838e 100644 --- a/src/components/dashboard/Contacts.tsx +++ b/src/components/dashboard/Contacts.tsx @@ -188,6 +188,7 @@ export default function Contacts({ isQuickSetting }: ContactsProps) { ); })} + );