diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 000000000..bfba45975 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{sh,bash}] +end_of_line = lf diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..aa4064161 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PLASMO_PUBLIC_APP_TYPE=extension diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6b97907bb..0624054c9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,6 +2,8 @@ Thank you for your interest in contributing to the ArConnect extension! Below are the guidelines to help you get started. +> **Important:** Always work from the "development" branch when making contributions. This ensures your changes are based on the latest development version. + ## Building the Project 1. **Clone the Repository:** Clone this repository to your local machine. diff --git a/.gitignore b/.gitignore index d2eb8083f..542768c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,4 @@ keys.json .tsbuildinfo # temporary remove development icon -assets/icon.development.png \ No newline at end of file +assets/icon.development.png diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index b51ba1e8c..a3309f0d7 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1083,6 +1083,10 @@ "message": "View all", "description": "View all link text" }, + "view_all_assets": { + "message": "View all assets", + "description": "View all link text" + }, "setting_tokens": { "message": "Tokens", "description": "Tokens setting title" @@ -1560,7 +1564,7 @@ "description": "Not connected indicator" }, "not_connected_text": { - "message": "This app is not yet using ArConnect. Find the \"Connect\" button on the page to connect it.", + "message": "\"Connect / Disconnect\" is an indicator for when your ArConnect wallet is connected to an Arweave or AO application. For regular use like sending, receiving, checking balances, etc., ArConnect will always show disconnected.", "description": "Not connected indicator explainer" }, "disconnect": { @@ -2221,5 +2225,65 @@ "transak_unavailable": { "message": "Transak is unavailable at the moment. Please try again later.", "description": "Transak unavailable error message" + }, + "print_archived": { + "message": "Print archived", + "description": "Print archived description" + }, + "new_data_uploaded": { + "message": "New data uploaded", + "description": "New data uploaded description" + }, + "sent_balance": { + "message": "Sent $QTY$ $TICKER$ to $ADDRESS$", + "description": "Sent balance description", + "placeholders": { + "qty": { + "content": "$1", + "example": "10" + }, + "ticker": { + "content": "$2", + "example": "AR" + }, + "address": { + "content": "$3", + "example": "ljvCPN31...-CS-6Iho8U" + } + } + }, + "received_balance": { + "message": "Received $QTY$ $TICKER$ from $ADDRESS$", + "description": "Received balance description", + "placeholders": { + "qty": { + "content": "$1", + "example": "10" + }, + "ticker": { + "content": "$2", + "example": "AR" + }, + "address": { + "content": "$3", + "example": "ljvCPN31...-CS-6Iho8U" + } + } + }, + "notification_from": { + "message": "from", + "description": "From description" + }, + "notification_to": { + "message": "to", + "description": "To description" + }, + "new_message": { + "message": "New message", + "description": "New message description" + }, + "new_transaction": { + "message": "New transaction", + "description": "New transaction description" } } diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index e89c42830..199c4db13 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1071,6 +1071,10 @@ "message": "查看全部", "description": "View all link text" }, + "view_all_assets": { + "message": "查看所有资产", + "description": "View all assets link text" + }, "setting_tokens": { "message": "代币", "description": "Tokens setting title" @@ -1549,7 +1553,7 @@ "description": "Not connected indicator" }, "not_connected_text": { - "message": "此应用尚未使用 ArConnect。找到页面上的“连接”按钮以连接。", + "message": "“连接/断开连接”是指示您的 ArConnect 钱包是否已连接到 Arweave 或 AO 应用程序的指示器。对于发送、接收、检查余额等常规使用,ArConnect 将始终显示断开连接。", "description": "Not connected indicator explainer" }, "disconnect": { @@ -2207,5 +2211,65 @@ "transak_unavailable": { "message": "Transak 目前不可用。请稍后再试。", "description": "Transak unavailable error message" - } + }, + "print_archived": { + "message": "打印已归档", + "description": "Print archived description" + }, + "new_data_uploaded": { + "message": "新数据已上传", + "description": "New data uploaded description" + }, + "sent_balance": { + "message": "已发送 $QTY$ $TICKER$ 给 $ADDRESS$", + "description": "Sent balance description", + "placeholders": { + "qty": { + "content": "$1", + "example": "10" + }, + "ticker": { + "content": "$2", + "example": "AR" + }, + "address": { + "content": "$3", + "example": "ljvCPN31...-CS-6Iho8U" + } + } + }, + "received_balance": { + "message": "收到 $QTY$ $TICKER$ 来自 $ADDRESS$", + "description": "Received balance description", + "placeholders": { + "qty": { + "content": "$1", + "example": "10" + }, + "ticker": { + "content": "$2", + "example": "AR" + }, + "address": { + "content": "$3", + "example": "ljvCPN31...-CS-6Iho8U" + } + } + }, + "notification_from": { + "message": "来自", + "description": "From description" + }, + "notification_to": { + "message": "至", + "description": "To description" + }, + "new_message": { + "message": "新消息", + "description": "New message description" + }, + "new_transaction": { + "message": "新交易", + "description": "New transaction description" + } } diff --git a/assets/icon512.development.png b/assets/icon512.development.png new file mode 100644 index 000000000..abe82b60b Binary files /dev/null and b/assets/icon512.development.png differ diff --git a/assets/icons/icon.development.png b/assets/icons/icon.development.png new file mode 100644 index 000000000..67e7b9398 Binary files /dev/null and b/assets/icons/icon.development.png differ diff --git a/assets/icons/offline/logo128.development.png b/assets/icons/offline/logo128.development.png new file mode 100644 index 000000000..2e1cf70f9 Binary files /dev/null and b/assets/icons/offline/logo128.development.png differ diff --git a/assets/icons/offline/logo256.development.png b/assets/icons/offline/logo256.development.png new file mode 100644 index 000000000..778adca1b Binary files /dev/null and b/assets/icons/offline/logo256.development.png differ diff --git a/assets/icons/offline/logo64.development.png b/assets/icons/offline/logo64.development.png new file mode 100644 index 000000000..e2904a59c Binary files /dev/null and b/assets/icons/offline/logo64.development.png differ diff --git a/assets/icons/online/logo128.development.png b/assets/icons/online/logo128.development.png new file mode 100644 index 000000000..4d4a80f7f Binary files /dev/null and b/assets/icons/online/logo128.development.png differ diff --git a/assets/icons/online/logo256.development.png b/assets/icons/online/logo256.development.png new file mode 100644 index 000000000..921908055 Binary files /dev/null and b/assets/icons/online/logo256.development.png differ diff --git a/assets/icons/online/logo64.development.png b/assets/icons/online/logo64.development.png new file mode 100644 index 000000000..1d9e5a5e8 Binary files /dev/null and b/assets/icons/online/logo64.development.png differ diff --git a/assets/popup.css b/assets/popup.css new file mode 100644 index 000000000..7ce859145 --- /dev/null +++ b/assets/popup.css @@ -0,0 +1,122 @@ +@font-face { + font-family: "ManropeLocal"; + font-style: normal; + font-weight: 300; + src: url(/assets/fonts/Manrope-Light.woff2) format('woff2'); +} + +@font-face { + font-family: "ManropeLocal"; + font-style: normal; + font-weight: 400; + src: url(/assets/fonts/Manrope-Regular.woff2) format('woff2'); +} + +@font-face { + font-family: "ManropeLocal"; + font-style: normal; + font-weight: 500; + src: url(/assets/fonts/Manrope-Medium.woff2) format('woff2'); +} + +@font-face { + font-family: "ManropeLocal"; + font-style: normal; + font-weight: 600; + src: url(/assets/fonts/Manrope-SemiBold.woff2) format('woff2'); +} + +@font-face { + font-family: "ManropeLocal"; + font-style: normal; + font-weight: 600; + src: url(/assets/fonts/Manrope-Bold.woff2) format('woff2'); +} + +@font-face { + font-family: "ManropeLocal"; + font-style: normal; + font-weight: 700; + src: url(/assets/fonts/Manrope-ExtraBold.woff2) format('woff2'); +} + +* { + scrollbar-width: none; +} + +*::-webkit-scrollbar { + display: none +} + +::selection { + background-color: rgba(171, 154, 255, .6); + color: #fff; +} + +:root { + --defaultBackgroundColor: white; +} + +@media (prefers-color-scheme: dark) { + :root { + --defaultBackgroundColor: black; + } +} + +body { + margin: 0; + padding: 0; +} + +body#popup { + width: 377px; + height: 600px; +} + +body.fullscreen { + width: 377px; + height: 100vh; + margin: 0 auto; +} + +body.fullscreen::before { + content: ""; + position: fixed; + top: 50%; + left: 50%; + width: 377px; + height: 100vh; + transform: translate(-50%, -50%); + pointer-events: none; + box-shadow: 0 0 128px 0 rgba(171, 154, 255, .4); +} + +body, +button, +input, +select, +textarea { + font-family: "ManropeLocal", "Manrope VF", "Manrope", sans-serif !important; +} + +button { + background: transparent; + border: 0; + padding: 0; +} + +#cover { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 99999999; + background-color: var(--backgroundColor, var(--defaultBackgroundColor, white)); + transition: background-color linear 230ms; +} + +#cover[aria-hidden] { + background: transparent; + pointer-events: none; +} diff --git a/assets/popup.js b/assets/popup.js new file mode 100644 index 000000000..7c36b0bcb --- /dev/null +++ b/assets/popup.js @@ -0,0 +1,3 @@ +const backgroundColor = localStorage.getItem("ARCONNECT_THEME_BACKGROUND_COLOR"); + +if (backgroundColor) document.documentElement.style.setProperty('--backgroundColor', backgroundColor); \ No newline at end of file diff --git a/package.json b/package.json index 5b53d93e5..63896f3b3 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,11 @@ "packageManager": "yarn@1.22.18", "homepage": "https://arconnect.io", "scripts": { - "dev:chrome": "plasmo dev", + "dev:chrome": "plasmo dev --verbose", "build:chrome": "plasmo build --no-hoist", "dev:firefox": "plasmo dev --target=firefox-mv2", "build:firefox": "plasmo build --target=firefox-mv2 --no-hoist", + "nuke": "rm -rf node_modules build .plasmo", "fmt": "prettier --write .", "fmt:check": "prettier --check .", "prepare": "husky install" @@ -25,7 +26,8 @@ "contextMenus", "tabs", "webNavigation", - "notifications" + "notifications", + "printerProvider" ], "web_accessible_resources": [ { @@ -69,7 +71,7 @@ "check-password-strength": "^2.0.7", "copy-to-clipboard": "^3.3.2", "dayjs": "^1.11.6", - "framer-motion": "7.5.3", + "framer-motion": "^11.11.7", "human-crypto-keys": "^0.1.4", "js-confetti": "^0.11.0", "mitt": "^3.0.0", diff --git a/src/api/background.ts b/src/api/background.ts index 0be5da8e5..2b5df2526 100644 --- a/src/api/background.ts +++ b/src/api/background.ts @@ -45,6 +45,8 @@ import subscriptionModule from "./modules/subscription"; import subscription from "./modules/subscription/subscription.background"; import userTokensModule from "./modules/user_tokens"; import userTokens from "./modules/user_tokens/user_tokens.background"; +import tokenBalanceModule from "./modules/token_balance"; +import tokenBalance from "./modules/token_balance/token_balance.background"; /** Background modules */ const modules: BackgroundModule[] = [ @@ -69,6 +71,7 @@ const modules: BackgroundModule[] = [ { ...signDataItemModule, function: signDataItem }, { ...subscriptionModule, function: subscription }, { ...userTokensModule, function: userTokens }, + { ...tokenBalanceModule, function: tokenBalance }, { ...batchSignDataItemModule, function: batchSignDataItem } ]; diff --git a/src/api/foreground.ts b/src/api/foreground.ts index cd0ca12b2..bd079a7b7 100644 --- a/src/api/foreground.ts +++ b/src/api/foreground.ts @@ -65,6 +65,8 @@ import signDataItem, { } from "./modules/sign_data_item/sign_data_item.foreground"; import userTokensModule from "./modules/user_tokens"; import userTokens from "./modules/user_tokens/user_tokens.foreground"; +import tokenBalanceModule from "./modules/token_balance"; +import tokenBalance from "./modules/token_balance/token_balance.foreground"; /** Foreground modules */ const modules: ForegroundModule[] = [ @@ -101,6 +103,7 @@ const modules: ForegroundModule[] = [ }, { ...subscriptionModule, function: subscription }, { ...userTokensModule, function: userTokens }, + { ...tokenBalanceModule, function: tokenBalance }, { ...batchSignDataItemModule, function: batchSignDataItem, diff --git a/src/api/modules/token_balance/index.ts b/src/api/modules/token_balance/index.ts new file mode 100644 index 000000000..9e341fac4 --- /dev/null +++ b/src/api/modules/token_balance/index.ts @@ -0,0 +1,11 @@ +import type { PermissionType } from "~applications/permissions"; +import type { ModuleProperties } from "~api/module"; + +const permissions: PermissionType[] = ["ACCESS_TOKENS"]; + +const tokenBalanceModule: ModuleProperties = { + functionName: "tokenBalance", + permissions +}; + +export default tokenBalanceModule; diff --git a/src/api/modules/token_balance/token_balance.background.ts b/src/api/modules/token_balance/token_balance.background.ts new file mode 100644 index 000000000..6abd5eeaa --- /dev/null +++ b/src/api/modules/token_balance/token_balance.background.ts @@ -0,0 +1,20 @@ +import type { ModuleFunction } from "~api/background"; +import { ExtensionStorage } from "~utils/storage"; +import { getAoTokenBalance, getNativeTokenBalance } from "~tokens/aoTokens/ao"; +import { AO_NATIVE_TOKEN } from "~utils/ao_import"; +import { isAddress } from "~utils/assertions"; + +const background: ModuleFunction = async (_, id?: string) => { + // validate input + isAddress(id); + const address = await ExtensionStorage.get("active_address"); + + const balance = + id === AO_NATIVE_TOKEN + ? await getNativeTokenBalance(address) + : (await getAoTokenBalance(address, id)).toString(); + + return balance; +}; + +export default background; diff --git a/src/api/modules/token_balance/token_balance.foreground.ts b/src/api/modules/token_balance/token_balance.foreground.ts new file mode 100644 index 000000000..220afbdbe --- /dev/null +++ b/src/api/modules/token_balance/token_balance.foreground.ts @@ -0,0 +1,6 @@ +import type { ModuleFunction } from "~api/module"; + +// no need to transform anything in the foreground +const foreground: ModuleFunction = () => {}; + +export default foreground; diff --git a/src/background.ts b/src/background.ts index 40a15215e..bec171759 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,3 +1,4 @@ +import { getCapabilities, getPrinters, handlePrintRequest } from "~lib/printer"; import { addressChangeListener, walletsChangeListener } from "~wallets/event"; import { keyRemoveAlarmListener, onWindowClose } from "~wallets/auth"; import { appConfigChangeListener } from "~applications/events"; @@ -83,4 +84,12 @@ browser.runtime.onInstalled.addListener(onInstalled); // handle ar:// protocol browser.webNavigation.onBeforeNavigate.addListener(protocolHandler); +// print to the permaweb (only on chrome) +if (typeof chrome !== "undefined") { + // @ts-expect-error + chrome.printerProvider.onGetCapabilityRequested.addListener(getCapabilities); + chrome.printerProvider.onGetPrintersRequested.addListener(getPrinters); + chrome.printerProvider.onPrintRequested.addListener(handlePrintRequest); +} + export {}; diff --git a/src/components/SliderMenu.tsx b/src/components/SliderMenu.tsx index 8f30b4a59..f1466bd1e 100644 --- a/src/components/SliderMenu.tsx +++ b/src/components/SliderMenu.tsx @@ -7,6 +7,7 @@ import styled, { useTheme } from "styled-components"; interface SliderMenuProps { title: string; + hasNav?: boolean; isOpen: boolean; onClose?: () => void; children?: React.ReactNode; @@ -14,6 +15,7 @@ interface SliderMenuProps { export default function SliderMenu({ title, + hasNav, isOpen, onClose, children @@ -37,6 +39,7 @@ export default function SliderMenu({ ` +const Wrapper = styled(motion.div)<{ + displayTheme: DisplayTheme; + hasNav: boolean; +}>` position: fixed; bottom: 0; left: 0; height: auto; - max-height: 93%; + max-height: calc(100% - 66px); border-top: ${(props) => props.displayTheme === "light" ? `1px solid ${props.theme.primary}` @@ -79,6 +85,8 @@ const Wrapper = styled(motion.div)<{ displayTheme: DisplayTheme }>` background-color: ${(props) => props.displayTheme === "light" ? "#ffffff" : "#191919"}; border-radius: 10px 10px 0 0; + padding-bottom: ${(props) => (props.hasNav ? "66px" : "0")}; + box-sizing: border-box; `; export const animationSlideFromBottom: Variants = { diff --git a/src/components/arlocal/Transaction.tsx b/src/components/arlocal/Transaction.tsx index aaeb1f6b8..e80d23a21 100644 --- a/src/components/arlocal/Transaction.tsx +++ b/src/components/arlocal/Transaction.tsx @@ -109,6 +109,7 @@ export default function Transaction({ arweave }: Props) { content: browser.i18n.getMessage("invalidPassword"), duration: 2400 }); + setSendingTx(false); return; } } diff --git a/src/components/dashboard/Wallets.tsx b/src/components/dashboard/Wallets.tsx index 53f6c2dc7..e0b6aec30 100644 --- a/src/components/dashboard/Wallets.tsx +++ b/src/components/dashboard/Wallets.tsx @@ -5,7 +5,6 @@ import { useStorage } from "@plasmohq/storage/hook"; import { type AnsUser, getAnsProfile } from "~lib/ans"; import { ExtensionStorage } from "~utils/storage"; import { useLocation, useRoute } from "wouter"; -import { PlusIcon } from "@iconicicons/react"; import type { StoredWallet } from "~wallets"; import { Reorder } from "framer-motion"; import WalletListItem from "./list/WalletListItem"; diff --git a/src/components/hardware/HardwareWalletTheme.tsx b/src/components/hardware/HardwareWalletTheme.tsx index d3521e825..d712610f7 100644 --- a/src/components/hardware/HardwareWalletTheme.tsx +++ b/src/components/hardware/HardwareWalletTheme.tsx @@ -1,38 +1,81 @@ -import { type DefaultTheme, ThemeProvider } from "styled-components"; -import { type PropsWithChildren, useMemo } from "react"; +import { ThemeProvider, type DefaultTheme } from "styled-components"; +import { useEffect, type PropsWithChildren } from "react"; import { useHardwareApi } from "~wallets/hooks"; +import { useTheme } from "~utils/theme"; +import { Provider } from "@arconnect/components"; +import { useTheme as useStyledComponentsTheme } from "styled-components"; +import { MotionGlobalConfig } from "framer-motion"; -export default function HardwareWalletTheme({ - children -}: PropsWithChildren<{}>) { - // hardware api type +const ARCONNECT_THEME_BACKGROUND_COLOR = "ARCONNECT_THEME_BACKGROUND_COLOR"; + +/** + * Modify the theme if the active wallet is a hardware wallet. We transform the + * default accent color to match the hardware wallet's accent. + */ +function hardwareThemeModifier(theme: DefaultTheme): DefaultTheme { + return { + ...theme, + theme: "154, 184, 255", + primary: "#9AB8FF", + primaryBtnHover: "#6F93E1" + }; +} + +function noThemeModifier(theme: DefaultTheme): DefaultTheme { + return theme; +} + +export function ArConnectThemeProvider({ children }: PropsWithChildren<{}>) { const hardwareApi = useHardwareApi(); + const theme = useTheme(); + const themeModifier = hardwareApi ? hardwareThemeModifier : noThemeModifier; + + useEffect(() => { + const reducedMotionPreference = window.matchMedia( + "(prefers-reduced-motion)" + ); - // hardware wallet accent color - const hardwareApiTheme = useMemo(() => { - if (!hardwareApi) return false; + if (reducedMotionPreference.matches) { + // This could also be always set to `true` at the top of the file and then set to `false` after the application + // loads if we still notice some flicker due to any animation/transition playing when the view first loads. + + MotionGlobalConfig.skipAnimations = true; + } + }, []); + + return ( + + + + + {children} + + + ); +} + +export function ThemeBackgroundObserver() { + const styledComponentsTheme = useStyledComponentsTheme(); + const backgroundColor = styledComponentsTheme.background; + + useEffect(() => { + let formattedBackgroundColor = ""; + + if (backgroundColor.length === 3 || backgroundColor.length === 6) { + formattedBackgroundColor = `#${backgroundColor}`; + } else if (/\d{1,3}, ?\d{1,3}, ?\d{1,3}/.test(backgroundColor)) { + formattedBackgroundColor = `rgb(${backgroundColor})`; + } else if (/\d{1,3}, ?\d{1,3}, ?\d{1,3}, ?.+/.test(backgroundColor)) { + formattedBackgroundColor = `rgba(${backgroundColor})`; + } - if (hardwareApi === "keystone") { - return "154, 184, 255"; + if (formattedBackgroundColor) { + localStorage.setItem( + ARCONNECT_THEME_BACKGROUND_COLOR, + formattedBackgroundColor + ); } + }, [backgroundColor]); - return false; - }, [hardwareApi]); - - // modify theme if the active wallet is a - // hardware wallet - // we transform the default accent color - // to match the hardware wallet's accent - function themeModifier(theme: DefaultTheme): DefaultTheme { - if (!hardwareApiTheme) return theme; - - return { - ...theme, - theme: hardwareApiTheme, - primary: "#9AB8FF", - primaryBtnHover: "#6F93E1" - }; - } - - return {children}; + return null; } diff --git a/src/components/popup/HeadV2.tsx b/src/components/popup/HeadV2.tsx index fef804ee0..e7b16aa31 100644 --- a/src/components/popup/HeadV2.tsx +++ b/src/components/popup/HeadV2.tsx @@ -28,7 +28,6 @@ interface HeadV2Props { // allow opening the wallet switcher showBack?: boolean; padding?: string; - allowOpen?: boolean; back?: (...args) => any; } @@ -37,8 +36,7 @@ export default function HeadV2({ showOptions = true, back, padding, - showBack = true, - allowOpen = true + showBack = true }: HeadV2Props) { // scroll position const [scrollDirection, setScrollDirection] = useState<"up" | "down">("up"); @@ -110,37 +108,40 @@ export default function HeadV2({ scrolled={scrolled} padding={padding} > - { - if (back) await back(); - else goBack(); - }} - > - - + {showBack ? ( + { + if (back) await back(); + else goBack(); + }} + > + + + ) : null} {title} - - { - if (!allowOpen) return; - setOpen(true); - }} - > - {!ans?.avatar && !svgieAvatar && } - - {hardwareApi === "keystone" && ( - - )} - - - + {showOptions ? ( + + { + setOpen(true); + }} + > + {!ans?.avatar && !svgieAvatar && } + + {hardwareApi === "keystone" && ( + + )} + + + + ) : null} {isOpen && setOpen(false)} />} diff --git a/src/components/popup/HistoryProvider.tsx b/src/components/popup/HistoryProvider.tsx index 01f8e3b73..69f72680f 100644 --- a/src/components/popup/HistoryProvider.tsx +++ b/src/components/popup/HistoryProvider.tsx @@ -1,5 +1,4 @@ -import { type PropsWithChildren, useEffect, useState } from "react"; -import { getDecryptionKey } from "~wallets/auth"; +import { type PropsWithChildren, useState } from "react"; import { useLocation } from "wouter"; import { type BackAction, @@ -27,18 +26,6 @@ export default function HistoryProvider({ children }: PropsWithChildren<{}>) { history.back(); }; - // redirect to unlock if decryiption - // key is not available - useEffect(() => { - (async () => { - const decryptionKey = await getDecryptionKey(); - - if (!decryptionKey) { - push("/unlock"); - } - })(); - }, []); - return ( {children} diff --git a/src/components/popup/Navigation.tsx b/src/components/popup/Navigation.tsx index 8c0495a11..c7e2193a2 100644 --- a/src/components/popup/Navigation.tsx +++ b/src/components/popup/Navigation.tsx @@ -88,7 +88,7 @@ const NavigationBarWrapper = styled.nav<{ displayTheme: DisplayTheme }>` height: 62px; background-color: ${(props) => props.displayTheme === "light" ? "#F5F5F5" : "#191919"}; - width: 378px; + width: 377px; display: flex; `; diff --git a/src/components/popup/Route.tsx b/src/components/popup/Route.tsx index c7a3c78ae..6aa301a39 100644 --- a/src/components/popup/Route.tsx +++ b/src/components/popup/Route.tsx @@ -14,32 +14,18 @@ const Route: typeof BaseRoute = ({ path, component, children }) => { ? children(params) : children; - return ( - - {matches && {routeContent}} - - ); + return matches ? {routeContent} : null; }; -export const Wrapper = styled(motion.div)<{ - responsive?: boolean; - expanded?: boolean; -}>` +const PageWrapper = styled(motion.main)` position: relative; - width: ${(props) => (props.responsive ? "100%" : "377.5px")}; - min-height: ${(props) => (props.expanded ? "100vh" : "600px")}; - max-height: max-content; - background-color: rgb(${(props) => props.theme.background}); -`; - -const PageWrapper = styled(Wrapper)` - position: absolute; top: 0; width: 100%; - transition: background-color 0.23s ease-in-out; + min-height: 100vh; + max-height: max-content; `; -const Page = ({ children }: PropsWithChildren) => { +export const Page = ({ children }: PropsWithChildren) => { const opacityAnimation: Variants = { initial: { opacity: 0 }, enter: { opacity: 1 }, @@ -48,6 +34,7 @@ const Page = ({ children }: PropsWithChildren) => { return ( { - if (!price) return
; + if (!price || props.ao) return
; const estimate = fractBalance.multipliedBy(price); diff --git a/src/components/popup/WalletHeader.tsx b/src/components/popup/WalletHeader.tsx index 84528852d..db3172fd7 100644 --- a/src/components/popup/WalletHeader.tsx +++ b/src/components/popup/WalletHeader.tsx @@ -94,9 +94,6 @@ export default function WalletHeader() { }); }; - // is the popup open in a tab - const [isExpanded, setExpanded] = useState(false); - // profile picture const ansProfile = useAnsProfile(activeAddress); @@ -156,11 +153,6 @@ export default function WalletHeader() { window.addEventListener("scroll", listener); - const queryParameters = new URLSearchParams(window.location.search); - const expanded = queryParameters.get("expanded"); - - if (expanded) setExpanded(true); - return () => window.removeEventListener("scroll", listener); }, []); @@ -242,18 +234,23 @@ export default function WalletHeader() { } } ]; - if (!isExpanded && items.length === 4) { + + if (location.pathname === "/popup.html") { + // This option won't be shown in the fullscreen mode (fullscreen.html) or the embedded wallet: + items.push({ icon: , title: "expand_view", - route: () => { - window.open(window.location.href.split("#")[0] + "?expanded=true"); + route: async () => { + await browser.tabs.create({ + url: browser.runtime.getURL("tabs/fullscreen.html") + }); } }); } return items; - }, [activeAddress, isExpanded]); + }, [activeAddress]); return ( 14}> diff --git a/src/components/popup/home/Balance.tsx b/src/components/popup/home/Balance.tsx index 4b0a62194..e81944278 100644 --- a/src/components/popup/home/Balance.tsx +++ b/src/components/popup/home/Balance.tsx @@ -111,18 +111,9 @@ export default function Balance() { })(); }, [activeAddress]); - // router push - const [push] = useHistory(); - // display theme const theme = useTheme(); - // lock wallet and terminate session - async function lockWallet() { - await removeDecryptionKey(); - push("/unlock"); - } - useEffect(() => { if ( balance.toNumber() !== historicalBalance[historicalBalance.length - 1] @@ -166,7 +157,10 @@ export default function Balance() { content={browser.i18n.getMessage("lock_wallet")} position="top" > - + diff --git a/src/components/popup/home/NoBalance.tsx b/src/components/popup/home/NoBalance.tsx index 827bf2250..051d98e0d 100644 --- a/src/components/popup/home/NoBalance.tsx +++ b/src/components/popup/home/NoBalance.tsx @@ -18,12 +18,12 @@ export default function NoBalance() { push("/receive")} + onClick={() => push("/tokens")} secondary fullWidth className="normal-font-weight" > - {browser.i18n.getMessage("receive_AR_button")} + {browser.i18n.getMessage("view_all_assets")} diff --git a/src/components/popup/home/Transactions.tsx b/src/components/popup/home/Transactions.tsx index 79ad7274a..6d55b533e 100644 --- a/src/components/popup/home/Transactions.tsx +++ b/src/components/popup/home/Transactions.tsx @@ -10,7 +10,8 @@ import { AO_RECEIVER_QUERY, AO_SENT_QUERY, AR_RECEIVER_QUERY, - AR_SENT_QUERY + AR_SENT_QUERY, + PRINT_ARWEAVE_QUERY } from "~notifications/utils"; import { useHistory } from "~utils/hash_router"; import { getArPrice } from "~lib/coingecko"; @@ -53,15 +54,21 @@ export default function Transactions() { AR_RECEIVER_QUERY, AR_SENT_QUERY, AO_SENT_QUERY, - AO_RECEIVER_QUERY + AO_RECEIVER_QUERY, + PRINT_ARWEAVE_QUERY ]; - const [rawReceived, rawSent, rawAoSent, rawAoReceived] = - await Promise.allSettled( - queries.map((query) => - gql(query, { address: activeAddress }, suggestedGateways[1]) - ) - ); + const [ + rawReceived, + rawSent, + rawAoSent, + rawAoReceived, + rawPrintArchive + ] = await Promise.allSettled( + queries.map((query) => + gql(query, { address: activeAddress }, suggestedGateways[1]) + ) + ); let sent = await processTransactions(rawSent, "sent"); sent = sent.filter((tx) => BigNumber(tx.node.quantity.ar).gt(0)); @@ -75,12 +82,17 @@ export default function Transactions() { "aoReceived", true ); + const printArchive = await processTransactions( + rawPrintArchive, + "printArchive" + ); let combinedTransactions: ExtendedTransaction[] = [ ...sent, ...received, ...aoReceived, - ...aoSent + ...aoSent, + ...printArchive ]; combinedTransactions.sort(sortFn); @@ -154,12 +166,14 @@ export default function Transactions() { : "Pending"} -
-
{getFormattedAmount(transaction)}
- - {getFormattedFiatAmount(transaction, arPrice, currency)} - -
+ {transaction.transactionType !== "printArchive" && ( +
+
{getFormattedAmount(transaction)}
+ + {getFormattedFiatAmount(transaction, arPrice, currency)} + +
+ )} )) diff --git a/src/lib/printer.ts b/src/lib/printer.ts new file mode 100644 index 000000000..330fb0401 --- /dev/null +++ b/src/lib/printer.ts @@ -0,0 +1,227 @@ +import { uploadDataToTurbo } from "~api/modules/dispatch/uploader"; +import { getActiveKeyfile, type DecryptedWallet } from "~wallets"; +import { freeDecryptedWallet } from "~wallets/encryption"; +import { createData, ArweaveSigner } from "arbundles"; +import { concatGatewayURL } from "~gateways/utils"; +import { findGateway } from "~gateways/wayfinder"; +import browser from "webextension-polyfill"; +import Arweave from "arweave"; +import { signAuth } from "~api/modules/sign/sign_auth"; +import { getActiveTab } from "~applications"; +import { sleep } from "~utils/sleep"; + +const ARCONNECT_PRINTER_ID = "arconnect-permaweb-printer"; + +/** + * Tells Chrome about the virtual printer's + * capabilities in CDD format + */ +export function getCapabilities( + printerId: string, + callback: PrinterCapabilitiesCallback +) { + // only return capabilities for the ArConnect printer + if (printerId !== ARCONNECT_PRINTER_ID) return; + + // mimic a regular printer's capabilities + callback({ + version: "1.0", + printer: { + supported_content_type: [ + { content_type: "application/pdf" }, + { content_type: "image/pwg-raster" } + ], + color: { + option: [ + { type: "STANDARD_COLOR", is_default: true }, + { type: "STANDARD_MONOCHROME" } + ] + }, + copies: { + default_copies: 1, + max_copies: 100 + }, + media_size: { + option: [ + { + name: "ISO_A4", + width_microns: 210000, + height_microns: 297000, + is_default: true + }, + { + name: "NA_LETTER", + width_microns: 215900, + height_microns: 279400 + } + ] + }, + page_orientation: { + option: [ + { + type: "PORTRAIT", + is_default: true + }, + { type: "LANDSCAPE" }, + { type: "AUTO" } + ] + }, + duplex: { + option: [ + { type: "NO_DUPLEX", is_default: true }, + { type: "LONG_EDGE" }, + { type: "SHORT_EDGE" } + ] + } + } + }); +} + +/** + * Printer capabilities request callback type + */ +type PrinterCapabilitiesCallback = (p: unknown) => void; + +/** + * Returns a list of "virtual" printers, + * in our case "Print/Publish to Arweave" + */ +export function getPrinters(callback: PrinterInfoCallback) { + callback([ + { + id: ARCONNECT_PRINTER_ID, + name: "Print to Arweave", + description: + "Publish the content you want to print on Arweave, permanently." + } + ]); +} + +/** + * Printer info request callback type + */ +type PrinterInfoCallback = (p: chrome.printerProvider.PrinterInfo[]) => void; + +/** + * Handles the request from the user to print the page to Arweave + */ +export async function handlePrintRequest( + printJob: chrome.printerProvider.PrintJob, + resultCallback: PrintCallback +) { + // only print for the ArConnect printer + if (printJob.printerId !== ARCONNECT_PRINTER_ID) return; + + // wallet + let decryptedWallet: DecryptedWallet; + + try { + // build data blog + const data = new Blob([printJob.document], { type: printJob.contentType }); + + // get user wallet + decryptedWallet = await getActiveKeyfile(); + + if (decryptedWallet.type === "hardware") + throw new Error("Cannot print with a hardware wallet."); + + // extension manifest + const manifest = browser.runtime.getManifest(); + + // setup tags + const tags = [ + { name: "App-Name", value: manifest.name }, + { name: "App-Version", value: manifest.version }, + { name: "Type", value: "Print-Archive" }, + { name: "Content-Type", value: printJob.contentType }, + { name: "print:title", value: printJob.title }, + { name: "print:timestamp", value: new Date().getTime().toString() } + ]; + + let transactionId: string; + + // find a gateway to upload and display the result + const gateway = await findGateway({}); + const arweave = Arweave.init(gateway); + + // create data item + const dataSigner = new ArweaveSigner(decryptedWallet.keyfile); + const transactionData = new Uint8Array(await data.arrayBuffer()); + const dataEntry = createData(transactionData, dataSigner, { tags }); + + // calculate reward for the transaction + const reward = await arweave.transactions.getPrice( + transactionData.byteLength + ); + + // get active tab + const activeTab = await getActiveTab(); + + await signAuth( + activeTab.url, + // @ts-expect-error + { + ...dataEntry.toJSON(), + reward, + sizeInBytes: transactionData.byteLength + }, + decryptedWallet.address + ); + + try { + // sign an upload data + await dataEntry.sign(dataSigner); + await uploadDataToTurbo(dataEntry, "https://turbo.ardrive.io"); + + await sleep(2000); + + // this has to be one of FAILED, INVALID_DATA, INVALID_TICKET, OK + resultCallback("OK"); + + transactionId = dataEntry.id; + } catch (error) { + // sign & post if there is something wrong with turbo + + const transaction = await arweave.createTransaction( + { data: transactionData }, + decryptedWallet.keyfile + ); + + for (const tag of tags) { + transaction.addTag(tag.name, tag.value); + } + + // sign and upload + await arweave.transactions.sign(transaction, decryptedWallet.keyfile); + const uploader = await arweave.transactions.getUploader(transaction); + + while (!uploader.isComplete) { + await uploader.uploadChunk(); + } + + await sleep(2000); + + // this has to be one of FAILED, INVALID_DATA, INVALID_TICKET, OK + resultCallback("OK"); + + transactionId = transaction.id; + } + + // open in new tab + await chrome.tabs.create({ + url: `${concatGatewayURL(gateway)}/${transactionId}` + }); + } catch (e) { + console.log("Printing failed:\n", e); + resultCallback("FAILED"); + } + + // free wallet from memory + if (decryptedWallet?.type == "local") + freeDecryptedWallet(decryptedWallet.keyfile); +} + +/** + * Print request (result) callback + */ +type PrintCallback = (result: string) => void; diff --git a/src/lib/redstone.ts b/src/lib/redstone.ts index 1e9421fed..7bc67cd06 100644 --- a/src/lib/redstone.ts +++ b/src/lib/redstone.ts @@ -9,9 +9,14 @@ import BigNumber from "bignumber.js"; * Hook for the redstone token price API * * @param symbol Token symbol + * @param isAoToken Token is ao token or not * @param opts Custom Redstone API "getPrice" options */ -export function usePrice(symbol?: string, opts?: GetPriceOptions) { +export function usePrice( + symbol?: string, + isAoToken?: boolean, + opts?: GetPriceOptions +) { const [price, setPrice] = useState(); const [loading, setLoading] = useState(false); @@ -20,7 +25,7 @@ export function usePrice(symbol?: string, opts?: GetPriceOptions) { useEffect(() => { (async () => { - if (!symbol) { + if (!symbol || isAoToken) { return; } diff --git a/src/lib/transactions.ts b/src/lib/transactions.ts index 47d30e85e..698a9ad62 100644 --- a/src/lib/transactions.ts +++ b/src/lib/transactions.ts @@ -178,6 +178,8 @@ export const getTransactionDescription = (transaction: ExtendedTransaction) => { return `${browser.i18n.getMessage("received")} ${ transaction.aoInfo.tickerName }`; + case "printArchive": + return browser.i18n.getMessage("print_archived"); default: return ""; } diff --git a/src/notifications/utils.ts b/src/notifications/utils.ts index acf7033a6..0a09f4b4e 100644 --- a/src/notifications/utils.ts +++ b/src/notifications/utils.ts @@ -243,6 +243,59 @@ query($address: String!, $after: String) { } `; +// PRINT ARWEAVE TRANSACTIONS +export const PRINT_ARWEAVE_QUERY = ` +query ($address: String!) { + transactions( + first: 10, + owners: [$address], + tags: [{name: "Type", values: ["Print-Archive"]}] + ) { + edges { + cursor + node { + id + recipient + owner { address } + quantity { ar } + block { timestamp, height } + tags { + name + value + } + } + } + } +}`; + +export const PRINT_ARWEAVE_QUERY_WITH_CURSOR = ` +query ($address: String!, $after: String) { + transactions( + first: 10, + owners: [$address], + tags: [{name: "Type", values: ["Print-Archive"]}], + after: $after + ) { + pageInfo { + hasNextPage + } + edges { + cursor + node { + id + recipient + owner { address } + quantity { ar } + block { timestamp, height } + tags { + name + value + } + } + } + } +}`; + export const combineAndSortTransactions = (responses: any[]) => { const combinedTransactions = responses.reduce((acc, response) => { const transactions = response.data.transactions.edges; @@ -327,6 +380,13 @@ export const processTransactions = ( warpContract = true; transactionType = transaction.node.owner.address === address ? "Sent" : "Received"; + } else { + const printArchiveTag = transaction.node.tags.find( + (tag) => tag.name === "Type" && tag.value === "Print-Archive" + ); + if (printArchiveTag) { + transactionType = "PrintArchive"; + } } } } diff --git a/src/popup.html b/src/popup.html new file mode 100644 index 000000000..6ee23af8b --- /dev/null +++ b/src/popup.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/popup.tsx b/src/popup.tsx index 1c20dbe5e..f9b8c6c06 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -1,13 +1,10 @@ -import Route, { Wrapper } from "~components/popup/Route"; -import styled, { createGlobalStyle } from "styled-components"; -import { GlobalStyle, useTheme } from "~utils/theme"; +import Route, { Page, Wrapper } from "~components/popup/Route"; +import styled from "styled-components"; import { useHashLocation } from "~utils/hash_router"; -import { Provider } from "@arconnect/components"; import { syncLabels, useSetUp } from "~wallets"; import { useEffect, useState } from "react"; import { Router } from "wouter"; -import HardwareWalletTheme from "~components/hardware/HardwareWalletTheme"; import HistoryProvider from "~components/popup/HistoryProvider"; import Home from "~routes/popup"; @@ -49,187 +46,158 @@ 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"; +import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme"; +import { AnimatePresence } from "framer-motion"; export default function Popup() { - const theme = useTheme(); - const [expanded, setExpanded] = useState(false); - - // init popup - useSetUp(); + const initialScreenType = useSetUp(); useEffect(() => { - // sync ans labels syncLabels(); - - // check expanded view - if (new URLSearchParams(window.location.search).get("expanded")) { - setExpanded(true); - } }, []); - return ( - - - - - - - - - - - - {(params: { quoteId: string }) => ( - - )} - - - {() => } - - {(params: { id?: string }) => } - - - {(params: { tokenID: string }) => ( - - )} - - - - - - - - {(params: { address: string }) => ( - - )} - - - {(params: { address: string }) => ( - - )} - - - {(params: { address: string }) => ( - - )} - - - - {(params: { url: string }) => ( - - )} - - - {(params: { url: string }) => ( - - )} - - - - {(params: { id: string }) => ( - - )} - - - - - {(params: { address: string }) => ( - - )} - - - - - {(params: { id: string }) => ( - - )} - - - {(params: { id: string }) => ( - - )} - - - {(params: { id: string }) => ( - - )} - - - - - {(params: { id: string }) => ( - - )} - - - - {(params: { id: string }) => } - - - - {(params: { id: string }) => } - - - {(params: { id: string; gateway?: string }) => ( - - )} - - - {(params: { token: string; qty: string }) => ( - - )} - - - {(params: { - token: string; - qty: string; - message?: string; - }) => ( - - )} - - - - - - - - - ); -} - -const HideScrollbar = createGlobalStyle<{ expanded?: boolean }>` - * { - scrollbar-width: none; + let content: React.ReactElement = null; - &::-webkit-scrollbar { - display: none - } + if (initialScreenType === "cover") { + content = ; + } else if (initialScreenType === "locked") { + content = ( + + + + ); + } else if (initialScreenType === "generating") { + // This can only happen in the embedded wallet: + content = ( + +

Generating Wallet...

+
+ ); + } else { + content = ( + + + + + + {(params: { quoteId: string }) => ( + + )} + + + {() => } + + {(params: { id?: string }) => } + + + {(params: { tokenID: string }) => ( + + )} + + + + + + + + {(params: { address: string }) => ( + + )} + + + {(params: { address: string }) => ( + + )} + + + {(params: { address: string }) => ( + + )} + + + + {(params: { url: string }) => } + + + {(params: { url: string }) => } + + + + {(params: { id: string }) => } + + + + + {(params: { address: string }) => ( + + )} + + + + + {(params: { id: string }) => ( + + )} + + + {(params: { id: string }) => ( + + )} + + + {(params: { id: string }) => ( + + )} + + + + + {(params: { id: string }) => ( + + )} + + + + {(params: { id: string }) => } + + + + {(params: { id: string }) => } + + + {(params: { id: string; gateway?: string }) => ( + + )} + + + {(params: { token: string; qty: string }) => ( + + )} + + + {(params: { token: string; qty: string; message?: string }) => ( + + )} + + + + + ); } - body { - ${(props) => - props?.expanded - ? `background-image: linear-gradient( to right, transparent, rgba( ${props.theme.theme},.4 ), transparent);` - : ""} - } -`; - -const ExpandedViewWrapper = styled.div` - width: 100%; - display: flex; - justify-content: center; -`; + return ( + + {content} + + ); +} diff --git a/src/routes/auth/sign.tsx b/src/routes/auth/sign.tsx index 118ce3406..26c1388a4 100644 --- a/src/routes/auth/sign.tsx +++ b/src/routes/auth/sign.tsx @@ -140,7 +140,7 @@ export default function Sign() { const size = useMemo(() => { if (!transaction) return 0; - return transaction.data.length; + return transaction?.sizeInBytes ?? transaction.data.length; }, [transaction]); // authorize @@ -165,6 +165,14 @@ export default function Sign() { })); }, [transaction]); + // Check if it's a printTx + const isPrintTx = useMemo(() => { + return ( + tags.some((tag) => tag.name === "print:title") && + tags.some((tag) => tag.name === "print:timestamp") + ); + }, [tags]); + const recipient = useMemo(() => { if (tags.length === 0) return transaction?.target || ""; @@ -278,11 +286,32 @@ export default function Sign() { {(!page && (
- {formatFiatBalance(fiatPrice, currency)} + {isPrintTx ? ( +
+ + {browser.i18n.getMessage("transaction_fee")} + +
+ ) : ( + {formatFiatBalance(fiatPrice, currency)} + )} - {formatTokenBalance(quantity)} + {isPrintTx + ? size > 100000 + ? formatTokenBalance(fee, 5) + : formatTokenBalance(0) + : formatTokenBalance(quantity)} AR + {isPrintTx && ( + {formatFiatBalance(fee, currency)} + )} @@ -300,15 +329,17 @@ export default function Sign() { {formatAddress(recipient, 6)} )} - - - {browser.i18n.getMessage("transaction_fee")} - - - {fee} - {" AR"} - - + {!isPrintTx && ( + + + {browser.i18n.getMessage("transaction_fee")} + + + {fee} + {" AR"} + + + )} {browser.i18n.getMessage("transaction_size")} diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx index 856b024cc..cf9173537 100644 --- a/src/routes/popup/index.tsx +++ b/src/routes/popup/index.tsx @@ -22,6 +22,7 @@ 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"; +import BigNumber from "bignumber.js"; export default function Home() { // get if the user has no balance @@ -38,13 +39,21 @@ export default function Home() { instance: ExtensionStorage }); + const [historicalBalance] = useStorage( + { + key: "historical_balance", + instance: ExtensionStorage + }, + [] + ); + const balance = useBalance(); // all tokens const tokens = useTokens(); // ao Tokens - const [aoTokens] = useAoTokens(); + const [aoTokens, aoTokensLoading] = useAoTokens(); // checking to see if it's a hardware wallet const wallet = useActiveWallet(); @@ -59,23 +68,20 @@ export default function Home() { if (!activeAddress) return; const findBalances = async (assets, aoTokens) => { - const t = [...assets, ...aoTokens]; - const tokens = t.find((token) => token.balance !== 0); - if (tokens) { - setNoBalance(false); - return; - } else if (balance.toNumber()) { + const hasTokensWithBalance = + aoTokensLoading || + [...assets, ...aoTokens].some((token) => + BigNumber(token.balance || "0").gt(0) + ); + + if ( + hasTokensWithBalance || + balance.toNumber() || + historicalBalance[historicalBalance.length - 1] !== 0 + ) { setNoBalance(false); - return; } else { - const history = await ExtensionStorage.get("historical_balance"); - // @ts-ignore - if (history[0] !== 0) { - setNoBalance(false); - return; - } else { - setNoBalance(true); - } + setNoBalance(true); } }; @@ -84,7 +90,14 @@ export default function Home() { } catch (error) { console.log(error); } - }, [activeAddress, assets, aoTokens]); + }, [ + activeAddress, + assets, + aoTokens, + balance, + historicalBalance, + aoTokensLoading + ]); useEffect(() => { const trackEventAndPage = async () => { diff --git a/src/routes/popup/notifications.tsx b/src/routes/popup/notifications.tsx index 1ea43c32a..58db1b735 100644 --- a/src/routes/popup/notifications.tsx +++ b/src/routes/popup/notifications.tsx @@ -58,8 +58,9 @@ export default function Notifications() { n.arBalanceNotifications.arNotifications, n.aoNotifications.aoNotifications ); - setNotifications(sortedNotifications); - const formattedTxMsgs = await formatTxMessage(sortedNotifications); + const { formattedTxMsgs, formattedNotifications } = + await formatTxMessage(sortedNotifications); + setNotifications(formattedNotifications); setFormattedTxMsgs(formattedTxMsgs); setLoading(false); } catch (error) { @@ -80,12 +81,18 @@ export default function Notifications() { const formatTxMessage = async ( notifications: Transaction[] - ): Promise => { - const formattedTxMsgs = await Promise.all( + ): Promise<{ + formattedTxMsgs: string[]; + formattedNotifications: Transaction[]; + }> => { + const address = await getActiveAddress(); + let formattedNotifications = await Promise.all( notifications.map(async (notification) => { try { let formattedMessage: string = ""; - if (notification.transactionType !== "Message") { + if (notification.transactionType === "PrintArchive") { + formattedMessage = browser.i18n.getMessage("print_archived"); + } else if (notification.transactionType !== "Message") { let ticker: string; let quantityTransfered; if (notification.isAo) { @@ -132,31 +139,71 @@ export default function Notifications() { } } if (notification.transactionType === "Sent") { - formattedMessage = `Sent ${quantityTransfered} ${ticker} to ${ + formattedMessage = browser.i18n.getMessage("sent_balance", [ + quantityTransfered, + ticker, notification.isAo ? findRecipient(notification) : formatAddress(notification.node.recipient, 4) - }`; + ]); } else if (notification.transactionType === "Received") { - formattedMessage = `Received ${quantityTransfered} ${ticker} from ${formatAddress( - notification.node.owner.address, - 4 - )}`; + formattedMessage = browser.i18n.getMessage("received_balance", [ + quantityTransfered, + ticker, + formatAddress(notification.node.owner.address, 4) + ]); + } else { + const recipient = notification.node.recipient; + const sender = notification.node.owner.address; + const isSent = sender === address; + const contentTypeTag = notification.node.tags.find( + (tag) => tag.name === "Content-Type" + ); + if (!recipient && contentTypeTag) { + formattedMessage = browser.i18n.getMessage("new_data_uploaded"); + } else if (!recipient) { + formattedMessage = `${browser.i18n.getMessage( + "new_transaction" + )} ${browser.i18n.getMessage("sent").toLowerCase()}`; + } else { + formattedMessage = `${browser.i18n.getMessage( + "new_transaction" + )} ${browser.i18n.getMessage( + isSent ? "notification_to" : "notification_from" + )} ${formatAddress(isSent ? recipient : sender, 4)}`; + } } } else { - formattedMessage = `New message from ${formatAddress( - notification.node.owner.address, - 4 - )}`; + const recipient = notification.node.recipient; + const sender = notification.node.owner.address; + const isSent = sender === address; + formattedMessage = `${browser.i18n.getMessage( + "new_message" + )} ${browser.i18n.getMessage( + isSent ? "notification_to" : "notification_from" + )} ${formatAddress(isSent ? recipient : sender, 4)}`; } - return formattedMessage; + return { formattedMessage, notification }; } catch { - return null; + return { formattedMessage: null, notification }; } }) ); - return formattedTxMsgs.filter((msg) => msg); + formattedNotifications = formattedNotifications.filter( + (notification) => notification.formattedMessage + ); + + const formattedTxMsgs = formattedNotifications.map( + (notification) => notification.formattedMessage + ); + + return { + formattedTxMsgs, + formattedNotifications: formattedNotifications.map( + ({ notification }) => notification + ) + }; }; const formatDate = (timestamp) => { diff --git a/src/routes/popup/send/index.tsx b/src/routes/popup/send/index.tsx index 77401098f..c62ccf5af 100644 --- a/src/routes/popup/send/index.tsx +++ b/src/routes/popup/send/index.tsx @@ -599,6 +599,7 @@ export default function Send({ id }: Props) { { setShownTokenSelector(false); @@ -651,6 +652,7 @@ export default function Send({ id }: Props) { { setShowSlider(false); diff --git a/src/routes/popup/transaction/[id].tsx b/src/routes/popup/transaction/[id].tsx index 2475295e4..9a1e8c258 100644 --- a/src/routes/popup/transaction/[id].tsx +++ b/src/routes/popup/transaction/[id].tsx @@ -42,11 +42,13 @@ import { generateProfileIcon, ProfilePicture } from "~components/Recipient"; -import { TempTransactionStorage } from "~utils/storage"; +import { ExtensionStorage, TempTransactionStorage } from "~utils/storage"; import { useContact } from "~contacts/hooks"; import { EventType, PageType, trackEvent, trackPage } from "~utils/analytics"; import BigNumber from "bignumber.js"; import { fetchTokenByProcessId } from "~lib/transactions"; +import { useStorage } from "@plasmohq/storage/hook"; +import type { StoredWallet } from "~wallets"; // pull contacts and check if to address is in contacts @@ -71,8 +73,22 @@ export default function Transaction({ id: rawId, gw, message }: Props) { const childRef = useRef(null); useAdjustAmountTitleWidth(parentRef, childRef, quantity); + const [wallets] = useStorage( + { + key: "wallets", + instance: ExtensionStorage + }, + [] + ); + + const fromAddress = transaction?.owner.address; + const toAddress = transaction?.recipient; + const fromMe = wallets.find((wallet) => wallet.address === fromAddress); + const toMe = wallets.find((wallet) => wallet.address === toAddress); + // const [contact, setContact] = useState(undefined); - const contact = useContact(transaction?.recipient); + const fromContact = useContact(fromAddress); + const toContact = useContact(toAddress); const [ao, setAo] = useState({ isAo: false }); @@ -234,6 +250,12 @@ export default function Transaction({ id: rawId, gw, message }: Props) { return !type.startsWith("text/") && !type.startsWith("application/"); }, [transaction]); + const isPrintTx = useMemo(() => { + return transaction?.tags?.some( + (tag) => tag.name === "Type" && tag.value === "Print-Archive" + ); + }, [transaction]); + const isImage = useMemo(() => { const type = getContentType(); @@ -242,7 +264,7 @@ export default function Transaction({ id: rawId, gw, message }: Props) { useEffect(() => { (async () => { - if (!transaction || !id || !arweave || isBinary) { + if (!transaction || !id || !arweave || isBinary || isPrintTx) { return; } @@ -265,7 +287,7 @@ export default function Transaction({ id: rawId, gw, message }: Props) { setData(txData); })(); - }, [id, transaction, gateway, isBinary]); + }, [id, transaction, gateway, isBinary, isPrintTx]); // get custom back params const [backPath, setBackPath] = useState(); @@ -356,7 +378,50 @@ export default function Transaction({ id: rawId, gw, message }: Props) { {browser.i18n.getMessage("transaction_from")} - {formatAddress(transaction.owner.address, 6)} +
+ {!fromContact ? ( + <> + {formatAddress(fromMe || fromAddress, 6)} + + {fromMe ? null : ( + + {browser.i18n.getMessage("user_not_in_contacts")}{" "} + { + trackEvent(EventType.ADD_CONTACT, { + fromSendFlow: true + }); + browser.tabs.create({ + url: browser.runtime.getURL( + `tabs/dashboard.html#/contacts/new?address=${fromAddress}` + ) + }); + }} + > + {browser.i18n.getMessage("create_contact")} + + + )} + + ) : ( +
+ {fromContact.profileIcon ? ( + + ) : ( + + {generateProfileIcon( + fromContact?.name || fromContact.address + )} + + )} + {fromContact?.name || + formatAddress(fromContact.address, 6)} +
+ )} +
@@ -365,44 +430,46 @@ export default function Transaction({ id: rawId, gw, message }: Props) {
- {!contact ? ( + {!toContact ? ( <> - {(transaction.recipient && - formatAddress(transaction.recipient, 6)) || - "-"} - - {browser.i18n.getMessage("user_not_in_contacts")}{" "} - { - trackEvent(EventType.ADD_CONTACT, { - fromSendFlow: true - }); - browser.tabs.create({ - url: browser.runtime.getURL( - `tabs/dashboard.html#/contacts/new?address=${transaction.recipient}` - ) - }); - }} - > - {browser.i18n.getMessage("create_contact")} - - + {formatAddress(toMe || toAddress, 6)} + + {toMe ? null : ( + + {browser.i18n.getMessage("user_not_in_contacts")}{" "} + { + trackEvent(EventType.ADD_CONTACT, { + fromSendFlow: true + }); + browser.tabs.create({ + url: browser.runtime.getURL( + `tabs/dashboard.html#/contacts/new?address=${toAddress}` + ) + }); + }} + > + {browser.i18n.getMessage("create_contact")} + + + )} ) : (
- {contact.profileIcon ? ( + {toContact.profileIcon ? ( ) : ( {generateProfileIcon( - contact?.name || contact.address + toContact?.name || toContact.address )} )} - {contact?.name || formatAddress(contact.address, 6)} + {toContact?.name || + formatAddress(toContact.address, 6)}
)}
@@ -479,7 +546,7 @@ export default function Transaction({ id: rawId, gw, message }: Props) { {JSON.stringify(input, undefined, 2)} )} - {(data || isBinary) && ( + {(data || isBinary || isPrintTx) && ( <> - {!message - ? browser.i18n.getMessage("transaction_data") - : browser.i18n.getMessage("signature_message")} + {!message + ? browser.i18n.getMessage("transaction_data") + : browser.i18n.getMessage("signature_message")} - {(!isImage && ( - - {(isBinary && - browser.i18n.getMessage( - "transaction_data_binary_warning" - )) || - data} - - )) || ( - - )} + {!isPrintTx && + ((!isImage && ( + + {(isBinary && + browser.i18n.getMessage( + "transaction_data_binary_warning" + )) || + data} + + )) || ( + + ))} )}
diff --git a/src/routes/popup/transaction/transactions.tsx b/src/routes/popup/transaction/transactions.tsx index e54a3c557..fb3526c2c 100644 --- a/src/routes/popup/transaction/transactions.tsx +++ b/src/routes/popup/transaction/transactions.tsx @@ -11,7 +11,8 @@ import { AO_RECEIVER_QUERY_WITH_CURSOR, AO_SENT_QUERY_WITH_CURSOR, AR_RECEIVER_QUERY_WITH_CURSOR, - AR_SENT_QUERY_WITH_CURSOR + AR_SENT_QUERY_WITH_CURSOR, + PRINT_ARWEAVE_QUERY_WITH_CURSOR } from "~notifications/utils"; import { useHistory } from "~utils/hash_router"; import { getArPrice } from "~lib/coingecko"; @@ -31,8 +32,8 @@ import { } from "~lib/transactions"; import BigNumber from "bignumber.js"; -const defaultCursors = ["", "", "", ""]; -const defaultHasNextPages = [true, true, true, true]; +const defaultCursors = ["", "", "", "", ""]; +const defaultHasNextPages = [true, true, true, true, true]; export default function Transactions() { const [cursors, setCursors] = useState(defaultCursors); @@ -63,10 +64,11 @@ export default function Transactions() { AR_RECEIVER_QUERY_WITH_CURSOR, AR_SENT_QUERY_WITH_CURSOR, AO_SENT_QUERY_WITH_CURSOR, - AO_RECEIVER_QUERY_WITH_CURSOR + AO_RECEIVER_QUERY_WITH_CURSOR, + PRINT_ARWEAVE_QUERY_WITH_CURSOR ]; - const [rawReceived, rawSent, rawAoSent, rawAoReceived] = + const [rawReceived, rawSent, rawAoSent, rawAoReceived, rawPrintArchive] = await Promise.allSettled( queries.map((query, idx) => { return hasNextPages[idx] @@ -94,9 +96,13 @@ export default function Transactions() { "aoReceived", true ); + const printArchive = await processTransactions( + rawPrintArchive, + "printArchive" + ); setCursors((prev) => - [received, sent, aoSent, aoReceived].map( + [received, sent, aoSent, aoReceived, printArchive].map( (data, idx) => data[data.length - 1]?.cursor ?? prev[idx] ) ); @@ -105,7 +111,7 @@ export default function Transactions() { received = received.filter((tx) => BigNumber(tx.node.quantity.ar).gt(0)); setHasNextPages( - [rawReceived, rawSent, rawAoSent, rawAoReceived].map( + [rawReceived, rawSent, rawAoSent, rawAoReceived, rawPrintArchive].map( (result) => (result.status === "fulfilled" && result.value?.data?.transactions?.pageInfo?.hasNextPage) ?? @@ -117,7 +123,8 @@ export default function Transactions() { ...sent, ...received, ...aoReceived, - ...aoSent + ...aoSent, + ...printArchive ]; combinedTransactions = combinedTransactions.map((transaction) => { @@ -221,16 +228,18 @@ export default function Transactions() { : "Pending"}
-
-
{getFormattedAmount(transaction)}
- - {getFormattedFiatAmount( - transaction, - arPrice, - currency - )} - -
+ {transaction.transactionType !== "printArchive" && ( +
+
{getFormattedAmount(transaction)}
+ + {getFormattedFiatAmount( + transaction, + arPrice, + currency + )} + +
+ )} ))} diff --git a/src/routes/popup/unlock.tsx b/src/routes/popup/unlock.tsx index 8edee3c12..9accf31ce 100644 --- a/src/routes/popup/unlock.tsx +++ b/src/routes/popup/unlock.tsx @@ -12,7 +12,6 @@ import { useToasts } from "@arconnect/components"; import HeadV2 from "~components/popup/HeadV2"; -import styled from "styled-components"; export default function Unlock() { // password input diff --git a/src/tabs/arlocal.html b/src/tabs/arlocal.html new file mode 100644 index 000000000..7b1215e63 --- /dev/null +++ b/src/tabs/arlocal.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/tabs/arlocal.tsx b/src/tabs/arlocal.tsx index bc1ce21a6..f10728c2f 100644 --- a/src/tabs/arlocal.tsx +++ b/src/tabs/arlocal.tsx @@ -1,12 +1,12 @@ import { InputWithBtn, InputWrapper } from "~components/arlocal/InputWrapper"; import { RefreshButton } from "~components/IconButton"; import { useEffect, useMemo, useState } from "react"; -import { GlobalStyle, useTheme } from "~utils/theme"; +import { useTheme } from "~utils/theme"; import { urlToGateway } from "~gateways/utils"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; import { RefreshIcon } from "@iconicicons/react"; -import { useNoWallets } from "~wallets"; +import { useNoWallets, useRemoveCover } from "~wallets"; import { ButtonV2 as Button, InputV2 as Input, @@ -30,8 +30,11 @@ import Mint from "~components/arlocal/Mint"; import browser from "webextension-polyfill"; import Arweave from "arweave"; import axios from "axios"; +import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme"; + +export default function ArLocal() { + useRemoveCover(); -function ArLocal() { // testnet data const testnetInput = useInput(); const [lastUsedTestnet, setLastUsedTestnet] = useStorage( @@ -156,61 +159,52 @@ function ArLocal() { const noWallets = useNoWallets(); return ( - - {noWallets && } - - - ArLocal {browser.i18n.getMessage("devtools")} - <Spacer x={0.2} /> - <Text noMargin>by ArConnect</Text> - - - {browser.i18n.getMessage(online ? "testnetLive" : "testnetDown")} - - - - - - - - loadTestnet()} - refreshing={loadingTestnet} - > - - - - - {(!online && ) || ( - <> - - - - - - - )} - - - ); -} - -export default function () { - const theme = useTheme(); - - return ( - - - - + + + {noWallets && } + + + ArLocal {browser.i18n.getMessage("devtools")} + <Spacer x={0.2} /> + <Text noMargin>by ArConnect</Text> + + + {browser.i18n.getMessage(online ? "testnetLive" : "testnetDown")} + + + + + + + + loadTestnet()} + refreshing={loadingTestnet} + > + + + + + {(!online && ) || ( + <> + + + + + + + )} + + + ); } diff --git a/src/tabs/auth.html b/src/tabs/auth.html new file mode 100644 index 000000000..7b1215e63 --- /dev/null +++ b/src/tabs/auth.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/tabs/auth.tsx b/src/tabs/auth.tsx index 574ea18d5..b425e882e 100644 --- a/src/tabs/auth.tsx +++ b/src/tabs/auth.tsx @@ -1,12 +1,10 @@ -import Route, { Wrapper } from "~components/popup/Route"; -import { GlobalStyle, useTheme } from "~utils/theme"; +import Route from "~components/popup/Route"; import { useHashLocation } from "~utils/hash_router"; -import { Provider } from "@arconnect/components"; -import { syncLabels } from "~wallets"; +import { syncLabels, useRemoveCover } from "~wallets"; import { useEffect } from "react"; import { Router } from "wouter"; -import HardwareWalletTheme from "~components/hardware/HardwareWalletTheme"; +import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme"; import Allowance from "~routes/auth/allowance"; import Signature from "~routes/auth/signature"; @@ -18,33 +16,31 @@ import Sign from "~routes/auth/sign"; import Subscription from "~routes/auth/subscription"; import SignKeystone from "~routes/auth/signKeystone"; import BatchSignDataItem from "~routes/auth/batchSignDataItem"; +import { AnimatePresence } from "framer-motion"; export default function Auth() { - const theme = useTheme(); + useRemoveCover(); useEffect(() => { syncLabels(); }, []); return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); } diff --git a/src/tabs/dashboard.html b/src/tabs/dashboard.html new file mode 100644 index 000000000..7b1215e63 --- /dev/null +++ b/src/tabs/dashboard.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/tabs/dashboard.tsx b/src/tabs/dashboard.tsx index b4bc4e2d1..b7f4bc792 100644 --- a/src/tabs/dashboard.tsx +++ b/src/tabs/dashboard.tsx @@ -1,15 +1,12 @@ -import { GlobalStyle, useTheme } from "~utils/theme"; import { useHashLocation } from "~utils/hash_router"; -import { Provider } from "@arconnect/components"; import { syncLabels, useSetUp } from "~wallets"; import { Router, Route } from "wouter"; import { useEffect } from "react"; import Settings from "~routes/dashboard"; +import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme"; export default function Dashboard() { - const theme = useTheme(); - useSetUp(); useEffect(() => { @@ -17,11 +14,10 @@ export default function Dashboard() { }, []); return ( - - + - + ); } diff --git a/src/tabs/devtools.html b/src/tabs/devtools.html new file mode 100644 index 000000000..7b1215e63 --- /dev/null +++ b/src/tabs/devtools.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/tabs/devtools.tsx b/src/tabs/devtools.tsx index 07a226aa6..e3b7cd420 100644 --- a/src/tabs/devtools.tsx +++ b/src/tabs/devtools.tsx @@ -1,19 +1,21 @@ -import { Card, Provider, Spacer, Text } from "@arconnect/components"; -import { GlobalStyle, useTheme } from "~utils/theme"; +import { Card, Spacer, Text } from "@arconnect/components"; import { useEffect, useMemo, useState } from "react"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; import { getTab } from "~applications/tab"; import { getAppURL } from "~utils/format"; -import { useNoWallets } from "~wallets"; +import { useNoWallets, useRemoveCover } from "~wallets"; import AppSettings from "~components/dashboard/subsettings/AppSettings"; import Connector from "~components/devtools/Connector"; import NoWallets from "~components/devtools/NoWallets"; import Application from "~applications/application"; import browser from "webextension-polyfill"; import styled from "styled-components"; +import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme"; + +export default function DevTools() { + useRemoveCover(); -export default function Devtools() { // fetch app data const [app, setApp] = useState(); @@ -44,15 +46,11 @@ export default function Devtools() { return connectedApps.includes(app.url); }, [app, connectedApps]); - // ui theme - const theme = useTheme(); - // no wallets const noWallets = useNoWallets(); return ( - - + {noWallets && } @@ -68,7 +66,7 @@ export default function Devtools() { (connected && app && )} - + ); } diff --git a/src/tabs/fullscreen.html b/src/tabs/fullscreen.html new file mode 100644 index 000000000..15e1dc5d3 --- /dev/null +++ b/src/tabs/fullscreen.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/tabs/fullscreen.tsx b/src/tabs/fullscreen.tsx new file mode 100644 index 000000000..688a42c86 --- /dev/null +++ b/src/tabs/fullscreen.tsx @@ -0,0 +1,5 @@ +import Popup from "~popup"; + +export default function () { + return ; +} diff --git a/src/tabs/welcome.html b/src/tabs/welcome.html new file mode 100644 index 000000000..7b1215e63 --- /dev/null +++ b/src/tabs/welcome.html @@ -0,0 +1,12 @@ + + + + __plasmo_static_index_title__ + + + + + +
+ + diff --git a/src/tabs/welcome.tsx b/src/tabs/welcome.tsx index 223b4539f..527b572c1 100644 --- a/src/tabs/welcome.tsx +++ b/src/tabs/welcome.tsx @@ -1,7 +1,5 @@ import { type Path, pathToRegexp } from "path-to-regexp"; -import { GlobalStyle, useTheme } from "~utils/theme"; import { useHashLocation } from "~utils/hash_router"; -import { Provider } from "@arconnect/components"; import { Router, Route } from "wouter"; import Home from "~routes/welcome"; @@ -10,13 +8,14 @@ import Setup from "~routes/welcome/setup"; import makeCachedMatcher from "wouter/matcher"; import GettingStarted from "~routes/welcome/gettingStarted"; +import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme"; +import { useRemoveCover } from "~wallets"; export default function Welcome() { - const theme = useTheme(); + useRemoveCover(); return ( - - + @@ -32,7 +31,7 @@ export default function Welcome() { )} - + ); } diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index 648a9b70d..7be33ac23 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -16,6 +16,8 @@ import { import type { Alarms } from "webextension-polyfill"; import type { KeystoneSigner } from "~wallets/hardware/keystone"; import browser from "webextension-polyfill"; +import { fetchTokenByProcessId } from "~lib/transactions"; +import { tokenTypeRegistry } from "~tokens/token"; export type AoInstance = ReturnType; @@ -223,7 +225,7 @@ export async function getAoTokenBalance( ): Promise { const aoTokens = (await ExtensionStorage.get("ao_tokens")) || []; - const aoToken = aoTokens.find((token) => token.processId === process); + let aoToken = aoTokens.find((token) => token.processId === process); const res = await dryrun({ Id, @@ -232,13 +234,35 @@ export async function getAoTokenBalance( tags: [{ name: "Action", value: "Balance" }] }); + const errorMessage = (res as any)?.error || res?.Error; + + if (errorMessage) { + throw new Error(errorMessage); + } + + if (res.Messages.length === 0) { + throw new Error( + "Invalid token process: Balance action handler missing or unsupported." + ); + } + for (const msg of res.Messages as Message[]) { const balance = getTagValue("Balance", msg.Tags); - if (balance && aoToken) { + if (balance && +balance) { + if (!aoToken) { + aoToken = await fetchTokenByProcessId(process); + if (!aoToken) { + throw new Error("Could not load token info."); + } + } + return new Quantity(BigInt(balance), BigInt(aoToken.Denomination)); } } + + // default return + return new Quantity(0n, 12n); } export async function getNativeTokenBalance(address: string): Promise { diff --git a/src/tokens/currency.ts b/src/tokens/currency.ts index 3f809167c..84c2c5876 100644 --- a/src/tokens/currency.ts +++ b/src/tokens/currency.ts @@ -10,13 +10,14 @@ export const tokenConfig: Intl.NumberFormatOptions = { * Format token balance */ export function formatTokenBalance( - balance: string | number | BigNumber | Quantity + balance: string | number | BigNumber | Quantity, + config?: number ) { const bigNum = BigNumber.isBigNumber(balance) ? balance : BigNumber(balance.toString()); return bigNum - .toFormat(tokenConfig.maximumFractionDigits) + .toFormat(config ? config : tokenConfig.maximumFractionDigits) .replace(/\.?0*$/, ""); } diff --git a/src/utils/apps.ts b/src/utils/apps.ts index e0fc60285..acdf2f34a 100644 --- a/src/utils/apps.ts +++ b/src/utils/apps.ts @@ -128,8 +128,9 @@ export const apps: App[] = [ lightBackground: "rgba(230, 235, 240, 1)" }, links: { - website: "https://bazar.arweave.dev", - twitter: "https://twitter.com/OurBazAR" + website: "https://bazar.arweave.net", + twitter: "https://twitter.com/OurBazAR", + discord: "https://discord.gg/weavers" } }, { @@ -318,7 +319,7 @@ export const apps: App[] = [ lightBackground: "rgba(230, 235, 240, 1)" }, links: { - website: "https://alex.arweave.dev/", + website: "https://alex.arweave.net/", twitter: "https://twitter.com/thealexarchive", discord: "http://discord.gg/2uZsWuTNvN" } diff --git a/src/utils/format.ts b/src/utils/format.ts index 8216a1bd9..45514e201 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,4 +1,5 @@ import BigNumber from "bignumber.js"; +import type { StoredWallet } from "~wallets"; /** * Get app URL from any link @@ -33,12 +34,27 @@ export function getCommunityUrl(link: string) { * * @returns Formatted address */ -export function formatAddress(address: string, count = 13) { - return ( - address.substring(0, count) + - "..." + - address.substring(address.length - count, address.length) - ); +export function formatAddress( + addressOrWallet?: string | StoredWallet, + count = 13 +) { + // TODO: What about ANS? + + if (!addressOrWallet) return "-"; + + if (typeof addressOrWallet === "string") { + return ( + addressOrWallet.substring(0, count) + + "..." + + addressOrWallet.substring( + addressOrWallet.length - count, + addressOrWallet.length + ) + ); + } + + // TODO: Does it make sense to use the nickname or you only see the active one? + return `${formatAddress(addressOrWallet.address, count)} (you)`; } /** diff --git a/src/utils/icon.ts b/src/utils/icon.ts index 7c41f23e5..bfad9b9c4 100644 --- a/src/utils/icon.ts +++ b/src/utils/icon.ts @@ -1,3 +1,5 @@ +// Production: + import offline64 from "url:/assets/icons/offline/logo64.png"; import offline128 from "url:/assets/icons/offline/logo128.png"; import offline256 from "url:/assets/icons/offline/logo256.png"; @@ -6,33 +8,83 @@ import online64 from "url:/assets/icons/online/logo64.png"; import online128 from "url:/assets/icons/online/logo128.png"; import online256 from "url:/assets/icons/online/logo256.png"; +// Development: + +// Same as production ones, then: +// +// 1. Open with GIMP. +// 2. Colors > Hua-Saturation: +// - Hue = -130 +// - Lightness = -75.5 +// - Saturation = 100 + +import devOffline64 from "url:/assets/icons/offline/logo64.development.png"; +import devOffline128 from "url:/assets/icons/offline/logo128.development.png"; +import devOffline256 from "url:/assets/icons/offline/logo256.development.png"; + +import devOnline64 from "url:/assets/icons/online/logo64.development.png"; +import devOnline128 from "url:/assets/icons/online/logo128.development.png"; +import devOnline256 from "url:/assets/icons/online/logo256.development.png"; + import browser from "webextension-polyfill"; -/** - * Update the popup icon - * - * @param hasPerms Does the site have any permissions? - */ -export async function updateIcon(hasPerms: boolean) { - const offlineLogos = { +interface LogosBySize { + 64: string; + 128: string; + 256: string; +} + +interface LogosByEnvironment { + default: LogosBySize; + development: LogosBySize; +} + +const offlineLogos: LogosByEnvironment = { + default: { 64: offline64, 128: offline128, 256: offline256 - }; - const onlineLogos = { + }, + development: { + 64: devOffline64, + 128: devOffline128, + 256: devOffline256 + } +}; + +const onlineLogos: LogosByEnvironment = { + default: { 64: online64, 128: online128, 256: online256 - }; + }, + development: { + 64: devOnline64, + 128: devOnline128, + 256: devOnline256 + } +}; + +/** + * Update the popup icon + * + * @param hasPerms Does the site have any permissions? + */ +export async function updateIcon(hasPerms: boolean) { + // Set logos if connected / if not connected: + const logosByEnvironment = hasPerms ? onlineLogos : offlineLogos; + + // Use the "gold" version for development: + const logosBySize = + logosByEnvironment[process.env.NODE_ENV] || logosByEnvironment.default; - // set logos if connected / if not connected if (browser.runtime.getManifest().manifest_version === 3) { await browser.action.setIcon({ - path: hasPerms ? onlineLogos : offlineLogos + path: logosBySize }); } else { await browser.browserAction.setIcon({ - path: hasPerms ? onlineLogos : offlineLogos + path: logosBySize }); } } diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 000000000..4081f8409 --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1,9 @@ +/** + * Pauses execution for a given number of milliseconds. + * + * @param {number} ms - Duration to sleep in milliseconds. + * @returns {Promise} Resolves after the specified delay. + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 7b0599be3..2db4c431d 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -1,108 +1,56 @@ -import { createGlobalStyle, css } from "styled-components"; +import { css } from "styled-components"; import type { DisplayTheme } from "@arconnect/components"; import { useEffect, useState } from "react"; import useSetting from "~settings/hook"; -// import fonts -import manropeLight from "url:/assets/fonts/Manrope-Light.woff2"; -import manropeRegular from "url:/assets/fonts/Manrope-Regular.woff2"; -import manropeMedium from "url:/assets/fonts/Manrope-Medium.woff2"; -import manropeSemiBold from "url:/assets/fonts/Manrope-SemiBold.woff2"; -import manropeBold from "url:/assets/fonts/Manrope-Bold.woff2"; -import manropeExtraBold from "url:/assets/fonts/Manrope-ExtraBold.woff2"; - type ThemeSetting = "light" | "dark" | "system"; -/** - * Determinates the theme of the UI - */ -export function useTheme() { - const [theme] = useSetting("display_theme"); - const [displayTheme, setDisplayTheme] = useState("light"); - - useEffect(() => { - if (theme !== "system") { - return setDisplayTheme(theme); - } - - // match theme - const darkModePreference = window.matchMedia( - "(prefers-color-scheme: dark)" - ); +const darkModePreference = + typeof window === "undefined" + ? null + : window.matchMedia("(prefers-color-scheme: dark)"); - setDisplayTheme(darkModePreference.matches ? "dark" : "light"); - - // listen for system theme changes - const listener = (e: MediaQueryListEvent) => - setDisplayTheme(e.matches ? "dark" : "light"); - - darkModePreference.addEventListener("change", listener); - - return () => darkModePreference.removeEventListener("change", listener); - }, [theme]); +function getInitialDisplayTheme(themeSetting: ThemeSetting): DisplayTheme { + if (themeSetting !== "system") { + // "light" or "dark" + return themeSetting; + } - return displayTheme; + return darkModePreference.matches ? "dark" : "light"; } -export const GlobalStyle = createGlobalStyle` - @font-face { - font-family: "ManropeLocal"; - font-style: normal; - font-weight: 300; - src: url(${manropeLight}) format('woff2'); - } - - @font-face { - font-family: "ManropeLocal"; - font-style: normal; - font-weight: 400; - src: url(${manropeRegular}) format('woff2'); - } +export function useTheme() { + const [themeSetting] = useSetting("display_theme"); - @font-face { - font-family: "ManropeLocal"; - font-style: normal; - font-weight: 500; - src: url(${manropeMedium}) format('woff2'); - } + const [displayTheme, setDisplayTheme] = useState(() => { + return getInitialDisplayTheme(themeSetting); + }); - @font-face { - font-family: "ManropeLocal"; - font-style: normal; - font-weight: 600; - src: url(${manropeSemiBold}) format('woff2'); - } + useEffect(() => { + setDisplayTheme(getInitialDisplayTheme(themeSetting)); + }, [themeSetting]); - @font-face { - font-family: "ManropeLocal"; - font-style: normal; - font-weight: 600; - src: url(${manropeBold}) format('woff2'); - } + useEffect(() => { + if (themeSetting !== "system") return; - @font-face { - font-family: "ManropeLocal"; - font-style: normal; - font-weight: 700; - src: url(${manropeExtraBold}) format('woff2'); - } + function handleDarkModePreferenceChange(e: MediaQueryListEvent) { + setDisplayTheme(e.matches ? "dark" : "light"); + } - body { - margin: 0; - padding: 0; - min-height: 500px; - transition: background-color .23s ease-in-out; - } + darkModePreference.addEventListener( + "change", + handleDarkModePreferenceChange + ); - body, button, input, select, textarea { - font-family: "ManropeLocal", "Manrope VF", "Manrope", sans-serif !important; - } + return () => + darkModePreference.removeEventListener( + "change", + handleDarkModePreferenceChange + ); + }, [themeSetting]); - ::selection { - background-color: rgba(${(props) => props.theme.theme}, .6); - color: #fff; - } -`; + return displayTheme; +} /** * Hover effect css diff --git a/src/wallets/index.ts b/src/wallets/index.ts index 0655bb0c1..cefc32977 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -46,28 +46,98 @@ export async function getWallets() { return wallets || []; } +type InitialScreenType = "cover" | "locked" | "generating" | "default"; + /** * Hook that opens a new tab if ArConnect has not been set up yet */ -export const useSetUp = () => +export function useSetUp() { + const [initialScreenType, setInitialScreenType] = + useState("cover"); + useEffect(() => { - (async () => { - const activeAddress = await getActiveAddress(); - const wallets = await getWallets(); + async function checkWalletState() { + const [activeAddress, wallets, decryptionKey] = await Promise.all([ + getActiveAddress(), + getWallets(), + getDecryptionKey() + ]); + + const hasWallets = activeAddress && wallets.length > 0; + + let nextInitialScreenType: InitialScreenType = "cover"; + + switch (process.env.PLASMO_PUBLIC_APP_TYPE) { + // `undefined` has been added here just in case, so that the default behavior if nothing is specific is + // building the browser extension, just like it was before adding support for the embedded wallet: + case undefined: + case "extension": { + if (!hasWallets) { + await browser.tabs.create({ + url: browser.runtime.getURL("tabs/welcome.html") + }); + + window.top.close(); + } else if (!decryptionKey) { + nextInitialScreenType = "locked"; + } else { + nextInitialScreenType = "default"; + } + + break; + } + + case "embedded": { + nextInitialScreenType = !hasWallets ? "generating" : "default"; + + break; + } + + default: { + throw new Error( + `Unknown APP_TYPE = ${process.env.PLASMO_PUBLIC_APP_TYPE}` + ); + } + } + + setInitialScreenType(nextInitialScreenType); + + const coverElement = document.getElementById("cover"); - if ( - !activeAddress || - activeAddress === "" || - wallets.length === 0 || - !wallets - ) { - await browser.tabs.create({ - url: browser.runtime.getURL("tabs/welcome.html") - }); - window.top.close(); + if (coverElement) { + if (nextInitialScreenType === "cover") { + coverElement.removeAttribute("aria-hidden"); + } else { + coverElement.setAttribute("aria-hidden", "true"); + } } - })(); + } + + ExtensionStorage.watch({ + decryption_key: checkWalletState + }); + + checkWalletState(); + + return () => { + ExtensionStorage.unwatch({ + decryption_key: checkWalletState + }); + }; + }, []); + + return initialScreenType; +} + +export function useRemoveCover() { + useEffect(() => { + const coverElement = document.getElementById("cover"); + + if (coverElement) { + coverElement.setAttribute("aria-hidden", "true"); + } }, []); +} /** * Hook to get if there are no wallets added @@ -91,20 +161,14 @@ export const useNoWallets = () => { * Hook for decryption key */ export function useDecryptionKey(): [string, (val: string) => void] { - const [decryptionKey, setDecryptionKey] = useStorage( - { - key: "decryption_key", - instance: ExtensionStorage - }, - (val) => { - if (!val) return undefined; - return atob(val); - } - ); + const [decryptionKey, setDecryptionKey] = useStorage({ + key: "decryption_key", + instance: ExtensionStorage + }); const set = (val: string) => setDecryptionKey(btoa(val)); - return [decryptionKey, set]; + return [decryptionKey ? atob(decryptionKey) : undefined, set]; } /** @@ -250,6 +314,40 @@ export async function getKeyfile(address: string): Promise { return decryptedWallet; } +/** + * Function to generate a unique nickname for a wallet. + * + * This function scans the existing wallets to find the next available + * nickname in the format "Account X", where X is the smallest unused + * positive integer. + * + * @param {WalletWithNickname[]} wallets - The array of existing wallets, each with a potential nickname. + * @returns {string} The unique nickname for the new wallet. + */ +function generateUniqueNickname(wallets: WalletWithNickname[]): string { + const existingNumbers = new Set(); + + // Populate the set with existing account numbers + wallets.forEach((wallet) => { + const match = wallet?.nickname?.match(/Account (\d+)/); + if (match) { + const accountNumber = parseInt(match[1], 10); + // Add positive account numbers to the set + if (accountNumber > 0) { + existingNumbers.add(accountNumber); + } + } + }); + + // Find the next available number + let number = 1; + while (existingNumbers.has(number)) { + number++; + } + + return `Account ${number}`; +} + /** * Add a wallet for the user * @@ -296,7 +394,7 @@ export async function addWallet( wallets.push({ type: "local", // @ts-expect-error - nickname: item.nickname || `Account ${wallets.length + 1}`, + nickname: item.nickname || generateUniqueNickname(wallets), address, keyfile: encrypted }); diff --git a/yarn.lock b/yarn.lock index d888d11db..feacc163d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,16 +1151,6 @@ "@lezer/lr" "^1.0.0" json5 "^2.2.1" -"@motionone/animation@^10.13.1": - version "10.18.0" - resolved "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz#868d00b447191816d5d5cf24b1cafa144017922b" - integrity sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw== - dependencies: - "@motionone/easing" "^10.18.0" - "@motionone/types" "^10.17.1" - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - "@motionone/animation@^10.17.0": version "10.17.0" resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.17.0.tgz#7633c6f684b5fee2b61c405881b8c24662c68fca" @@ -1171,18 +1161,6 @@ "@motionone/utils" "^10.17.0" tslib "^2.3.1" -"@motionone/dom@10.13.1": - version "10.13.1" - resolved "https://registry.npmjs.org/@motionone/dom/-/dom-10.13.1.tgz#fc29ea5d12538f21b211b3168e502cfc07a24882" - integrity sha512-zjfX+AGMIt/fIqd/SL1Lj93S6AiJsEA3oc5M9VkUr+Gz+juRmYN1vfvZd6MvEkSqEjwPQgcjN7rGZHrDB9APfQ== - dependencies: - "@motionone/animation" "^10.13.1" - "@motionone/generators" "^10.13.1" - "@motionone/types" "^10.13.0" - "@motionone/utils" "^10.13.1" - hey-listen "^1.0.8" - tslib "^2.3.1" - "@motionone/dom@^10.15.3": version "10.17.0" resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.17.0.tgz#519dd78aab0750a94614c69a82da5290cd617383" @@ -1203,23 +1181,6 @@ "@motionone/utils" "^10.17.0" tslib "^2.3.1" -"@motionone/easing@^10.18.0": - version "10.18.0" - resolved "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz#7b82f6010dfee3a1bb0ee83abfbaff6edae0c708" - integrity sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg== - dependencies: - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/generators@^10.13.1": - version "10.18.0" - resolved "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz#fe09ab5cfa0fb9a8884097feb7eb60abeb600762" - integrity sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg== - dependencies: - "@motionone/types" "^10.17.1" - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - "@motionone/generators@^10.17.0": version "10.17.0" resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.17.0.tgz#878d292539c41434c13310d5f863a87a94e6e689" @@ -1229,25 +1190,11 @@ "@motionone/utils" "^10.17.0" tslib "^2.3.1" -"@motionone/types@^10.13.0", "@motionone/types@^10.17.1": - version "10.17.1" - resolved "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz#cf487badbbdc9da0c2cb86ffc1e5d11147c6e6fb" - integrity sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A== - "@motionone/types@^10.17.0": version "10.17.0" resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.17.0.tgz#179571ce98851bac78e19a1c3974767227f08ba3" integrity sha512-EgeeqOZVdRUTEHq95Z3t8Rsirc7chN5xFAPMYFobx8TPubkEfRSm5xihmMUkbaR2ErKJTUw3347QDPTHIW12IA== -"@motionone/utils@^10.13.1", "@motionone/utils@^10.18.0": - version "10.18.0" - resolved "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz#a59ff8932ed9009624bca07c56b28ef2bb2f885e" - integrity sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw== - dependencies: - "@motionone/types" "^10.17.1" - hey-listen "^1.0.8" - tslib "^2.3.1" - "@motionone/utils@^10.17.0": version "10.17.0" resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.17.0.tgz#cc0ba8acdc6848ff48d8c1f2d0d3e7602f4f942e" @@ -6791,19 +6738,12 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -framer-motion@7.5.3: - version "7.5.3" - resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-7.5.3.tgz#a1de7d6c4abbf7333619d4a6c8df74c12240be43" - integrity sha512-VvANga9Z7bYtKMAsM/je81FwJDHfThOYywN04xVQ4OGdMVY09Bowx/q7nZd6XtytLuv6byc6GT1mYwag+SQ/nw== +framer-motion@^11.11.7: + version "11.11.7" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.11.7.tgz#3af277845ff690f6935fbcf781fa3eb4146e2321" + integrity sha512-89CgILOXPeG3L7ymOTGrLmf8IiKubYLUN/QkYgQuLvehAHfqgwJbLfCnhuyRI4WTds1TXkUp67A7IJrgRY/j1w== dependencies: - "@motionone/dom" "10.13.1" - framesync "6.1.2" - hey-listen "^1.0.8" - popmotion "11.0.5" - style-value-types "5.1.2" - tslib "2.4.0" - optionalDependencies: - "@emotion/is-prop-valid" "^0.8.2" + tslib "^2.4.0" framer-motion@^7.5.3: version "7.10.3" @@ -6816,13 +6756,6 @@ framer-motion@^7.5.3: optionalDependencies: "@emotion/is-prop-valid" "^0.8.2" -framesync@6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz#755eff2fb5b8f3b4d2b266dd18121b300aefea27" - integrity sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g== - dependencies: - tslib "2.4.0" - from2@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -9856,16 +9789,6 @@ plimit-lit@^3.0.1: 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" - integrity sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA== - dependencies: - framesync "6.1.2" - hey-listen "^1.0.8" - style-value-types "5.1.2" - tslib "2.4.0" - possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -11163,14 +11086,6 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== -style-value-types@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz#6be66b237bd546048a764883528072ed95713b62" - integrity sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q== - dependencies: - hey-listen "^1.0.8" - tslib "2.4.0" - styled-components@^5.3.6: version "5.3.11" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8"