From b8c91f2e28df9e5c99b2450641255ace370e3e7f Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Tue, 9 Jul 2024 16:55:48 -0700 Subject: [PATCH 01/33] fix: updated copy for permissions --- assets/_locales/en/messages.json | 18 ++--- assets/_locales/zh_CN/messages.json | 18 ++--- .../dashboard/subsettings/AppSettings.tsx | 65 +++++++++++-------- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 7e7e65de..ee264c1f 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -248,39 +248,39 @@ "description": "Send transaction button" }, "permissionDescriptionAccessAddress": { - "message": "Access the current address selected in ArConnect", + "message": "Allows access to the active address", "description": "Description for the \"ACCESS_ADDRESS\" permission" }, "permissionDescriptionAccessPublicKey": { - "message": "Access the public key of the current address selected in ArConnect", + "message": "Allows access to the active address public key", "description": "Description for the \"ACCESS_PUBLIC_KEY\" permission" }, "permissionDescriptionAccessAllAddresses": { - "message": "Access all addresses added to ArConnect", + "message": "Allows access to all the active and non-active wallet addresses", "description": "Description for the \"ACCESS_ALL_ADDRESSES\" permission" }, "permissionDescriptionSign": { - "message": "Sign a transaction", + "message": "Allows the signing of a transaction without a review and confirmation", "description": "Description for the \"SIGN_TRANSACTION\" permission" }, "permissionDescriptionEncrypt": { - "message": "Encrypt data using the user's keyfile", + "message": "Allows encrypting data using wallet's keyfile. It does not grant access to keyfile.", "description": "Description for the \"ENCRYPT\" permission" }, "permissionDescriptionDecrypt": { - "message": "Decrypt data using the user's keyfile", + "message": "Allows decrypting data using wallet's keyfile. It does not grant access to keyfile.", "description": "Description for the \"DECRYPT\" permission" }, "permissionDescriptionSignature": { - "message": "Sign data using the user's keyfile", + "message": "Allows the signing of data without a review and confirmation", "description": "Description for the \"SIGNATURE\" permission" }, "permissionDescriptionArweaveConfig": { - "message": "Access the user's custom Arweave config", + "message": "Access read access to ArConnect configuration file", "description": "Description for the \"ACCESS_ARWEAVE_CONFIG\" permission" }, "permissionDescriptionDispatch": { - "message": "Dispatch an Arweave transaction or interaction", + "message": "Allows using dispatch transactions without a review and confirmation.", "description": "Description for the \"DISPATCH\" permission" }, "copyId": { diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 7851219e..d4ae3f57 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -248,39 +248,39 @@ "description": "Send transaction button" }, "permissionDescriptionAccessAddress": { - "message": "访问 ArConnect 中选择的当前地址", + "message": "允许访问活动地址", "description": "Description for the \"ACCESS_ADDRESS\" permission" }, "permissionDescriptionAccessPublicKey": { - "message": "访问 ArConnect 中选择的当前地址的公钥", + "message": "允许访问活动地址的公钥", "description": "Description for the \"ACCESS_PUBLIC_KEY\" permission" }, "permissionDescriptionAccessAllAddresses": { - "message": "访问添加到 ArConnect 的所有地址", + "message": "允许访问所有活动和非活动钱包地址", "description": "Description for the \"ACCESS_ALL_ADDRESSES\" permission" }, "permissionDescriptionSign": { - "message": "签署交易", + "message": "允许在没有审核和确认的情况下签署交易", "description": "Description for the \"SIGN_TRANSACTION\" permission" }, "permissionDescriptionEncrypt": { - "message": "使用用户的密钥文件加密数据", + "message": "允许使用钱包的密钥文件加密数据。这并不授予对密钥文件的访问权限", "description": "Description for the \"ENCRYPT\" permission" }, "permissionDescriptionDecrypt": { - "message": "使用用户的密钥文件解密数据", + "message": "允许使用钱包的密钥文件解密数据。这并不授予对密钥文件的访问权限", "description": "Description for the \"DECRYPT\" permission" }, "permissionDescriptionSignature": { - "message": "使用用户的密钥文件签署数据", + "message": "允许在没有审核和确认的情况下签署数据", "description": "Description for the \"SIGNATURE\" permission" }, "permissionDescriptionArweaveConfig": { - "message": "访问用户的自定义 Arweave 配置", + "message": "允许读取 ArConnect 配置文件", "description": "Description for the \"ACCESS_ARWEAVE_CONFIG\" permission" }, "permissionDescriptionDispatch": { - "message": "发送 Arweave 交易或互动", + "message": "允许在没有审核和确认的情况下使用派发交易", "description": "Description for the \"DISPATCH\" permission" }, "copyId": { diff --git a/src/components/dashboard/subsettings/AppSettings.tsx b/src/components/dashboard/subsettings/AppSettings.tsx index 0d950b53..29784df4 100644 --- a/src/components/dashboard/subsettings/AppSettings.tsx +++ b/src/components/dashboard/subsettings/AppSettings.tsx @@ -111,34 +111,45 @@ export default function AppSettings({ app, showTitle = false }: Props) { )} {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 - ); - } + {Object.keys(permissionData).map((permissionName: PermissionType, i) => { + let formattedPermissionName = permissionName + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" "); + + if (permissionName === "SIGNATURE") { + formattedPermissionName = "Sign Data"; + } - return val; - }) - } - checked={settings.permissions.includes(permissionName)} - > - {permissionName} -
- - {browser.i18n.getMessage(permissionData[permissionName])} - -
- {i !== Object.keys(permissionData).length - 1 && } -
- ))} + return ( +
+ + 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)} + > + {formattedPermissionName} +
+ + {browser.i18n.getMessage(permissionData[permissionName])} + +
+ {i !== Object.keys(permissionData).length - 1 && } +
+ ); + })} {browser.i18n.getMessage("allowance")} Date: Tue, 9 Jul 2024 17:33:03 -0700 Subject: [PATCH 02/33] fix: updated quick settings permissions titles --- .../popup/settings/apps/[url]/permissions.tsx | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/src/routes/popup/settings/apps/[url]/permissions.tsx b/src/routes/popup/settings/apps/[url]/permissions.tsx index 2f9ea830..2145bd6c 100644 --- a/src/routes/popup/settings/apps/[url]/permissions.tsx +++ b/src/routes/popup/settings/apps/[url]/permissions.tsx @@ -9,7 +9,7 @@ import { useLocation } from "wouter"; export default function AppPermissions({ url }: Props) { // app settings - const app = new Application(url); + const app = new Application(decodeURIComponent(url)); const [settings, updateSettings] = app.hook(); const [, setLocation] = useLocation(); @@ -24,42 +24,53 @@ export default function AppPermissions({ url }: Props) { {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 - ); - } + (permissionName: PermissionType, i) => { + let formattedPermissionName = permissionName + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" "); - return val; - }) - } - checked={settings.permissions.includes(permissionName)} - /> -
- {permissionName} - - {browser.i18n.getMessage(permissionData[permissionName])} - -
-
- {i !== Object.keys(permissionData).length - 1 && ( - - )} -
- ) + if (permissionName === "SIGNATURE") { + formattedPermissionName = "Sign Data"; + } + + return ( +
+ + + 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)} + /> +
+ {formattedPermissionName} + + {browser.i18n.getMessage(permissionData[permissionName])} + +
+
+ {i !== Object.keys(permissionData).length - 1 && ( + + )} +
+ ); + } )}
From 4f3433c688e3487186c43163933404d661779fb8 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 10 Jul 2024 18:00:58 +0545 Subject: [PATCH 03/33] feat: Add edit permissions screen --- assets/_locales/en/messages.json | 8 ++ assets/_locales/zh_CN/messages.json | 8 ++ src/components/Checkbox.tsx | 27 +++-- src/components/auth/App.tsx | 33 +++--- src/routes/auth/permissions.tsx | 169 ++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 src/routes/auth/permissions.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 7e7e65de..4e6671af 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -49,6 +49,10 @@ "message": "Hide", "description": "Hide text" }, + "save": { + "message": "Save", + "description": "Save button text" + }, "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" @@ -295,6 +299,10 @@ "message": "Permissions", "description": "Permissions title" }, + "edit_permissions": { + "message": "Edit permissions", + "description": "Edit permissions title" + }, "allowance": { "message": "Allowance", "description": "Allowance title" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 7851219e..9e9e6870 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -49,6 +49,10 @@ "message": "隐藏", "description": "Hide text" }, + "save": { + "message": "节省", + "description": "Save button text" + }, "add_wallet": { "message": "添加钱包", "description": "Add a wallet text" @@ -295,6 +299,10 @@ "message": "权限", "description": "Permissions title" }, + "edit_permissions": { + "message": "编辑权限", + "description": "Edit permissions title" + }, "allowance": { "message": "额度", "description": "Allowance title" diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 22fcbfbd..26a7336c 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,10 @@ -import { useEffect, useMemo, useState, type HTMLProps } from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + type HTMLProps +} from "react"; import styled from "styled-components"; export const Checkbox = ({ @@ -10,18 +16,15 @@ export const Checkbox = ({ const [state, setState] = useState(checked); const effectiveId = useMemo(() => id || generateUniqueId(), []); - async function toggle() { - let newVal = state; - - setState((val) => { - newVal = !val; - return newVal; + const toggle = useCallback(async () => { + setState((prevState) => { + const newState = !prevState; + if (onChange) { + onChange(newState); + } + return newState; }); - - if (onChange) { - await onChange(newVal); - } - } + }, [onChange]); useEffect(() => setState(checked), [checked]); diff --git a/src/components/auth/App.tsx b/src/components/auth/App.tsx index 1facd10f..2c6da81f 100644 --- a/src/components/auth/App.tsx +++ b/src/components/auth/App.tsx @@ -22,7 +22,8 @@ export default function App({ appName, appUrl, gateway, - allowance + allowance, + showTitle = true }: Props) { // allowance spent in AR const spent = useMemo(() => { @@ -50,14 +51,18 @@ export default function App({ return ( <> - - - - + {showTitle && ( + <> + + + + + + )} @@ -103,12 +108,9 @@ const SidePaddingSection = styled(Section)` `; const Wrapper = styled.div<{ displayTheme: DisplayTheme }>` - background-color: rgb( - ${(props) => - props.displayTheme === "light" ? "0, 0, 0" : props.theme.cardBackground} - ); - border-radius: 27px; - padding: 1rem; + border-radius: 10px; + padding-top: 1rem; + padding-bottom: 1rem; display: flex; align-items: center; justify-content: space-between; @@ -164,4 +166,5 @@ interface Props { appUrl: string; gateway?: Gateway; allowance?: Allowance; + showTitle?: boolean; } diff --git a/src/routes/auth/permissions.tsx b/src/routes/auth/permissions.tsx new file mode 100644 index 00000000..00b30da4 --- /dev/null +++ b/src/routes/auth/permissions.tsx @@ -0,0 +1,169 @@ +import { ButtonV2, Section, Text } from "@arconnect/components"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import { permissionData, type PermissionType } from "~applications/permissions"; +import Checkbox from "~components/Checkbox"; +import { useEffect, useMemo, useState } from "react"; +import { defaultGateway, type Gateway } from "~gateways/gateway"; +import type { AppInfo } from "~applications/application"; +import { useAuthParams } from "~utils/auth"; +import HeadV2 from "~components/popup/HeadV2"; +import App from "~components/auth/App"; + +export default function Permissions() { + const [permissions, setPermissions] = useState>( + new Map() + ); + + // connect params + const params = useAuthParams<{ + url: string; + permissions: PermissionType[]; + appInfo: AppInfo; + gateway?: Gateway; + }>(); + + // app data + const appData = useMemo(() => { + if (!params) return {}; + + return params.appInfo; + }, [params]); + + // app url + const appUrl = useMemo(() => { + if (!params) return ""; + + return params.url; + }, [params]); + + useEffect(() => { + if (!params) return; + + setPermissions( + new Map(params.permissions.map((permission) => [permission, true])) + ); + }, [params]); + + function onSave() { + const updatedPermissions = Array.from(permissions.entries()) + .filter(([, value]) => value) + .map(([key]) => key); + + const updatedParams = { + ...params, + permissions: updatedPermissions, + type: "connect" + }; + + // TODO: Code to go back + } + + return ( + +
+ + +
+ {browser.i18n.getMessage("permissions")} + + {Object.keys(permissionData).map( + (permissionName: PermissionType, i) => { + let formattedPermissionName = permissionName + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" "); + + if (permissionName === "SIGNATURE") { + formattedPermissionName = "Sign Data"; + } + + return ( +
+ + { + setPermissions((prevPermissions) => { + const newPermissions = new Map(prevPermissions); + newPermissions.set(permissionName, checked); + return newPermissions; + }); + }} + checked={!!permissions.get(permissionName)} + /> +
+ + {formattedPermissionName} + + + {browser.i18n.getMessage( + permissionData[permissionName] + )} + +
+
+
+ ); + } + )} +
+
+
+
+ + {browser.i18n.getMessage("save")} + +
+
+ ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 100vh; +`; + +const Title = styled(Text).attrs({ + heading: true +})` + margin-bottom: 0.75em; + font-size: 1.125rem; +`; + +const PermissionsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; + +const Permission = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +export const PermissionDescription = styled(Text).attrs({ + noMargin: true +})` + font-size: 0.625rem; +`; + +export const PermissionTitle = styled(Text).attrs({ + noMargin: true, + heading: true +})` + font-size: 0.875rem; +`; From ceb033d21f6d3387f99ad2d3acf87b6fde125566 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 10 Jul 2024 22:20:45 +0545 Subject: [PATCH 04/33] feat(wallet): add validation for key length during import --- assets/_locales/en/messages.json | 12 +++ assets/_locales/zh_CN/messages.json | 12 +++ src/components/WalletKeySizeErrorModal.tsx | 101 ++++++++++++++++++ .../dashboard/subsettings/AddWallet.tsx | 18 +++- src/routes/welcome/load/wallets.tsx | 15 +++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/components/WalletKeySizeErrorModal.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 7e7e65de..257f87ea 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -49,6 +49,10 @@ "message": "Hide", "description": "Hide text" }, + "close": { + "message": "Close", + "description": "Close button text" + }, "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" @@ -665,6 +669,14 @@ "message": "Error exporting wallet", "description": "Wallet export error notification" }, + "import_wallet_key_length_short_error_title": { + "message": "Key Length Too Short", + "description": "Key Length Too Short error title" + }, + "import_wallet_key_length_short_error": { + "message": "The key length of the wallet you are importing is less than the required length to transact on the AO network. It is recommended you create a new ArConnect wallet and transfer your assets to your new wallet.", + "description": "Key Length Too Short error" + }, "development_version": { "message": "Development", "description": "Development version badge text" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 7851219e..6bcb11a2 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -49,6 +49,10 @@ "message": "隐藏", "description": "Hide text" }, + "close": { + "message": "关闭", + "description": "Close button text" + }, "add_wallet": { "message": "添加钱包", "description": "Add a wallet text" @@ -665,6 +669,14 @@ "message": "导出钱包出错", "description": "Wallet export error notification" }, + "import_wallet_key_length_short_error_title": { + "message": "密钥长度太短", + "description": "Key Length Too Short error" + }, + "import_wallet_key_length_short_error": { + "message": "您导入的钱包的密钥长度小于在 AO 网络上进行交易所需的长度。建议您创建一个新的ArConnect钱包,并将您的资产转移到新钱包中。", + "description": "Key Length Too Short error" + }, "development_version": { "message": "开发版本", "description": "Development version badge text" diff --git a/src/components/WalletKeySizeErrorModal.tsx b/src/components/WalletKeySizeErrorModal.tsx new file mode 100644 index 00000000..be43ff48 --- /dev/null +++ b/src/components/WalletKeySizeErrorModal.tsx @@ -0,0 +1,101 @@ +import { + ButtonV2, + ModalV2, + Spacer, + Text, + type DisplayTheme +} from "@arconnect/components"; +import { useRef } from "react"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; + +interface Props { + isOpen: boolean; + setOpen: (value: boolean) => void; + back?: () => void; +} + +export const WalletKeySizeErrorModal = ({ isOpen, setOpen, back }: Props) => { + const modalRef = useRef(null); + + return ( + + + +
+ + {browser.i18n.getMessage( + "import_wallet_key_length_short_error_title" + )} + + + + {browser.i18n.getMessage("import_wallet_key_length_short_error")} + + +
+
+ { + setOpen(false); + if (back) { + back(); + } + }} + > + {browser.i18n.getMessage("close")} + +
+
+ ); +}; + +const Content = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: none; + align-self: stretch; + flex-grow: 0; +`; + +const ContentWrapper = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: space-between; +`; + +const CenterText = styled(Text).attrs({ + noMargin: true +})<{ displayTheme?: DisplayTheme }>` + width: 245px; + text-align: center; + color: ${(props) => + props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; + font-weight: 500; + font-size: 11px; + line-height: 16px; + align-self: stretch; + flex: none; + flex-grow: 0; + + a { + color: rgb(${(props) => props.theme.theme}); + text-decoration: none; + } +`; + +const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>` + font-size: 18px; + font-weight: 500; + color: ${(props) => + props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; +`; diff --git a/src/components/dashboard/subsettings/AddWallet.tsx b/src/components/dashboard/subsettings/AddWallet.tsx index 324a508d..43494eb3 100644 --- a/src/components/dashboard/subsettings/AddWallet.tsx +++ b/src/components/dashboard/subsettings/AddWallet.tsx @@ -11,7 +11,8 @@ import { Spacer, useToasts, ButtonV2, - InputV2 + InputV2, + useModal } from "@arconnect/components"; import BackupWalletPage from "~components/welcome/generate/BackupWalletPage"; import KeystoneButton from "~components/hardware/KeystoneButton"; @@ -21,11 +22,16 @@ import * as bip39 from "bip39-web-crypto"; import Arweave from "arweave/web/common"; import styled from "styled-components"; import { defaultGateway } from "~gateways/gateway"; +import { ArweaveSigner } from "arbundles"; +import { WalletKeySizeErrorModal } from "~components/WalletKeySizeErrorModal"; export default function AddWallet() { // password input const passwordInput = useInput(); + // wallet size error modal + const walletModal = useModal(); + // toasts const { setToast } = useToasts(); @@ -132,6 +138,15 @@ export default function AddWallet() { ? await jwkFromMnemonic(providedWallet) : providedWallet; + const signer = new ArweaveSigner(jwk); + const expectedLength = signer.ownerLength; + const actualLength = signer.publicKey.byteLength; + if (expectedLength !== actualLength) { + walletModal.setOpen(true); + finishUp(); + return; + } + await addWallet(jwk, passwordInput.state); // send success toast @@ -326,6 +341,7 @@ export default function AddWallet() { {browser.i18n.getMessage("generate_wallet")} +
); } diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx index 1bca81ea..13cf9910 100644 --- a/src/routes/welcome/load/wallets.tsx +++ b/src/routes/welcome/load/wallets.tsx @@ -23,6 +23,8 @@ import Paragraph from "~components/Paragraph"; import browser from "webextension-polyfill"; import styled from "styled-components"; import { addExpiration } from "~wallets/auth"; +import { ArweaveSigner } from "arbundles"; +import { WalletKeySizeErrorModal } from "~components/WalletKeySizeErrorModal"; export default function Wallets() { // password context @@ -37,6 +39,9 @@ export default function Wallets() { // migration modal const migrationModal = useModal(); + // wallet size error modal + const walletModal = useModal(); + // wallets to migrate const [walletsToMigrate, setWalletsToMigrate] = useState([]); @@ -131,6 +136,15 @@ export default function Wallets() { ? await jwkFromMnemonic(loadedWallet) : loadedWallet; + const signer = new ArweaveSigner(jwk); + const expectedLength = signer.ownerLength; + const actualLength = signer.publicKey.byteLength; + if (expectedLength !== actualLength) { + walletModal.setOpen(true); + finishUp(); + return; + } + // add wallet await addWallet(jwk, password); await addExpiration(); @@ -248,6 +262,7 @@ export default function Wallets() { + setLocation(`/`)} /> ); } From c9f8c522b44d37ade2a9cdbe403ca5fdd0ae89b0 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 10 Jul 2024 22:41:04 +0545 Subject: [PATCH 05/33] fix: check bits only after logged in --- src/routes/popup/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx index d9a6cd89..5a512ff2 100644 --- a/src/routes/popup/index.tsx +++ b/src/routes/popup/index.tsx @@ -107,6 +107,8 @@ export default function Home() { useEffect(() => { const checkBits = async () => { + if (!loggedIn) return; + const bits = await checkWalletBits(); if (bits === null) { @@ -117,7 +119,7 @@ export default function Home() { }; checkBits(); - }, []); + }, [loggedIn]); useEffect(() => { // check whether to show announcement From 22c5b2327e23ac8c6d9930bfb3993f5b4c0c1704 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Wed, 10 Jul 2024 23:11:08 +0545 Subject: [PATCH 06/33] refactor: Place common modal components in single component --- .../dashboard/subsettings/AddWallet.tsx | 2 +- src/components/modals/Components.tsx | 47 ++++++++ .../{ => modals}/WalletKeySizeErrorModal.tsx | 55 +--------- src/routes/popup/announcement.tsx | 100 ++---------------- src/routes/popup/send/announcement.tsx | 60 ++--------- src/routes/welcome/load/wallets.tsx | 2 +- 6 files changed, 65 insertions(+), 201 deletions(-) create mode 100644 src/components/modals/Components.tsx rename src/components/{ => modals}/WalletKeySizeErrorModal.tsx (51%) diff --git a/src/components/dashboard/subsettings/AddWallet.tsx b/src/components/dashboard/subsettings/AddWallet.tsx index 43494eb3..1a5e7948 100644 --- a/src/components/dashboard/subsettings/AddWallet.tsx +++ b/src/components/dashboard/subsettings/AddWallet.tsx @@ -23,7 +23,7 @@ import Arweave from "arweave/web/common"; import styled from "styled-components"; import { defaultGateway } from "~gateways/gateway"; import { ArweaveSigner } from "arbundles"; -import { WalletKeySizeErrorModal } from "~components/WalletKeySizeErrorModal"; +import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; export default function AddWallet() { // password input diff --git a/src/components/modals/Components.tsx b/src/components/modals/Components.tsx new file mode 100644 index 00000000..a385ab42 --- /dev/null +++ b/src/components/modals/Components.tsx @@ -0,0 +1,47 @@ +import { Text, type DisplayTheme } from "@arconnect/components"; +import styled from "styled-components"; + +export const Content = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: none; + align-self: stretch; + flex-grow: 0; +`; + +export const ContentWrapper = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: space-between; +`; + +export const CenterText = styled(Text).attrs({ + noMargin: true +})<{ displayTheme?: DisplayTheme }>` + width: 245px; + text-align: center; + color: ${(props) => + props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; + font-weight: 500; + font-size: 11px; + line-height: 16px; + align-self: stretch; + flex: none; + flex-grow: 0; + + a { + color: rgb(${(props) => props.theme.theme}); + text-decoration: none; + } +`; + +export const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>` + font-size: 18px; + font-weight: 500; + color: ${(props) => + props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; +`; diff --git a/src/components/WalletKeySizeErrorModal.tsx b/src/components/modals/WalletKeySizeErrorModal.tsx similarity index 51% rename from src/components/WalletKeySizeErrorModal.tsx rename to src/components/modals/WalletKeySizeErrorModal.tsx index be43ff48..cff645bd 100644 --- a/src/components/WalletKeySizeErrorModal.tsx +++ b/src/components/modals/WalletKeySizeErrorModal.tsx @@ -1,13 +1,7 @@ -import { - ButtonV2, - ModalV2, - Spacer, - Text, - type DisplayTheme -} from "@arconnect/components"; +import { ButtonV2, ModalV2, Spacer } from "@arconnect/components"; import { useRef } from "react"; import browser from "webextension-polyfill"; -import styled from "styled-components"; +import { ContentWrapper, Content, HeaderText, CenterText } from "./Components"; interface Props { isOpen: boolean; @@ -54,48 +48,3 @@ export const WalletKeySizeErrorModal = ({ isOpen, setOpen, back }: Props) => { ); }; - -const Content = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: none; - align-self: stretch; - flex-grow: 0; -`; - -const ContentWrapper = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: space-between; -`; - -const CenterText = styled(Text).attrs({ - noMargin: true -})<{ displayTheme?: DisplayTheme }>` - width: 245px; - text-align: center; - color: ${(props) => - props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; - font-weight: 500; - font-size: 11px; - line-height: 16px; - align-self: stretch; - flex: none; - flex-grow: 0; - - a { - color: rgb(${(props) => props.theme.theme}); - text-decoration: none; - } -`; - -const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>` - font-size: 18px; - font-weight: 500; - color: ${(props) => - props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; -`; diff --git a/src/routes/popup/announcement.tsx b/src/routes/popup/announcement.tsx index 349c012d..7b649c2b 100644 --- a/src/routes/popup/announcement.tsx +++ b/src/routes/popup/announcement.tsx @@ -1,16 +1,15 @@ -import { - ButtonV2, - ModalV2, - Spacer, - Text, - type DisplayTheme -} from "@arconnect/components"; +import { ButtonV2, ModalV2, Spacer } from "@arconnect/components"; import { ExtensionStorage } from "~utils/storage"; import { useEffect, useRef, useState } from "react"; import browser from "webextension-polyfill"; import aoLogo from "url:/assets/ecosystem/ao-token-logo.png"; -import styled from "styled-components"; import { useStorage } from "@plasmohq/storage/hook"; +import { + ContentWrapper, + Content, + HeaderText, + CenterText +} from "~components/modals/Components"; export const AnnouncementPopup = ({ isOpen, setOpen }) => { const [notifications, setNotifications] = useStorage({ @@ -27,11 +26,6 @@ export const AnnouncementPopup = ({ isOpen, setOpen }) => { } }, [notifications]); - const handleCheckbox = async () => { - setChecked((prev) => !prev); - setNotifications((prev) => !prev); - }; - const handleClickOutside = (event) => { if (modalRef.current && !modalRef.current.contains(event.target)) { ExtensionStorage.set("show_announcement", false); @@ -87,83 +81,3 @@ export const AnnouncementPopup = ({ isOpen, setOpen }) => { ); }; - -export const Content = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: none; - align-self: stretch; - flex-grow: 0; -`; - -export const ContentWrapper = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: space-between; -`; - -const CenterText = styled(Text).attrs({ - noMargin: true -})<{ displayTheme?: DisplayTheme }>` - width: 245px; - text-align: center; - color: ${(props) => - props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; - font-weight: 500; - font-size: 11px; - line-height: 16px; - align-self: stretch; - flex: none; - flex-grow: 0; -`; - -const Link = styled.u` - cursor: pointer; -`; - -const CheckContainer = styled.div` - width: 245px; - display: flex; - flex-direction: row; - padding-left: 72px; - align-items: center; - isolation: isolate; - font-weight: 500; - font-size: 11px; - flex: none; - flex-grow: 0; - gap: 8px; -`; - -const CheckedSvg = styled.svg` - position: absolute; - left: calc(50% + 4px / 2 - 113px); - width: 18px; - height: 18px; - cursor: pointer; - flex: none; - flex-grow: 0; - background: #8e7bea; - border-radius: 2px; -`; - -const UncheckedSvg = styled.svg` - position: absolute; - left: calc(50% + 4px / 2 - 113px); - width: 18px; - height: 18px; - cursor: pointer; - flex: none; - flex-grow: 0; -`; - -const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>` - font-size: 18px; - font-weight: 500; - color: ${(props) => - props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; -`; diff --git a/src/routes/popup/send/announcement.tsx b/src/routes/popup/send/announcement.tsx index e5f77b6b..91f8484a 100644 --- a/src/routes/popup/send/announcement.tsx +++ b/src/routes/popup/send/announcement.tsx @@ -1,14 +1,13 @@ -import { - ButtonV2, - ModalV2, - Spacer, - Text, - type DisplayTheme -} from "@arconnect/components"; +import { ButtonV2, ModalV2, Spacer } from "@arconnect/components"; import { useRef } from "react"; import browser from "webextension-polyfill"; import aoLogo from "url:/assets/ecosystem/ao-token-logo.png"; -import styled from "styled-components"; +import { + HeaderText, + CenterText, + Content, + ContentWrapper +} from "~components/modals/Components"; export const AnnouncementPopup = ({ isOpen, setOpen }) => { const modalRef = useRef(null); @@ -60,48 +59,3 @@ export const AnnouncementPopup = ({ isOpen, setOpen }) => { ); }; - -const Content = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: none; - align-self: stretch; - flex-grow: 0; -`; - -const ContentWrapper = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: space-between; -`; - -const CenterText = styled(Text).attrs({ - noMargin: true -})<{ displayTheme?: DisplayTheme }>` - width: 245px; - text-align: center; - color: ${(props) => - props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; - font-weight: 500; - font-size: 11px; - line-height: 16px; - align-self: stretch; - flex: none; - flex-grow: 0; - - a { - color: rgb(${(props) => props.theme.theme}); - text-decoration: none; - } -`; - -const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>` - font-size: 18px; - font-weight: 500; - color: ${(props) => - props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"}; -`; diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx index 13cf9910..c278c522 100644 --- a/src/routes/welcome/load/wallets.tsx +++ b/src/routes/welcome/load/wallets.tsx @@ -24,7 +24,7 @@ import browser from "webextension-polyfill"; import styled from "styled-components"; import { addExpiration } from "~wallets/auth"; import { ArweaveSigner } from "arbundles"; -import { WalletKeySizeErrorModal } from "~components/WalletKeySizeErrorModal"; +import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; export default function Wallets() { // password context From 7c4b4e522d61a80a2ade5ce62a43fce09a7d986d Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 10 Jul 2024 10:38:21 -0700 Subject: [PATCH 07/33] feat: add screen 1 permission --- src/components/auth/App.tsx | 21 +++-- src/routes/auth/connect.tsx | 163 +++++++++++++++++++++++++++++------- 2 files changed, 150 insertions(+), 34 deletions(-) diff --git a/src/components/auth/App.tsx b/src/components/auth/App.tsx index 1facd10f..4008ef48 100644 --- a/src/components/auth/App.tsx +++ b/src/components/auth/App.tsx @@ -2,7 +2,8 @@ import { type DisplayTheme, Section, Spacer, - Text + Text, + ListItem } from "@arconnect/components"; import { defaultGateway, type Gateway } from "~gateways/gateway"; import { useTheme as useDisplayTheme } from "~utils/theme"; @@ -50,16 +51,24 @@ export default function App({ return ( <> - + {/* - - + */} + {/* */} - + + {/* {!appIcon && } @@ -91,7 +100,7 @@ export default function App({ {" AR"} )} - + */} ); diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index e513ee08..9b0ff2ca 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -32,6 +32,11 @@ import styled from "styled-components"; import { EventType, trackEvent } from "~utils/analytics"; import Application from "~applications/application"; import { defaultGateway, type Gateway } from "~gateways/gateway"; +import HeadV2 from "~components/popup/HeadV2"; +import { CheckIcon, CloseIcon } from "@iconicicons/react"; +import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; +import { defaultAllowance } from "~applications/allowance"; +import Arweave from "arweave"; export default function Connect() { // active address @@ -40,12 +45,16 @@ export default function Connect() { instance: ExtensionStorage }); + const arweave = new Arweave(defaultGateway); + // wallet switcher open const [switcherOpen, setSwitcherOpen] = useState(false); // page const [page, setPage] = useState<"unlock" | "permissions">("unlock"); + const allowanceInput = useInput(); + // connect params const params = useAuthParams<{ url: string; @@ -73,6 +82,9 @@ export default function Connect() { PermissionType[] >([]); + // allowance for permissions + const [allowanceEnabled, setAllowanceEnabled] = useState(true); + useEffect(() => { (async () => { if (!params) return; @@ -147,6 +159,14 @@ export default function Connect() { permissions, name: appData.name, logo: appData.logo, + allowance: { + enabled: allowanceEnabled, + limit: + allowanceEnabled && allowanceInput.state + ? arweave.ar.arToWinston(allowanceInput.state) + : defaultAllowance.limit, + spent: "0" // in winstons + }, // TODO: wayfinder gateway: params.gateway || defaultGateway }); @@ -169,15 +189,18 @@ export default function Connect() { closeWindow(); } + useEffect(() => { + allowanceInput.setState(arweave.ar.winstonToAr(defaultAllowance.limit)); + }, []); + return (
- - - + {page === "unlock" && ( @@ -233,36 +256,53 @@ export default function Connect() { {page === "permissions" && (
- - {browser.i18n.getMessage("allow_these_permissions")} - + + {/* {browser.i18n.getMessage("allow_these_permissions")} */} + Bazar wants to connect to your wallet with the following + permissions + + {params.url} + + + App Permissions + Edit Permissions + + + {arweave.ar.arToWinston(allowanceInput.state)} {requestedPermissions.map((permission, i) => ( -
- - setPermissions((val) => { - if (checked && val.includes(permission)) return val; - if (!checked && !val.includes(permission)) - return val; - if (checked && !val.includes(permission)) { - return [...val, permission]; - } - if (!checked && val.includes(permission)) { - return val.filter((p) => p !== permission); - } - }) - } - > + + + {browser.i18n.getMessage( permissionData[permission.toUpperCase()] )} - - {i !== requestedPermissions.length - 1 && ( - - )} -
+ + ))} + +
Allowance
+ +
+ {allowanceEnabled && ( + + AR} + type="number" + {...allowanceInput.bindings} + /> + + )}
)} @@ -295,6 +335,23 @@ const WalletSelectWrapper = styled.div` position: relative; `; +const Permissions = styled.div` + padding-bottom: 1rem; +`; + +const Permission = styled.div` + margin: 0; + align-items: center; + display: flex; + gap: 8px; +`; + +const PermissionsTitle = styled.div` + display: flex; + width: 100%; + justify-content: space-between; +`; + const SelectIcon = styled(ChevronDownIcon)` font-size: 1rem; width: 1.375rem; @@ -303,6 +360,56 @@ const SelectIcon = styled(ChevronDownIcon)` transition: all 0.23s ease-in-out; `; +const Description = styled(Text)<{ alt?: boolean }>` + color: ${(props) => + props.alt ? `rgb(${props.theme.theme})` : props.theme.primaryTextv2}; + margin-bottom: 4px; + ${(props) => + props.alt && + ` + cursor: pointer; + `} +`; +const Url = styled(Text)` + color: ${(props) => props.theme.secondaryTextv2}; + font-size: 12px; +`; + +const StyledCheckIcon = styled(CheckIcon)` + width: 17px; + height: 17px; + min-width: 17px; + min-height: 17px; + flex-shrink: 0; + color: rgba(20, 209, 16, 1); +`; + +const AllowanceInput = styled(InputV2)` + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } +`; + +const PermissionItem = styled(Text)` + color: ${(props) => props.theme.primaryTextv2}; + margin: 0; + font-size: 14px; +`; + +const AllowanceSection = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-end; + padding-top: 18px; + div { + color: ${(props) => props.theme.primaryTextv2}; + font-size: 18px; + font-weight: 00; + } +`; + const WalletSelect = styled(Card)<{ open: boolean }>` position: relative; display: flex; From 44d164379c77342a41a2d1f16d87c763c526975d Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 10 Jul 2024 12:42:54 -0700 Subject: [PATCH 08/33] fix: re-added title removed misc --- src/components/auth/App.tsx | 33 ++++++++++++++++++--------------- src/routes/auth/connect.tsx | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/auth/App.tsx b/src/components/auth/App.tsx index 4008ef48..fd123ef0 100644 --- a/src/components/auth/App.tsx +++ b/src/components/auth/App.tsx @@ -23,7 +23,8 @@ export default function App({ appName, appUrl, gateway, - allowance + allowance, + showTitle = true }: Props) { // allowance spent in AR const spent = useMemo(() => { @@ -51,14 +52,18 @@ export default function App({ return ( <> - {/* - - */} - {/* */} + {showTitle && ( + <> + + + + + + )} ` - background-color: rgb( - ${(props) => - props.displayTheme === "light" ? "0, 0, 0" : props.theme.cardBackground} - ); - border-radius: 27px; - padding: 1rem; + border-radius: 10px; + padding-top: 1rem; + padding-bottom: 1rem; display: flex; align-items: center; justify-content: space-between; @@ -173,4 +175,5 @@ interface Props { appUrl: string; gateway?: Gateway; allowance?: Allowance; + showTitle?: boolean; } diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index 9b0ff2ca..803fb84a 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -204,6 +204,7 @@ export default function Connect() { Edit Permissions - {arweave.ar.arToWinston(allowanceInput.state)} {requestedPermissions.map((permission, i) => ( From 1b3efe5887c530542471bfa3533fb0b206d39d48 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 10 Jul 2024 16:56:30 -0700 Subject: [PATCH 09/33] chore: converted permissions page to component --- src/components/auth/Permissions.tsx | 124 ++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/components/auth/Permissions.tsx diff --git a/src/components/auth/Permissions.tsx b/src/components/auth/Permissions.tsx new file mode 100644 index 00000000..37cdb484 --- /dev/null +++ b/src/components/auth/Permissions.tsx @@ -0,0 +1,124 @@ +import { Section, Text } from "@arconnect/components"; +import browser from "webextension-polyfill"; +import styled from "styled-components"; +import { permissionData, type PermissionType } from "~applications/permissions"; +import Checkbox from "~components/Checkbox"; +import { useEffect, useState } from "react"; + +type PermissionsProps = { + requestedPermissions: PermissionType[]; + update: (updatedPermissions: PermissionType[]) => void; +}; + +export default function Permissions({ + requestedPermissions, + update +}: PermissionsProps) { + const [permissions, setPermissions] = useState>( + new Map() + ); + + useEffect(() => { + setPermissions( + new Map(requestedPermissions.map((permission) => [permission, true])) + ); + }, []); + + return ( + +
+
+ {browser.i18n.getMessage("permissions")} + + {Object.keys(permissionData).map( + (permissionName: PermissionType, i) => { + let formattedPermissionName = permissionName + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" "); + + if (permissionName === "SIGNATURE") { + formattedPermissionName = "Sign Data"; + } + + return ( +
+ + { + const updated = new Map(permissions); + updated.set(permissionName, checked); + + setPermissions(updated); + const updatedPermissions = Array.from( + updated.entries() + ) + .filter(([, value]) => value) + .map(([key]) => key); + update(updatedPermissions); + }} + checked={requestedPermissions.includes(permissionName)} + /> +
+ + {formattedPermissionName} + + + {browser.i18n.getMessage( + permissionData[permissionName] + )} + +
+
+
+ ); + } + )} +
+
+
+
+ ); +} + +const Wrapper = styled.div` + display: flex; + width: 100vw; + flex-direction: column; + justify-content: space-between; +`; + +const Title = styled(Text).attrs({ + heading: true +})` + margin-bottom: 0.75em; + font-size: 1.125rem; +`; + +const PermissionsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; + +const Permission = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +export const PermissionDescription = styled(Text).attrs({ + noMargin: true +})` + font-size: 0.625rem; + font-weight: 500; +`; + +export const PermissionTitle = styled(Text).attrs({ + noMargin: true, + heading: true +})` + font-size: 0.875rem; + font-weight: 500; +`; From 989bb8b6851f726ae99ecf4fd1f4d104da08ee1b Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 10 Jul 2024 17:13:20 -0700 Subject: [PATCH 10/33] fix: changes made for Permissions component --- src/routes/auth/connect.tsx | 203 +++++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 70 deletions(-) diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index 803fb84a..49fef2d6 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -26,7 +26,6 @@ import WalletSwitcher from "~components/popup/WalletSwitcher"; import Wrapper from "~components/auth/Wrapper"; import browser from "webextension-polyfill"; import Label from "~components/auth/Label"; -import Head from "~components/popup/Head"; import App from "~components/auth/App"; import styled from "styled-components"; import { EventType, trackEvent } from "~utils/analytics"; @@ -37,6 +36,7 @@ import { CheckIcon, CloseIcon } from "@iconicicons/react"; import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; import { defaultAllowance } from "~applications/allowance"; import Arweave from "arweave"; +// import Permissions from "../../components/auth/Permissions"; export default function Connect() { // active address @@ -85,6 +85,9 @@ export default function Connect() { // allowance for permissions const [allowanceEnabled, setAllowanceEnabled] = useState(true); + // state management for edit + const [edit, setEdit] = useState(false); + useEffect(() => { (async () => { if (!params) return; @@ -105,9 +108,16 @@ export default function Connect() { setRequestedPermissions( requested.filter((p) => Object.keys(permissionData).includes(p)) ); + setRequetedPermCopy( + requested.filter((p) => Object.keys(permissionData).includes(p)) + ); })(); }, [params]); + const [requestedPermCopy, setRequetedPermCopy] = useState( + [] + ); + // permissions to add const [permissions, setPermissions] = useState([]); @@ -197,9 +207,9 @@ export default function Connect() {
setEdit(false) : cancel} /> )} {page === "permissions" && ( - -
- - {/* {browser.i18n.getMessage("allow_these_permissions")} */} - Bazar wants to connect to your wallet with the following - permissions - - {params.url} - - - App Permissions - Edit Permissions - - - {requestedPermissions.map((permission, i) => ( - - - - {browser.i18n.getMessage( - permissionData[permission.toUpperCase()] - )} - - - ))} - -
Allowance
- -
- {allowanceEnabled && ( - - AR} - type="number" - {...allowanceInput.bindings} - /> - - )} -
-
+ <> + {!edit ? ( + +
+ + {/* {browser.i18n.getMessage("allow_these_permissions")} */} + Bazar wants to connect to your wallet with the following + permissions + + {params.url} + + + App Permissions + { + setEdit(!edit); + }} + > + Edit Permissions + + + + {requestedPermissions.map((permission, i) => ( + + + + {browser.i18n.getMessage( + permissionData[permission.toUpperCase()] + )} + + + ))} + {requestedPermCopy + .filter( + (permission) => + !requestedPermissions.includes(permission) + ) + .map((permission, i) => ( + + + + {browser.i18n.getMessage( + permissionData[permission.toUpperCase()] + )} + + + ))} + + +
Allowance
+ +
+ {allowanceEnabled && ( + + AR} + type="number" + {...allowanceInput.bindings} + /> + + )} +
+
+ ) : ( + <> + {/* */} + + )} + )}
- { - if (page === "unlock") { - await unlock(); - } else { - await connect(); - } - }} - > - {browser.i18n.getMessage(page === "unlock" ? "sign_in" : "connect")} - - - - {browser.i18n.getMessage("cancel")} - + {edit ? ( + setEdit(false)}> + {browser.i18n.getMessage("save")} + + ) : ( + <> + { + if (page === "unlock") { + await unlock(); + } else { + await connect(); + } + }} + > + {browser.i18n.getMessage( + page === "unlock" ? "sign_in" : "connect" + )} + + + + {browser.i18n.getMessage("cancel")} + + + )}
); @@ -335,7 +389,7 @@ const WalletSelectWrapper = styled.div` position: relative; `; -const Permissions = styled.div` +const StyledPermissions = styled.div` padding-bottom: 1rem; `; @@ -384,6 +438,15 @@ const StyledCheckIcon = styled(CheckIcon)` color: rgba(20, 209, 16, 1); `; +const StyledCloseIcon = styled(CloseIcon)` + width: 17px; + height: 17px; + min-width: 17px; + min-height: 17px; + flex-shrink: 0; + color: ${(props) => props.theme.fail}; +`; + const AllowanceInput = styled(InputV2)` &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { From 9f03b905f3d165526fce89dd2a6e8ead8a3638e1 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 11 Jul 2024 09:53:43 +0545 Subject: [PATCH 11/33] refactor: Reuse getWalletKeyLength function --- .../dashboard/subsettings/AddWallet.tsx | 7 ++----- src/routes/welcome/load/wallets.tsx | 12 +++++++----- src/utils/analytics.ts | 15 +++++++++------ src/wallets/index.ts | 8 ++++++++ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/components/dashboard/subsettings/AddWallet.tsx b/src/components/dashboard/subsettings/AddWallet.tsx index 1a5e7948..d48e8292 100644 --- a/src/components/dashboard/subsettings/AddWallet.tsx +++ b/src/components/dashboard/subsettings/AddWallet.tsx @@ -4,7 +4,7 @@ import type { JWKInterface } from "arweave/web/lib/wallet"; import { checkPassword } from "~wallets/auth"; import { useEffect, useState } from "react"; import { useLocation } from "wouter"; -import { addWallet } from "~wallets"; +import { addWallet, getWalletKeyLength } from "~wallets"; import { Text, useInput, @@ -22,7 +22,6 @@ import * as bip39 from "bip39-web-crypto"; import Arweave from "arweave/web/common"; import styled from "styled-components"; import { defaultGateway } from "~gateways/gateway"; -import { ArweaveSigner } from "arbundles"; import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; export default function AddWallet() { @@ -138,9 +137,7 @@ export default function AddWallet() { ? await jwkFromMnemonic(providedWallet) : providedWallet; - const signer = new ArweaveSigner(jwk); - const expectedLength = signer.ownerLength; - const actualLength = signer.publicKey.byteLength; + const { actualLength, expectedLength } = await getWalletKeyLength(jwk); if (expectedLength !== actualLength) { walletModal.setOpen(true); finishUp(); diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx index c278c522..25e37d62 100644 --- a/src/routes/welcome/load/wallets.tsx +++ b/src/routes/welcome/load/wallets.tsx @@ -1,6 +1,11 @@ import { isValidMnemonic, jwkFromMnemonic } from "~wallets/generator"; import { ExtensionStorage, OLD_STORAGE_NAME } from "~utils/storage"; -import { addWallet, getWallets, setActiveWallet } from "~wallets"; +import { + addWallet, + getWalletKeyLength, + getWallets, + setActiveWallet +} from "~wallets"; import type { KeystoneAccount } from "~wallets/hardware/keystone"; import type { JWKInterface } from "arweave/web/lib/wallet"; import { useContext, useEffect, useMemo, useState } from "react"; @@ -23,7 +28,6 @@ import Paragraph from "~components/Paragraph"; import browser from "webextension-polyfill"; import styled from "styled-components"; import { addExpiration } from "~wallets/auth"; -import { ArweaveSigner } from "arbundles"; import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; export default function Wallets() { @@ -136,9 +140,7 @@ export default function Wallets() { ? await jwkFromMnemonic(loadedWallet) : loadedWallet; - const signer = new ArweaveSigner(jwk); - const expectedLength = signer.ownerLength; - const actualLength = signer.publicKey.byteLength; + const { expectedLength, actualLength } = await getWalletKeyLength(jwk); if (expectedLength !== actualLength) { walletModal.setOpen(true); finishUp(); diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 544b8451..ff368d77 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -1,7 +1,12 @@ import { getSetting } from "~settings"; import { ExtensionStorage, TempTransactionStorage } from "./storage"; import { AnalyticsBrowser } from "@segment/analytics-next"; -import { getWallets, getActiveKeyfile, getActiveAddress } from "~wallets"; +import { + getWallets, + getActiveKeyfile, + getActiveAddress, + getWalletKeyLength +} from "~wallets"; import Arweave from "arweave"; import { defaultGateway } from "~gateways/gateway"; import { v4 as uuid } from "uuid"; @@ -9,7 +14,6 @@ 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"; @@ -268,10 +272,9 @@ export const checkWalletBits = async (): Promise => { }); isLocalWallet(decryptedWallet); - const signer = new ArweaveSigner(decryptedWallet.keyfile); - const owner = signer.publicKey; - const expectedLength = signer.ownerLength; - const actualLength = owner.byteLength; + const { actualLength, expectedLength } = await getWalletKeyLength( + decryptedWallet.keyfile + ); const lengthsMatch = expectedLength === actualLength; diff --git a/src/wallets/index.ts b/src/wallets/index.ts index d0ec0454..abcd442c 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -20,6 +20,7 @@ import { getDecryptionKey, setDecryptionKey } from "./auth"; +import { ArweaveSigner } from "arbundles"; /** * Locally stored wallet @@ -395,3 +396,10 @@ export async function syncLabels(alarmInfo?: Alarms.Alarm) { })) ); } + +export async function getWalletKeyLength(jwk: JWKInterface) { + const signer = new ArweaveSigner(jwk); + const expectedLength = signer.ownerLength; + const actualLength = signer.publicKey.byteLength; + return { actualLength, expectedLength }; +} From 9d11e93bafaeff48daacb37e353c0f2dd59eb832 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 11 Jul 2024 11:09:10 +0545 Subject: [PATCH 12/33] refactor: Import from correct component --- src/routes/popup/passwordPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/popup/passwordPopup.tsx b/src/routes/popup/passwordPopup.tsx index e9648c10..0b0f0d76 100644 --- a/src/routes/popup/passwordPopup.tsx +++ b/src/routes/popup/passwordPopup.tsx @@ -12,7 +12,7 @@ import aoLogo from "url:/assets/ecosystem/ao-token-logo.png"; import styled from "styled-components"; import { CheckIcon, CloseIcon } from "@iconicicons/react"; import { ResetButton } from "~components/dashboard/Reset"; -import { Content, ContentWrapper } from "./announcement"; +import { Content, ContentWrapper } from "~components/modals/Components"; export const PasswordWarningModal = ({ open, From 663b2a9cf73ba6ed06b75e7cb7bc250bafab6f6a Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 10 Jul 2024 22:31:03 -0700 Subject: [PATCH 13/33] fix: enabled permissions component --- src/routes/auth/connect.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index 49fef2d6..6ae6a664 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -36,7 +36,7 @@ import { CheckIcon, CloseIcon } from "@iconicicons/react"; import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; import { defaultAllowance } from "~applications/allowance"; import Arweave from "arweave"; -// import Permissions from "../../components/auth/Permissions"; +import Permissions from "../../components/auth/Permissions"; export default function Connect() { // active address @@ -342,10 +342,10 @@ export default function Connect() { ) : ( <> - {/* */} + /> )} From d0deebc94adb52a273898ffb3bc8e9406f0fac9d Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 11 Jul 2024 14:50:00 +0545 Subject: [PATCH 14/33] feat(wallet): add validation for key length during wallet generation --- assets/_locales/en/messages.json | 12 +++ assets/_locales/zh_CN/messages.json | 12 +++ .../dashboard/subsettings/AddWallet.tsx | 17 +++- .../modals/WalletRetryCreationModal.tsx | 84 ++++++++++++++++++ src/routes/popup/passwordPopup.tsx | 2 +- src/routes/welcome/generate/backup.tsx | 33 +++++-- src/routes/welcome/generate/confirm.tsx | 2 +- src/routes/welcome/generate/done.tsx | 2 +- src/routes/welcome/setup.tsx | 85 +++++++++++-------- 9 files changed, 201 insertions(+), 48 deletions(-) create mode 100644 src/components/modals/WalletRetryCreationModal.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 257f87ea..e0e07e00 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -53,6 +53,10 @@ "message": "Close", "description": "Close button text" }, + "retry": { + "message": "Retry", + "description": "Retry button text" + }, "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" @@ -677,6 +681,14 @@ "message": "The key length of the wallet you are importing is less than the required length to transact on the AO network. It is recommended you create a new ArConnect wallet and transfer your assets to your new wallet.", "description": "Key Length Too Short error" }, + "generate_wallet_key_length_short_error": { + "message": "Error when generating wallet, click the retry button below to try again.", + "description": "Key Length Too Short error" + }, + "generate_wallet_in_progress": { + "message": "The wallet creation process can take 30-60 seconds.", + "description": "Generate wallet in progresss text" + }, "development_version": { "message": "Development", "description": "Development version badge text" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 6bcb11a2..15c7da26 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -53,6 +53,10 @@ "message": "关闭", "description": "Close button text" }, + "retry": { + "message": "重试", + "description": "Retry button text" + }, "add_wallet": { "message": "添加钱包", "description": "Add a wallet text" @@ -677,6 +681,14 @@ "message": "您导入的钱包的密钥长度小于在 AO 网络上进行交易所需的长度。建议您创建一个新的ArConnect钱包,并将您的资产转移到新钱包中。", "description": "Key Length Too Short error" }, + "generate_wallet_key_length_short_error": { + "message": "生成钱包时出错,请点击下方重试按钮重试。", + "description": "Key Length Too Short error" + }, + "generate_wallet_in_progress": { + "message": "钱包创建过程可能需要 30-60 秒。", + "description": "Generate wallet in progresss text" + }, "development_version": { "message": "开发版本", "description": "Development version badge text" diff --git a/src/components/dashboard/subsettings/AddWallet.tsx b/src/components/dashboard/subsettings/AddWallet.tsx index d48e8292..1e3f3639 100644 --- a/src/components/dashboard/subsettings/AddWallet.tsx +++ b/src/components/dashboard/subsettings/AddWallet.tsx @@ -23,6 +23,7 @@ import Arweave from "arweave/web/common"; import styled from "styled-components"; import { defaultGateway } from "~gateways/gateway"; import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; +import { WalletRetryCreationModal } from "~components/modals/WalletRetryCreationModal"; export default function AddWallet() { // password input @@ -31,6 +32,9 @@ export default function AddWallet() { // wallet size error modal const walletModal = useModal(); + // wallet retry modal + const walletRetryModal = useModal(); + // toasts const { setToast } = useToasts(); @@ -184,7 +188,7 @@ export default function AddWallet() { }, []); // generate new wallet - async function generateWallet() { + async function generateWallet(retry?: boolean) { setGenerating(true); // generate a seedphrase @@ -239,6 +243,13 @@ export default function AddWallet() { } try { + const { actualLength, expectedLength } = await getWalletKeyLength( + generatedWallet.jwk + ); + if (actualLength !== expectedLength) { + walletRetryModal.setOpen(true); + return; + } // add the wallet await addWallet(generatedWallet.jwk, passwordInput.state); @@ -339,6 +350,10 @@ export default function AddWallet() {
+
); } diff --git a/src/components/modals/WalletRetryCreationModal.tsx b/src/components/modals/WalletRetryCreationModal.tsx new file mode 100644 index 00000000..812c25bc --- /dev/null +++ b/src/components/modals/WalletRetryCreationModal.tsx @@ -0,0 +1,84 @@ +import { ButtonV2, ModalV2, Spacer } from "@arconnect/components"; +import { useRef, useState } from "react"; +import browser from "webextension-polyfill"; +import { ContentWrapper, Content, HeaderText, CenterText } from "./Components"; +import styled from "styled-components"; +import type { JWKInterface } from "arweave/web/lib/wallet"; +import { getWalletKeyLength } from "~wallets"; + +interface Props { + isOpen: boolean; + setOpen: (value: boolean) => void; + onRetry: ( + retry?: boolean + ) => Promise>; +} + +export const WalletRetryCreationModal = ({ + isOpen, + setOpen, + onRetry +}: Props) => { + const modalRef = useRef(null); + const [loading, setLoading] = useState(false); + + async function handleRetry() { + setLoading(true); + try { + const { jwk } = await onRetry(true); + const { actualLength, expectedLength } = await getWalletKeyLength(jwk); + if (actualLength === expectedLength) { + setOpen(false); + } + } catch {} + setLoading(false); + } + + return ( + } + > + + +
+ + {browser.i18n.getMessage( + "import_wallet_key_length_short_error_title" + )} + + + + {browser.i18n.getMessage( + "generate_wallet_key_length_short_error" + )} + + +
+
+ + + {browser.i18n.getMessage("retry")} + + setOpen(false)}> + {browser.i18n.getMessage("cancel")} + + + + {loading && ( + + {browser.i18n.getMessage("generate_wallet_in_progress")} + + )} +
+
+ ); +}; + +const ButtonsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; diff --git a/src/routes/popup/passwordPopup.tsx b/src/routes/popup/passwordPopup.tsx index e9648c10..e7e03562 100644 --- a/src/routes/popup/passwordPopup.tsx +++ b/src/routes/popup/passwordPopup.tsx @@ -12,7 +12,7 @@ import aoLogo from "url:/assets/ecosystem/ao-token-logo.png"; import styled from "styled-components"; import { CheckIcon, CloseIcon } from "@iconicicons/react"; import { ResetButton } from "~components/dashboard/Reset"; -import { Content, ContentWrapper } from "./announcement"; +import { ContentWrapper, Content } from "~components/modals/Components"; export const PasswordWarningModal = ({ open, diff --git a/src/routes/welcome/generate/backup.tsx b/src/routes/welcome/generate/backup.tsx index 846ea723..a74a9984 100644 --- a/src/routes/welcome/generate/backup.tsx +++ b/src/routes/welcome/generate/backup.tsx @@ -1,4 +1,4 @@ -import { ButtonV2, Spacer, Text } from "@arconnect/components"; +import { ButtonV2, Spacer, Text, useModal } from "@arconnect/components"; import { useLocation, useRoute } from "wouter"; import { useContext, useEffect, useState } from "react"; import { WalletContext } from "../setup"; @@ -14,13 +14,18 @@ import { EyeOffIcon } from "@iconicicons/react"; import { PageType, trackPage } from "~utils/analytics"; +import { getWalletKeyLength } from "~wallets"; +import { WalletRetryCreationModal } from "~components/modals/WalletRetryCreationModal"; export default function Backup() { // seed blur status const [shown, setShown] = useState(false); + // wallet retry modal + const walletRetryModal = useModal(); + // wallet context - const generatedWallet = useContext(WalletContext); + const { wallet: generatedWallet, generateWallet } = useContext(WalletContext); // route const [, params] = useRoute<{ setup: string; page: string }>("/:setup/:page"); @@ -36,6 +41,19 @@ export default function Backup() { setTimeout(() => setCopyDisplay(true), 1050); } + async function handleNext() { + if (generatedWallet.jwk) { + const { actualLength, expectedLength } = await getWalletKeyLength( + generatedWallet.jwk + ); + if (expectedLength !== actualLength) { + walletRetryModal.setOpen(true); + } else { + setLocation(`/${params.setup}/${Number(params.page) + 1}`); + } + } + } + // Segment useEffect(() => { trackPage(PageType.ONBOARD_BACKUP); @@ -55,15 +73,14 @@ export default function Backup() { {browser.i18n.getMessage("copySeed")} - - setLocation(`/${params.setup}/${Number(params.page) + 1}`) - } - > + {browser.i18n.getMessage("next")} + ); } diff --git a/src/routes/welcome/generate/confirm.tsx b/src/routes/welcome/generate/confirm.tsx index 96a4d46a..6e243010 100644 --- a/src/routes/welcome/generate/confirm.tsx +++ b/src/routes/welcome/generate/confirm.tsx @@ -10,7 +10,7 @@ import { PageType, trackPage } from "~utils/analytics"; export default function Confirm() { // wallet context - const generatedWallet = useContext(WalletContext); + const { wallet: generatedWallet } = useContext(WalletContext); // toasts const { setToast } = useToasts(); diff --git a/src/routes/welcome/generate/done.tsx b/src/routes/welcome/generate/done.tsx index c9bebde0..8903811e 100644 --- a/src/routes/welcome/generate/done.tsx +++ b/src/routes/welcome/generate/done.tsx @@ -22,7 +22,7 @@ import { addExpiration } from "~wallets/auth"; export default function Done() { // wallet context - const wallet = useContext(WalletContext); + const { wallet } = useContext(WalletContext); const [, setLocation] = useLocation(); diff --git a/src/routes/welcome/setup.tsx b/src/routes/welcome/setup.tsx index 63dc56eb..dfb4edce 100644 --- a/src/routes/welcome/setup.tsx +++ b/src/routes/welcome/setup.tsx @@ -92,54 +92,57 @@ export default function Setup({ setupMode, page }: Props) { const { setToast } = useToasts(); // generate wallet in the background - const [generatedWallet, setGeneratedWallet] = useState( - {} - ); + const [generatedWallet, setGeneratedWallet] = useState({}); const navigate = () => { setLocation(`/${params.setup}/${page - 1}`); }; - useEffect(() => { - (async () => { - // only generate wallet if the - // setup mode is wallet generation - if (!isGenerateWallet || generatedWallet.address) return; + async function generateWallet(retry?: boolean) { + // only generate wallet if the + // setup mode is wallet generation + if ((!isGenerateWallet || generatedWallet.address) && !retry) return; + + // prevent user from closing the window + // while ArConnect is generating a wallet + window.onbeforeunload = () => + browser.i18n.getMessage("close_tab_generate_wallet_message"); - // prevent user from closing the window - // while ArConnect is generating a wallet - window.onbeforeunload = () => - browser.i18n.getMessage("close_tab_generate_wallet_message"); + try { + const arweave = new Arweave(defaultGateway); - try { - const arweave = new Arweave(defaultGateway); + // generate seed + const seed = await bip39.generateMnemonic(); - // generate seed - const seed = await bip39.generateMnemonic(); + setGeneratedWallet({ mnemonic: seed }); - setGeneratedWallet({ mnemonic: seed }); + // generate wallet from seedphrase + const generatedKeyfile = await jwkFromMnemonic(seed); - // generate wallet from seedphrase - const generatedKeyfile = await jwkFromMnemonic(seed); + setGeneratedWallet((val) => ({ ...val, jwk: generatedKeyfile })); - setGeneratedWallet((val) => ({ ...val, jwk: generatedKeyfile })); + // get address + const address = await arweave.wallets.jwkToAddress(generatedKeyfile); - // get address - const address = await arweave.wallets.jwkToAddress(generatedKeyfile); + setGeneratedWallet((val) => ({ ...val, address })); + + return generatedWallet; + } catch (e) { + console.log("Error generating wallet", e); + setToast({ + type: "error", + content: browser.i18n.getMessage("error_generating_wallet"), + duration: 2300 + }); + } - setGeneratedWallet((val) => ({ ...val, address })); - } catch (e) { - console.log("Error generating wallet", e); - setToast({ - type: "error", - content: browser.i18n.getMessage("error_generating_wallet"), - duration: 2300 - }); - } + // reset before unload + window.onbeforeunload = null; + return {}; + } - // reset before unload - window.onbeforeunload = null; - })(); + useEffect(() => { + generateWallet(); }, [isGenerateWallet]); // animate content sice @@ -189,7 +192,9 @@ export default function Setup({ setupMode, page }: Props) { - + @@ -290,9 +295,17 @@ export const PasswordContext = createContext({ password: "" }); -export const WalletContext = createContext({}); +export const WalletContext = createContext({ + wallet: {}, + generateWallet: (retry?: boolean) => Promise.resolve({}) +}); interface WalletContextValue { + wallet: GeneratedWallet; + generateWallet: (retry?: boolean) => Promise; +} + +interface GeneratedWallet { address?: string; mnemonic?: string; jwk?: JWKInterface; From 781325dfa32d751cd262c74789df1e7d5987f8fb Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 11 Jul 2024 16:21:42 +0545 Subject: [PATCH 15/33] fix: Update matches logic for password --- src/routes/welcome/load/password.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/routes/welcome/load/password.tsx b/src/routes/welcome/load/password.tsx index e961db78..16e6b9bb 100644 --- a/src/routes/welcome/load/password.tsx +++ b/src/routes/welcome/load/password.tsx @@ -70,16 +70,12 @@ export default function Password() { setLocation(`/${params.setup}/${Number(params.page) + 1}`); } - // password valid - const validPassword = useMemo( - () => checkPasswordValid(passwordInput.state), - [passwordInput] - ); - // passwords match const matches = useMemo( - () => passwordInput.state === validPasswordInput.state && validPassword, - [passwordInput, validPasswordInput, validPassword] + () => + passwordInput.state === validPasswordInput.state && + passwordInput.state?.length >= 5, + [passwordInput, validPasswordInput] ); // Segment From 6ed2cc83602a6227f1a22cac8f89bc09e4196d6f Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 11 Jul 2024 18:00:04 +0545 Subject: [PATCH 16/33] refactor: Add loading spinner to Next button in backup page --- assets/_locales/en/messages.json | 6 +++- assets/_locales/zh_CN/messages.json | 6 +++- .../modals/WalletRetryCreationModal.tsx | 3 +- src/routes/popup/passwordPopup.tsx | 2 +- src/routes/welcome/generate/backup.tsx | 36 ++++++++++++++++--- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index e0e07e00..56467ae7 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -681,8 +681,12 @@ "message": "The key length of the wallet you are importing is less than the required length to transact on the AO network. It is recommended you create a new ArConnect wallet and transfer your assets to your new wallet.", "description": "Key Length Too Short error" }, + "generate_wallet_key_length_short_error_title": { + "message": "Error generating wallet", + "description": "Key Length Too Short error title" + }, "generate_wallet_key_length_short_error": { - "message": "Error when generating wallet, click the retry button below to try again.", + "message": "Click the retry button below to try again.", "description": "Key Length Too Short error" }, "generate_wallet_in_progress": { diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 15c7da26..85f919f7 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -681,8 +681,12 @@ "message": "您导入的钱包的密钥长度小于在 AO 网络上进行交易所需的长度。建议您创建一个新的ArConnect钱包,并将您的资产转移到新钱包中。", "description": "Key Length Too Short error" }, + "generate_wallet_key_length_short_error_title": { + "message": "生成钱包时出错", + "description": "Key Length Too Short error title" + }, "generate_wallet_key_length_short_error": { - "message": "生成钱包时出错,请点击下方重试按钮重试。", + "message": "单击下面的重试按钮再试一次。", "description": "Key Length Too Short error" }, "generate_wallet_in_progress": { diff --git a/src/components/modals/WalletRetryCreationModal.tsx b/src/components/modals/WalletRetryCreationModal.tsx index 812c25bc..8f58320a 100644 --- a/src/components/modals/WalletRetryCreationModal.tsx +++ b/src/components/modals/WalletRetryCreationModal.tsx @@ -23,6 +23,7 @@ export const WalletRetryCreationModal = ({ const [loading, setLoading] = useState(false); async function handleRetry() { + if (loading) return; setLoading(true); try { const { jwk } = await onRetry(true); @@ -46,7 +47,7 @@ export const WalletRetryCreationModal = ({
{browser.i18n.getMessage( - "import_wallet_key_length_short_error_title" + "generate_wallet_key_length_short_error_title" )} diff --git a/src/routes/popup/passwordPopup.tsx b/src/routes/popup/passwordPopup.tsx index e7e03562..0b0f0d76 100644 --- a/src/routes/popup/passwordPopup.tsx +++ b/src/routes/popup/passwordPopup.tsx @@ -12,7 +12,7 @@ import aoLogo from "url:/assets/ecosystem/ao-token-logo.png"; import styled from "styled-components"; import { CheckIcon, CloseIcon } from "@iconicicons/react"; import { ResetButton } from "~components/dashboard/Reset"; -import { ContentWrapper, Content } from "~components/modals/Components"; +import { Content, ContentWrapper } from "~components/modals/Components"; export const PasswordWarningModal = ({ open, diff --git a/src/routes/welcome/generate/backup.tsx b/src/routes/welcome/generate/backup.tsx index a74a9984..366ccda7 100644 --- a/src/routes/welcome/generate/backup.tsx +++ b/src/routes/welcome/generate/backup.tsx @@ -1,6 +1,6 @@ import { ButtonV2, Spacer, Text, useModal } from "@arconnect/components"; import { useLocation, useRoute } from "wouter"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { WalletContext } from "../setup"; import Paragraph from "~components/Paragraph"; import browser from "webextension-polyfill"; @@ -21,12 +21,18 @@ export default function Backup() { // seed blur status const [shown, setShown] = useState(false); + // loading + const [loading, setLoading] = useState(false); + // wallet retry modal const walletRetryModal = useModal(); // wallet context const { wallet: generatedWallet, generateWallet } = useContext(WalletContext); + // ref to track the latest generated wallet + const walletRef = useRef(generatedWallet); + // route const [, params] = useRoute<{ setup: string; page: string }>("/:setup/:page"); const [, setLocation] = useLocation(); @@ -42,18 +48,38 @@ export default function Backup() { } async function handleNext() { - if (generatedWallet.jwk) { + if (loading) return; + setLoading(true); + + try { + if (!walletRef.current.jwk) { + await new Promise((resolve) => { + const checkState = setInterval(() => { + if (walletRef.current.jwk) { + clearInterval(checkState); + resolve(null); + } + }, 500); + }); + } + const { actualLength, expectedLength } = await getWalletKeyLength( - generatedWallet.jwk + walletRef.current.jwk ); if (expectedLength !== actualLength) { walletRetryModal.setOpen(true); } else { setLocation(`/${params.setup}/${Number(params.page) + 1}`); } - } + } catch {} + + setLoading(false); } + useEffect(() => { + walletRef.current = generatedWallet; + }, [generatedWallet]); + // Segment useEffect(() => { trackPage(PageType.ONBOARD_BACKUP); @@ -73,7 +99,7 @@ export default function Backup() { {browser.i18n.getMessage("copySeed")} - + {browser.i18n.getMessage("next")} From ec6ed15abc2dd6445b4ce371d009b4ddb5aab1ec Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Thu, 11 Jul 2024 22:44:45 +0545 Subject: [PATCH 17/33] refactor: Retry generating wallet if imported from seedphrase on bits mismatch --- .../dashboard/subsettings/AddWallet.tsx | 17 ++++++++++++----- src/routes/welcome/load/wallets.tsx | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/dashboard/subsettings/AddWallet.tsx b/src/components/dashboard/subsettings/AddWallet.tsx index 1e3f3639..00f868f0 100644 --- a/src/components/dashboard/subsettings/AddWallet.tsx +++ b/src/components/dashboard/subsettings/AddWallet.tsx @@ -136,16 +136,23 @@ export default function AddWallet() { try { // load jwk from seedphrase input state - const jwk = + let jwk = typeof providedWallet === "string" ? await jwkFromMnemonic(providedWallet) : providedWallet; - const { actualLength, expectedLength } = await getWalletKeyLength(jwk); + let { actualLength, expectedLength } = await getWalletKeyLength(jwk); if (expectedLength !== actualLength) { - walletModal.setOpen(true); - finishUp(); - return; + if (typeof providedWallet !== "string") { + walletModal.setOpen(true); + finishUp(); + return; + } else { + while (expectedLength !== actualLength) { + jwk = await jwkFromMnemonic(providedWallet); + ({ actualLength, expectedLength } = await getWalletKeyLength(jwk)); + } + } } await addWallet(jwk, passwordInput.state); diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx index 25e37d62..c585ccdd 100644 --- a/src/routes/welcome/load/wallets.tsx +++ b/src/routes/welcome/load/wallets.tsx @@ -135,16 +135,25 @@ export default function Wallets() { if (loadedWallet) { // load jwk from seedphrase input state - const jwk = + let jwk = typeof loadedWallet === "string" ? await jwkFromMnemonic(loadedWallet) : loadedWallet; - const { expectedLength, actualLength } = await getWalletKeyLength(jwk); + let { actualLength, expectedLength } = await getWalletKeyLength(jwk); if (expectedLength !== actualLength) { - walletModal.setOpen(true); - finishUp(); - return; + if (typeof loadedWallet !== "string") { + walletModal.setOpen(true); + finishUp(); + return; + } else { + while (expectedLength !== actualLength) { + jwk = await jwkFromMnemonic(loadedWallet); + ({ actualLength, expectedLength } = await getWalletKeyLength( + jwk + )); + } + } } // add wallet From ad72c2c8c102a1b8366fe1d68048c86468c36b0d Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 11 Jul 2024 10:43:49 -0700 Subject: [PATCH 18/33] fix: removed placeholder misc fixes --- assets/_locales/en/messages.json | 26 ++++++++++++++-- assets/_locales/zh_CN/messages.json | 26 ++++++++++++++-- src/components/auth/Permissions.tsx | 28 +++++++++++------ src/routes/auth/connect.tsx | 48 ++++++++++++++++++----------- 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 4e6671af..bd164993 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -303,6 +303,10 @@ "message": "Edit permissions", "description": "Edit permissions title" }, + "app_permissions": { + "message": "App permissions", + "description": "App permissions title" + }, "allowance": { "message": "Allowance", "description": "Allowance title" @@ -937,9 +941,27 @@ "message": "Connect", "description": "Connect button text" }, + "always_allow": { + "message": "Always allow", + "description": "Always allow button" + }, + "always_ask_permission": { + "message": "Always ask permission", + "description": "Always allow button" + }, + "allow_selected_permissions": { + "message": "Allow selected permissions", + "description": "Always allow button" + }, "allow_these_permissions": { - "message": "Allow these permissions for the application:", - "description": "Auth permissions label" + "message": "$APPNAME$ wants to connect to your wallet with the following permissions", + "description": "Auth permissions label", + "placeholders": { + "appname": { + "content": "$1", + "example": "permafacts.arweave.dev" + } + } }, "reset_allowance": { "message": "Reset allowance", diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 9e9e6870..14b12802 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -303,6 +303,10 @@ "message": "编辑权限", "description": "Edit permissions title" }, + "app_permissions": { + "message": "应用权限", + "description": "App permissions title" + }, "allowance": { "message": "额度", "description": "Allowance title" @@ -933,9 +937,27 @@ "message": "连接", "description": "Connect button text" }, + "always_allow": { + "message": "始终允许", + "description": "Always allow button" + }, + "always_ask_permission": { + "message": "始终询问权限", + "description": "Always allow button" + }, + "allow_selected_permissions": { + "message": "允许选择的权限", + "description": "Always allow button" + }, "allow_these_permissions": { - "message": "允许这些权限给应用程序:", - "description": "Auth permissions label" + "message": "$APPNAME$ 想要使用以下权限连接到您的钱包", + "description": "Auth permissions label", + "placeholders": { + "appname": { + "content": "$1", + "example": "permafacts.arweave.dev" + } + } }, "reset_allowance": { "message": "重置额度", diff --git a/src/components/auth/Permissions.tsx b/src/components/auth/Permissions.tsx index 37cdb484..ce640e4f 100644 --- a/src/components/auth/Permissions.tsx +++ b/src/components/auth/Permissions.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from "@arconnect/components"; +import { ButtonV2, Section, Text } from "@arconnect/components"; import browser from "webextension-polyfill"; import styled from "styled-components"; import { permissionData, type PermissionType } from "~applications/permissions"; @@ -8,11 +8,13 @@ import { useEffect, useState } from "react"; type PermissionsProps = { requestedPermissions: PermissionType[]; update: (updatedPermissions: PermissionType[]) => void; + closeEdit: (setEdit: boolean) => void; }; export default function Permissions({ requestedPermissions, - update + update, + closeEdit }: PermissionsProps) { const [permissions, setPermissions] = useState>( new Map() @@ -49,14 +51,7 @@ export default function Permissions({ onChange={(checked) => { const updated = new Map(permissions); updated.set(permissionName, checked); - setPermissions(updated); - const updatedPermissions = Array.from( - updated.entries() - ) - .filter(([, value]) => value) - .map(([key]) => key); - update(updatedPermissions); }} checked={requestedPermissions.includes(permissionName)} /> @@ -78,6 +73,20 @@ export default function Permissions({
+
+ { + const updatedPermissions = Array.from(permissions.entries()) + .filter(([, value]) => value) + .map(([key]) => key); + update(updatedPermissions); + closeEdit(false); + }} + > + {browser.i18n.getMessage("save")} + +
); } @@ -86,6 +95,7 @@ const Wrapper = styled.div` display: flex; width: 100vw; flex-direction: column; + height: calc(100vh - 163px); justify-content: space-between; `; diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index 6ae6a664..1ee48614 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -203,6 +203,12 @@ export default function Connect() { allowanceInput.setState(arweave.ar.winstonToAr(defaultAllowance.limit)); }, []); + const removedPermissions = useMemo(() => { + return requestedPermCopy.filter( + (permission) => !requestedPermissions.includes(permission) + ); + }, [requestedPermCopy, requestedPermissions]); + return (
@@ -270,21 +276,24 @@ export default function Connect() {
- {/* {browser.i18n.getMessage("allow_these_permissions")} */} - Bazar wants to connect to your wallet with the following - permissions + {browser.i18n.getMessage( + "allow_these_permissions", + appData.name || appUrl + )} {params.url} - App Permissions + + {browser.i18n.getMessage("app_permissions")} + { setEdit(!edit); }} > - Edit Permissions + {browser.i18n.getMessage("edit_permissions")} @@ -315,7 +324,7 @@ export default function Connect() { ))} -
Allowance
+
{browser.i18n.getMessage("allowance")}
AR} @@ -343,8 +352,9 @@ export default function Connect() { ) : ( <> )} @@ -353,12 +363,8 @@ export default function Connect() {
-
- {edit ? ( - setEdit(false)}> - {browser.i18n.getMessage("save")} - - ) : ( + {!edit && ( +
<> {browser.i18n.getMessage( - page === "unlock" ? "sign_in" : "connect" + page === "unlock" + ? "sign_in" + : removedPermissions.length > 0 + ? "allow_selected_permissions" + : "always_allow" )} - {browser.i18n.getMessage("cancel")} + {browser.i18n.getMessage( + page === "unlock" ? "cancel" : "always_ask_permission" + )} - )} -
+
+ )}
); } From d10b56cb619adaa626ec4c4fcefebc0f8fcd60b0 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 11 Jul 2024 10:48:46 -0700 Subject: [PATCH 19/33] chore: removed unused route --- src/routes/auth/permissions.tsx | 169 -------------------------------- 1 file changed, 169 deletions(-) delete mode 100644 src/routes/auth/permissions.tsx diff --git a/src/routes/auth/permissions.tsx b/src/routes/auth/permissions.tsx deleted file mode 100644 index 00b30da4..00000000 --- a/src/routes/auth/permissions.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { ButtonV2, Section, Text } from "@arconnect/components"; -import browser from "webextension-polyfill"; -import styled from "styled-components"; -import { permissionData, type PermissionType } from "~applications/permissions"; -import Checkbox from "~components/Checkbox"; -import { useEffect, useMemo, useState } from "react"; -import { defaultGateway, type Gateway } from "~gateways/gateway"; -import type { AppInfo } from "~applications/application"; -import { useAuthParams } from "~utils/auth"; -import HeadV2 from "~components/popup/HeadV2"; -import App from "~components/auth/App"; - -export default function Permissions() { - const [permissions, setPermissions] = useState>( - new Map() - ); - - // connect params - const params = useAuthParams<{ - url: string; - permissions: PermissionType[]; - appInfo: AppInfo; - gateway?: Gateway; - }>(); - - // app data - const appData = useMemo(() => { - if (!params) return {}; - - return params.appInfo; - }, [params]); - - // app url - const appUrl = useMemo(() => { - if (!params) return ""; - - return params.url; - }, [params]); - - useEffect(() => { - if (!params) return; - - setPermissions( - new Map(params.permissions.map((permission) => [permission, true])) - ); - }, [params]); - - function onSave() { - const updatedPermissions = Array.from(permissions.entries()) - .filter(([, value]) => value) - .map(([key]) => key); - - const updatedParams = { - ...params, - permissions: updatedPermissions, - type: "connect" - }; - - // TODO: Code to go back - } - - return ( - -
- - -
- {browser.i18n.getMessage("permissions")} - - {Object.keys(permissionData).map( - (permissionName: PermissionType, i) => { - let formattedPermissionName = permissionName - .split("_") - .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) - .join(" "); - - if (permissionName === "SIGNATURE") { - formattedPermissionName = "Sign Data"; - } - - return ( -
- - { - setPermissions((prevPermissions) => { - const newPermissions = new Map(prevPermissions); - newPermissions.set(permissionName, checked); - return newPermissions; - }); - }} - checked={!!permissions.get(permissionName)} - /> -
- - {formattedPermissionName} - - - {browser.i18n.getMessage( - permissionData[permissionName] - )} - -
-
-
- ); - } - )} -
-
-
-
- - {browser.i18n.getMessage("save")} - -
-
- ); -} - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: space-between; - min-height: 100vh; -`; - -const Title = styled(Text).attrs({ - heading: true -})` - margin-bottom: 0.75em; - font-size: 1.125rem; -`; - -const PermissionsWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10px; -`; - -const Permission = styled.div` - display: flex; - align-items: center; - gap: 8px; -`; - -export const PermissionDescription = styled(Text).attrs({ - noMargin: true -})` - font-size: 0.625rem; -`; - -export const PermissionTitle = styled(Text).attrs({ - noMargin: true, - heading: true -})` - font-size: 0.875rem; -`; From 52bf07e97dffa35f697cce737f38bbcd46b63b23 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 11 Jul 2024 14:07:15 -0700 Subject: [PATCH 20/33] fix: removed excess messaging --- assets/_locales/en/messages.json | 6 +++--- assets/_locales/zh_CN/messages.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index ee264c1f..1a2d6798 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -260,7 +260,7 @@ "description": "Description for the \"ACCESS_ALL_ADDRESSES\" permission" }, "permissionDescriptionSign": { - "message": "Allows the signing of a transaction without a review and confirmation", + "message": "Allows the signing of a transaction.", "description": "Description for the \"SIGN_TRANSACTION\" permission" }, "permissionDescriptionEncrypt": { @@ -272,7 +272,7 @@ "description": "Description for the \"DECRYPT\" permission" }, "permissionDescriptionSignature": { - "message": "Allows the signing of data without a review and confirmation", + "message": "Allows the signing of data.", "description": "Description for the \"SIGNATURE\" permission" }, "permissionDescriptionArweaveConfig": { @@ -280,7 +280,7 @@ "description": "Description for the \"ACCESS_ARWEAVE_CONFIG\" permission" }, "permissionDescriptionDispatch": { - "message": "Allows using dispatch transactions without a review and confirmation.", + "message": "Allows using dispatch transactions.", "description": "Description for the \"DISPATCH\" permission" }, "copyId": { diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index d4ae3f57..a6988cad 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -260,7 +260,7 @@ "description": "Description for the \"ACCESS_ALL_ADDRESSES\" permission" }, "permissionDescriptionSign": { - "message": "允许在没有审核和确认的情况下签署交易", + "message": "允许签署交易。", "description": "Description for the \"SIGN_TRANSACTION\" permission" }, "permissionDescriptionEncrypt": { @@ -272,7 +272,7 @@ "description": "Description for the \"DECRYPT\" permission" }, "permissionDescriptionSignature": { - "message": "允许在没有审核和确认的情况下签署数据", + "message": "允许签署数据。", "description": "Description for the \"SIGNATURE\" permission" }, "permissionDescriptionArweaveConfig": { @@ -280,7 +280,7 @@ "description": "Description for the \"ACCESS_ARWEAVE_CONFIG\" permission" }, "permissionDescriptionDispatch": { - "message": "允许在没有审核和确认的情况下使用派发交易", + "message": "允许使用调度交易。", "description": "Description for the \"DISPATCH\" permission" }, "copyId": { From c1f02c492b158105cd3462b298bb57cac8ace2f5 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 11 Jul 2024 17:59:16 -0700 Subject: [PATCH 21/33] chore: added error messaging, updated mismatch analytics --- assets/_locales/en/messages.json | 8 +++++ assets/_locales/zh_CN/messages.json | 8 +++++ src/routes/auth/signDataItem.tsx | 53 ++++++++++++++++++++++++++++- src/routes/popup/index.tsx | 6 ---- src/utils/analytics.ts | 25 +++++++++++--- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 7e7e65de..cdd8c057 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -2109,5 +2109,13 @@ "incorrect_password_error_message": { "message": "Password incorrect. Please enter the password to your ArConnect and try again. Not the password to the wallet you are importing.", "description": "Error message if password is incorrect" + }, + "mismatch_warning_title": { + "message": "Wallet Mismatch Detected.", + "description": "Title for wallet mismatch warning on sda" + }, + "mismatch_warning": { + "message": "Your wallet has a mismatched bit length. You can proceed, but may encounter errors. Transactions could fail or behave unexpectedly.", + "description": "warning for mismatch wallet" } } diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 7851219e..3a647e61 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -2103,5 +2103,13 @@ "incorrect_password_error_message": { "message": "密码错误。请输入 ArConnect 的密码后再试,而不是您正在导入的钱包密码。", "description": "Error message if password is incorrect" + }, + "mismatch_warning_title": { + "message": "检测到钱包不匹配。", + "description": "Title for wallet mismatch warning on sda" + }, + "mismatch_warning": { + "message": "您的钱包存在不匹配的位长度。您可以继续操作,但可能会遇到错误。交易可能会失败或表现异常。", + "description": "warning for mismatch wallet" } } diff --git a/src/routes/auth/signDataItem.tsx b/src/routes/auth/signDataItem.tsx index 1bb21e68..9c4e5235 100644 --- a/src/routes/auth/signDataItem.tsx +++ b/src/routes/auth/signDataItem.tsx @@ -36,10 +36,17 @@ import { getPrice } from "~lib/coingecko"; 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"; +import { LogoWrapper, Logo, WarningIcon } from "~components/popup/Token"; import arLogoLight from "url:/assets/ar/logo_light.png"; import arLogoDark from "url:/assets/ar/logo_dark.png"; import { useTheme } from "~utils/theme"; +import { + checkWalletBits, + EventType, + trackEvent, + type WalletBitsCheck +} from "~utils/analytics"; +import { Degraded, WarningWrapper } from "~routes/popup/send"; interface Tag { name: string; @@ -65,6 +72,7 @@ export default function SignDataItem() { const [logo, setLogo] = useState(""); const [amount, setAmount] = useState(null); const [showTags, setShowTags] = useState(false); + const [mismatch, setMismatch] = useState(false); const { setToast } = useToasts(); const recipient = @@ -236,6 +244,34 @@ export default function SignDataItem() { } }, [tokenName, logo, theme]); + // check for if bits check exists, if it does, check mismatch + useEffect(() => { + const walletCheck = async () => { + if (!activeAddress) { + setMismatch(false); + return; + } + try { + const storageKey = `bits_check_${activeAddress}`; + const storedCheck = await ExtensionStorage.get< + WalletBitsCheck | boolean + >(storageKey); + + if (typeof storedCheck !== "object") { + const bits = await checkWalletBits(); + if (bits !== null) { + setMismatch(bits); + } + } else { + setMismatch(storedCheck.mismatch); + } + } catch (error) { + console.error("Error checking wallet bits:", error); + } + }; + walletCheck(); + }, [activeAddress]); + return (
@@ -244,6 +280,21 @@ export default function SignDataItem() { showOptions={false} back={cancel} /> + {mismatch && transfer && ( + + + {" "} + +
+

{browser.i18n.getMessage("mismatch_warning_title")}

+ +

{browser.i18n.getMessage("mismatch_warning")}

+ + {/* Read more */} +
+
+
+ )} {browser.i18n.getMessage( diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx index d9a6cd89..b213fb0e 100644 --- a/src/routes/popup/index.tsx +++ b/src/routes/popup/index.tsx @@ -108,12 +108,6 @@ export default function Home() { useEffect(() => { const checkBits = async () => { const bits = await checkWalletBits(); - - if (bits === null) { - return; - } else { - await trackEvent(EventType.BITS_LENGTH, { mismatch: bits }); - } }; checkBits(); diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 544b8451..05df52dc 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -234,6 +234,11 @@ const setToStartOfNextMonth = (currentDate: Date): Date => { return newDate; }; +export interface WalletBitsCheck { + checked: boolean; + mismatch: boolean; +} + /** * Checks the bit length the active Arweave wallet. * @@ -254,11 +259,16 @@ export const checkWalletBits = async (): Promise => { if (!activeAddress) { return null; } - const hasBeenTracked = await ExtensionStorage.get( - `bits_check_${activeAddress}` + + const storageKey = `bits_check_${activeAddress}`; + + const hasBeenTracked = await ExtensionStorage.get( + storageKey ); - if (hasBeenTracked) { + if (typeof hasBeenTracked === "boolean") { + await ExtensionStorage.remove(storageKey); + } else if (hasBeenTracked && hasBeenTracked.checked) { return null; } @@ -273,11 +283,16 @@ export const checkWalletBits = async (): Promise => { const expectedLength = signer.ownerLength; const actualLength = owner.byteLength; + freeDecryptedWallet(decryptedWallet.keyfile); + const lengthsMatch = expectedLength === actualLength; - freeDecryptedWallet(decryptedWallet.keyfile); + await ExtensionStorage.set(`bits_check_${activeAddress}`, { + checked: true, + mismatch: !lengthsMatch + }); - await ExtensionStorage.set(`bits_check_${activeAddress}`, true); + await trackEvent(EventType.BITS_LENGTH, { mismatch: !lengthsMatch }); return !lengthsMatch; } catch (error) { From 1c119d84884e30b26362518bda8e73ffd2e2bbb0 Mon Sep 17 00:00:00 2001 From: soralit Date: Thu, 27 Jun 2024 18:07:14 +0800 Subject: [PATCH 22/33] feat(keystone): support ao signing --- src/routes/popup/send/confirm.tsx | 67 +++++++++++++++++++++++++++++-- src/routes/popup/send/index.tsx | 30 ++++---------- src/tokens/aoTokens/ao.ts | 49 ++++++++++++++++++++++ src/wallets/hardware/keystone.ts | 45 +++++++++++++++++++++ 4 files changed, 166 insertions(+), 25 deletions(-) diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index 0b745bb7..8844fb2f 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -45,16 +45,26 @@ import { import { fractionedToBalance } from "~tokens/currency"; import { type Token } from "~tokens/token"; import { useContact } from "~contacts/hooks"; -import { sendAoTransfer, useAo } from "~tokens/aoTokens/ao"; +import { + sendAoTransfer, + sendAoTransferKeystone, + useAo +} from "~tokens/aoTokens/ao"; import { useActiveWallet } from "~wallets/hooks"; import { UR } from "@ngraveio/bc-ur"; -import { decodeSignature, transactionToUR } from "~wallets/hardware/keystone"; +import { + KeystoneSigner, + decodeSignature, + transactionToUR, + type KeystoneInteraction +} from "~wallets/hardware/keystone"; import { useScanner } from "@arconnect/keystone-sdk"; import Progress from "~components/Progress"; import { updateSubscription } from "~subscriptions"; import { SubscriptionStatus } from "~subscriptions/subscription"; import { checkPassword } from "~wallets/auth"; import BigNumber from "bignumber.js"; +import { SignType } from "@keystonehq/bc-ur-registry-arweave"; interface Props { tokenID: string; @@ -507,6 +517,26 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { const [transactionUR, setTransactionUR] = useState(); const [preparedTx, setPreparedTx] = useState>(); + const keystoneInteraction = useMemo(() => { + const keystoneInteraction: KeystoneInteraction = { + display(data) { + setTransactionUR(data); + } + }; + return keystoneInteraction; + }, []); + + const keystoneSigner = useMemo(() => { + if (wallet?.type !== "hardware") return null; + const keystoneSigner = new KeystoneSigner( + Buffer.from(Arweave.utils.b64UrlToBuffer(wallet.publicKey)), + wallet.xfp, + isAo ? SignType.DataItem : SignType.Transaction, + keystoneInteraction + ); + return keystoneSigner; + }, [wallet, isAo, keystoneInteraction]); + useEffect(() => { (async () => { if (!recipient?.address) return; @@ -524,6 +554,32 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { // is a hardware wallet if (wallet?.type !== "hardware") return; + if (isAo) { + try { + setPreparedTx(prepared); + const res = await sendAoTransferKeystone( + ao, + tokenID, + recipient.address, + fractionedToBalance(amount, token, "AO"), + keystoneSigner + ); + if (res) { + setToast({ + type: "success", + content: browser.i18n.getMessage("sent_tx"), + duration: 2000 + }); + push(`/transaction/${res}`); + setIsLoading(false); + } + return res; + } catch (err) { + console.log("err in ao", err); + throw err; + } + } + const arweave = new Arweave(prepared.gateway); const convertedTransaction = arweave.transactions.fromRaw( prepared.transaction @@ -548,7 +604,7 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { push("/send/transfer"); } })(); - }, [wallet, recipient]); + }, [wallet, recipient, keystoneSigner]); // current hardware wallet operation const [hardwareStatus, setHardwareStatus] = useState<"play" | "scan">(); @@ -582,6 +638,11 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { // decode signature const { id, signature } = await decodeSignature(res); + if (isAo) { + keystoneSigner.submitSignature(signature); + return; + } + // set signature transaction.setSignature({ id, diff --git a/src/routes/popup/send/index.tsx b/src/routes/popup/send/index.tsx index 1b801e80..ea6e621e 100644 --- a/src/routes/popup/send/index.tsx +++ b/src/routes/popup/send/index.tsx @@ -138,9 +138,6 @@ export default function Send({ id }: Props) { "token" ); - const wallet = useActiveWallet(); - const keystoneError = wallet?.type === "hardware" && isAo; - // tokens const tokens = useTokens(); @@ -428,32 +425,21 @@ export default function Send({ id }: Props) { {AO_NATIVE_TOKEN === tokenID && ( )} - + {/* TOP INPUT */} - {(keystoneError || degraded) && ( + {degraded && (
- {keystoneError ? ( - <> -

{browser.i18n.getMessage("keystone_ao_title")}

- - {browser.i18n.getMessage("keystone_ao_description")} - - - ) : ( - <> -

{browser.i18n.getMessage("ao_degraded")}

- - {browser.i18n - .getMessage("ao_degraded_description") - .replace("
", "")} -
- - )} +

{browser.i18n.getMessage("ao_degraded")}

+ + {browser.i18n + .getMessage("ao_degraded_description") + .replace("
", "")} +
)} diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index 18407756..9e296881 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -14,6 +14,7 @@ import { AO_NATIVE_TOKEN_BALANCE_MIRROR } from "~utils/ao_import"; import type { Alarms } from "webextension-polyfill"; +import type { KeystoneSigner } from "~wallets/hardware/keystone"; export type AoInstance = ReturnType; @@ -484,6 +485,54 @@ export const aoTokensCacheHandler = async (alarmInfo?: Alarms.Alarm) => { } } await ExtensionStorage.set("ao_tokens", updatedTokens); +} + +export const sendAoTransferKeystone = async ( + ao: AoInstance, + process: string, + recipient: string, + amount: string, + keystoneSigner: KeystoneSigner +) => { + try { + const dataItemSigner = async ({ + data, + tags = [], + target, + anchor + }: { + data: any; + tags?: { name: string; value: string }[]; + target?: string; + anchor?: string; + }): Promise<{ id: string; raw: ArrayBuffer }> => { + const signer = keystoneSigner; + const dataItem = createData(data, signer, { tags, target, anchor }); + const serial = dataItem.getRaw(); + const signature = await signer.sign(serial); + dataItem.setSignature(Buffer.from(signature)); + + return { + id: dataItem.id, + raw: dataItem.getRaw() + }; + }; + const transferID = await ao.message({ + process, + signer: dataItemSigner, + tags: [ + { name: "Action", value: "Transfer" }, + { + name: "Recipient", + value: recipient + }, + { name: "Quantity", value: amount } + ] + }); + return transferID; + } catch (err) { + console.log("err", err); + } }; export interface TokenInfo { diff --git a/src/wallets/hardware/keystone.ts b/src/wallets/hardware/keystone.ts index 4458c773..d3e63e23 100644 --- a/src/wallets/hardware/keystone.ts +++ b/src/wallets/hardware/keystone.ts @@ -10,6 +10,51 @@ import type { UR } from "@ngraveio/bc-ur"; import { v4 as uuid } from "uuid"; import Arweave from "arweave"; import { defaultGateway } from "~gateways/gateway"; +import { Signer } from "arbundles"; +import { EventEmitter } from "events"; + +export interface KeystoneInteraction { + display(data: UR); +} + +export class KeystoneSigner implements Signer { + readonly signatureType: number = 1; + readonly ownerLength: number = 512; + readonly signatureLength: number = 512; + #_event = new EventEmitter(); + public get publicKey(): Buffer { + return this._publicKey; + } + + constructor( + private _publicKey: Buffer, + private mfp: string, + private signType: SignType, + private interaction: KeystoneInteraction, + private options: SignatureOptions = { saltLength: 32 } + ) {} + sign(message: Uint8Array, _opts?: any): Promise { + const data = Buffer.from(message); + const signRequest = ArweaveSignRequest.constructArweaveRequest( + data, + this.mfp, + this.signType, + this.options.saltLength + ); + return new Promise(async (resolve) => { + const ur = signRequest.toUR(); + this.interaction.display(ur); + this.#_event.once("submit-signature", (signature) => { + resolve(signature); + }); + }); + } + + submitSignature(signature: string) { + const signatureBytes = Buffer.from(signature, "base64"); + this.#_event.emit("submit-signature", signatureBytes); + } +} /** * Decode cbor result from a keystone QR code From 4c8028d9a2c1cb918c1b5cc02c7d601f5a0665f7 Mon Sep 17 00:00:00 2001 From: soralit Date: Fri, 28 Jun 2024 16:26:20 +0800 Subject: [PATCH 23/33] feat(keystone): wait for transaction prepared for keystone --- src/routes/popup/send/confirm.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index 8844fb2f..f2e53ab5 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -520,11 +520,12 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { const keystoneInteraction = useMemo(() => { const keystoneInteraction: KeystoneInteraction = { display(data) { + setIsLoading(false); setTransactionUR(data); } }; return keystoneInteraction; - }, []); + }, [setIsLoading]); const keystoneSigner = useMemo(() => { if (wallet?.type !== "hardware") return null; @@ -539,6 +540,7 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { useEffect(() => { (async () => { + setIsLoading(true); if (!recipient?.address) return; // get the tx from storage @@ -587,6 +589,7 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { // get tx UR try { + setIsLoading(false); setTransactionUR( await transactionToUR( convertedTransaction, @@ -604,7 +607,7 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { push("/send/transfer"); } })(); - }, [wallet, recipient, keystoneSigner]); + }, [wallet, recipient, keystoneSigner, setIsLoading]); // current hardware wallet operation const [hardwareStatus, setHardwareStatus] = useState<"play" | "scan">(); From 8bbcd6cc5b1c9cc83905597c1fba61ea997e058c Mon Sep 17 00:00:00 2001 From: soralit Date: Mon, 8 Jul 2024 18:06:44 +0800 Subject: [PATCH 24/33] feat(keystone): support sign ao transfer via api for dapps --- src/api/modules/connect/auth.ts | 2 +- src/api/modules/sign/chunks.ts | 2 +- src/api/modules/sign/sign_auth.ts | 51 ++++++++- src/api/modules/sign/transaction_builder.ts | 48 +++++++- .../sign_data_item.background.ts | 103 +++++++++++------- .../sign_message/sign_message.background.ts | 8 +- .../{signMessage.tsx => signKeystone.tsx} | 89 ++++++++++----- src/tabs/auth.tsx | 4 +- src/utils/auth.ts | 3 + src/utils/data_item.ts | 39 +++++++ src/wallets/hardware/keystone.ts | 18 +++ 11 files changed, 294 insertions(+), 73 deletions(-) rename src/routes/auth/{signMessage.tsx => signKeystone.tsx} (68%) create mode 100644 src/utils/data_item.ts diff --git a/src/api/modules/connect/auth.ts b/src/api/modules/connect/auth.ts index a389df49..dd9bbada 100644 --- a/src/api/modules/connect/auth.ts +++ b/src/api/modules/connect/auth.ts @@ -11,7 +11,7 @@ export type AuthType = | "token" | "sign" | "subscription" - | "signMessage" + | "signKeystone" | "signature" | "signDataItem"; diff --git a/src/api/modules/sign/chunks.ts b/src/api/modules/sign/chunks.ts index 51d75759..affe2361 100644 --- a/src/api/modules/sign/chunks.ts +++ b/src/api/modules/sign/chunks.ts @@ -7,7 +7,7 @@ import { nanoid } from "nanoid"; */ export interface Chunk { collectionID: string; // unique ID for the collection, that is the parent of this chunk - type: "tag" | "data" | "start" | "end"; + type: "tag" | "data" | "bytes" | "start" | "end"; index: number; // index of the chunk, to make sure it is not in the wrong order value?: number[] | Tag; // Uint8Array converted to number array or a tag } diff --git a/src/api/modules/sign/sign_auth.ts b/src/api/modules/sign/sign_auth.ts index 3b006fcd..d025d1d6 100644 --- a/src/api/modules/sign/sign_auth.ts +++ b/src/api/modules/sign/sign_auth.ts @@ -1,8 +1,9 @@ import { onMessage, sendMessage } from "@arconnect/webext-bridge"; -import { deconstructTransaction } from "./transaction_builder"; +import { bytesToChunks, deconstructTransaction } from "./transaction_builder"; import type Transaction from "arweave/web/lib/transaction"; import type { AuthResult } from "shim"; import authenticate from "../connect/auth"; +import { nanoid } from "nanoid"; /** * Request a manual signature for the transaction. @@ -73,15 +74,57 @@ export const signAuth = ( } ); -export const signAuthMessage = (dataToSign: Uint8Array) => +export type AuthKeystoneType = "Message" | "DataItem"; + +export interface AuthKeystoneData { + type: AuthKeystoneType; + data: Uint8Array; +} + +export const signAuthKeystone = (dataToSign: AuthKeystoneData) => new Promise>( (resolve, reject) => { // start auth + const collectionID = nanoid(); authenticate({ - type: "signMessage", - data: Buffer.from(dataToSign).toString("base64") + type: "signKeystone", + keystoneSignType: dataToSign.type, + collectionID }) .then((res) => resolve(res)) .catch((err) => reject(err)); + const dataChunks = bytesToChunks(dataToSign.data, collectionID, 0); + + // send tx in chunks to sign if requested + onMessage("auth_listening", async ({ sender }) => { + if (sender.context !== "web_accessible") return; + + // send data chunks + for (const chunk of dataChunks) { + try { + await sendMessage( + "auth_chunk", + chunk, + `web_accessible@${sender.tabId}` + ); + } catch (e) { + // chunk fail + return reject( + `Error while sending a data chunk of collection "${collectionID}": \n${e}` + ); + } + } + + // end chunk + await sendMessage( + "auth_chunk", + { + collectionID, + type: "end", + index: dataChunks.length + }, + `web_accessible@${sender.tabId}` + ); + }); } ); diff --git a/src/api/modules/sign/transaction_builder.ts b/src/api/modules/sign/transaction_builder.ts index c6c83dfd..348b6c9c 100644 --- a/src/api/modules/sign/transaction_builder.ts +++ b/src/api/modules/sign/transaction_builder.ts @@ -9,6 +9,52 @@ import { nanoid } from "nanoid"; */ export type SplitTransaction = Partial; +/** + * Split an Uint8Array to chunks, per chunks value max size is limited to 0.5mb + */ +export const bytesToChunks = ( + data: Uint8Array, + id: string, + start_index: number +): Chunk[] => { + const dataChunks: Chunk[] = []; + + for (let i = 0; i < Math.ceil(data.length / CHUNK_SIZE); i++) { + const sliceFrom = i * CHUNK_SIZE; + const chunkValue = data.slice(sliceFrom, sliceFrom + CHUNK_SIZE); + + dataChunks.push({ + collectionID: id, + type: "bytes", + index: i + start_index, + value: Array.from(chunkValue) + }); + } + return dataChunks; +}; + +/** + * Reconstruct bytes from chunks + */ +export const bytesFromChunks = (chunks: Chunk[]): Uint8Array => { + chunks.sort((a, b) => a.index - b.index); + + const dataSize = getDataSize(chunks); + const reconstructedData = new Uint8Array(dataSize); + + let previousLength = 0; + + for (const chunk of chunks) { + if (chunk.type === "bytes") { + const chunkBuffer = new Uint8Array(chunk.value as number[]); + + reconstructedData.set(chunkBuffer, previousLength); + previousLength += chunkBuffer.length; + } + } + return reconstructedData; +}; + /** * Split the tags and the data of a transaction in * chunks and remove them from the transaction object @@ -81,7 +127,7 @@ export function getDataSize(chunks: Chunk[]): number { let dataSize = 0; for (const chunk of chunks) { - if (chunk.type === "data") { + if (chunk.type === "data" || chunk.type === "bytes") { dataSize += chunk.value?.length || 0; } } diff --git a/src/api/modules/sign_data_item/sign_data_item.background.ts b/src/api/modules/sign_data_item/sign_data_item.background.ts index cca05116..3056c537 100644 --- a/src/api/modules/sign_data_item/sign_data_item.background.ts +++ b/src/api/modules/sign_data_item/sign_data_item.background.ts @@ -9,12 +9,18 @@ import type { ModuleFunction } from "~api/background"; import { ArweaveSigner, createData } from "arbundles"; import Application from "~applications/application"; import { getPrice } from "../dispatch/uploader"; -import { getActiveKeyfile } from "~wallets"; +import { getActiveKeyfile, getActiveWallet } from "~wallets"; import browser from "webextension-polyfill"; -import { signAuth } from "../sign/sign_auth"; +import { + signAuth, + signAuthKeystone, + type AuthKeystoneData +} from "../sign/sign_auth"; import Arweave from "arweave"; import authenticate from "../connect/auth"; import BigNumber from "bignumber.js"; +import { createDataItem } from "~utils/data_item"; +import signMessage from "../sign_message"; const background: ModuleFunction = async ( appData, @@ -69,10 +75,6 @@ const background: ModuleFunction = async ( throw new Error("No wallets added"); }); - // ensure that the currently selected - // wallet is not a local wallet - isLocalWallet(decryptedWallet); - // create app const app = new Application(appData.appURL); @@ -83,44 +85,71 @@ const background: ModuleFunction = async ( const { data, ...options } = dataItem; const binaryData = new Uint8Array(data); - // create bundlr tx as a data entry - const dataSigner = new ArweaveSigner(decryptedWallet.keyfile); - const dataEntry = createData(binaryData, dataSigner, options); + if (decryptedWallet.type == "local") { + // create bundlr tx as a data entry + const dataSigner = new ArweaveSigner(decryptedWallet.keyfile); + const dataEntry = createData(binaryData, dataSigner, options); - // check allowance - // const price = await getPrice(dataEntry, await app.getBundler()); - const allowance = await app.getAllowance(); + // check allowance + // const price = await getPrice(dataEntry, await app.getBundler()); + const allowance = await app.getAllowance(); - // allowance or sign auth - try { - if (!allowance.enabled) { - // get address - const address = await arweave.wallets.jwkToAddress( - decryptedWallet.keyfile - ); + // allowance or sign auth + try { + if (!allowance.enabled) { + // get address + const address = await arweave.wallets.jwkToAddress( + decryptedWallet.keyfile + ); - await signAuth( - appData.appURL, - // @ts-expect-error - dataEntry.toJSON(), - address - ); + await signAuth( + appData.appURL, + // @ts-expect-error + dataEntry.toJSON(), + address + ); + } + } catch (e) { + freeDecryptedWallet(decryptedWallet.keyfile); + throw new Error(e?.message || e); } - } catch (e) { - freeDecryptedWallet(decryptedWallet.keyfile); - throw new Error(e?.message || e); - } + // sign item + await dataEntry.sign(dataSigner); - // sign item - await dataEntry.sign(dataSigner); + // update allowance spent amount (in winstons) + // await updateAllowance(appData.appURL, price); - // update allowance spent amount (in winstons) - // await updateAllowance(appData.appURL, price); - - // remove keyfile - freeDecryptedWallet(decryptedWallet.keyfile); + // remove keyfile + freeDecryptedWallet(decryptedWallet.keyfile); - return Array.from(dataEntry.getRaw()); + return Array.from(dataEntry.getRaw()); + } else { + // create bundlr tx as a data entry + const activeWallet = await getActiveWallet(); + if (activeWallet.type != "hardware") throw new Error("Invalid Wallet Type"); + const signerConfig = { + signatureType: 1, + signatureLength: 512, + ownerLength: 512, + publicKey: Buffer.from( + Arweave.utils.b64UrlToBuffer(activeWallet.publicKey) + ) + }; + const dataEntry = createDataItem(binaryData, signerConfig, options); + try { + const data: AuthKeystoneData = { + type: "DataItem", + data: dataEntry.getRaw() + }; + const res = await signAuthKeystone(data); + dataEntry.setSignature( + Buffer.from(Arweave.utils.b64UrlToBuffer(res.data.signature)) + ); + } catch (e) { + throw new Error(e?.message || e); + } + return Array.from(dataEntry.getRaw()); + } }; export default background; diff --git a/src/api/modules/sign_message/sign_message.background.ts b/src/api/modules/sign_message/sign_message.background.ts index 716b25d6..c680c03b 100644 --- a/src/api/modules/sign_message/sign_message.background.ts +++ b/src/api/modules/sign_message/sign_message.background.ts @@ -8,7 +8,7 @@ import { isNumberArray, isSignMessageOptions } from "~utils/assertions"; -import { signAuthMessage } from "../sign/sign_auth"; +import { signAuthKeystone, type AuthKeystoneData } from "../sign/sign_auth"; import Arweave from "arweave"; const background: ModuleFunction = async ( @@ -66,7 +66,11 @@ const background: ModuleFunction = async ( return Array.from(new Uint8Array(signature)); } else { - const res = await signAuthMessage(dataToSign); + const data: AuthKeystoneData = { + type: "Message", + data: dataToSign + }; + const res = await signAuthKeystone(data); const sig = Arweave.utils.b64UrlToBuffer(res.data.signature); return Array.from(sig); } diff --git a/src/routes/auth/signMessage.tsx b/src/routes/auth/signKeystone.tsx similarity index 68% rename from src/routes/auth/signMessage.tsx rename to src/routes/auth/signKeystone.tsx index 6fa406f0..fdbb94e4 100644 --- a/src/routes/auth/signMessage.tsx +++ b/src/routes/auth/signKeystone.tsx @@ -1,15 +1,13 @@ import { replyToAuthRequest, useAuthParams, useAuthUtils } from "~utils/auth"; -import { decodeSignature, messageToUR } from "~wallets/hardware/keystone"; +import { + dataItemToUR, + decodeSignature, + messageToUR +} from "~wallets/hardware/keystone"; import { useEffect, useState } from "react"; import { useScanner } from "@arconnect/keystone-sdk"; import { useActiveWallet } from "~wallets/hooks"; import type { UR } from "@ngraveio/bc-ur"; -import { - Properties, - PropertyName, - PropertyValue, - TransactionProperty -} from "~routes/popup/transaction/[id]"; import { ButtonV2, Section, @@ -24,31 +22,69 @@ import Progress from "~components/Progress"; import browser from "webextension-polyfill"; import Head from "~components/popup/Head"; import Message from "~components/auth/Message"; - -export default function SignMessage() { +import type { AuthKeystoneType } from "~api/modules/sign/sign_auth"; +import { onMessage, sendMessage } from "@arconnect/webext-bridge"; +import type { Chunk } from "~api/modules/sign/chunks"; +import { bytesFromChunks } from "~api/modules/sign/transaction_builder"; +export default function SignKeystone() { // sign params const params = useAuthParams<{ - data: string; + collectionId: string; + keystoneSignType: string; }>(); - // reconstructed transaction const [dataToSign, setDataToSign] = useState(); + const [dataType, setDataType] = useState("Message"); useEffect(() => { (async () => { - if (!params?.data) return; - // reset tx - setDataToSign(Buffer.from(params?.data, "base64")); + // request chunks + if (params) { + setDataType(params?.keystoneSignType); + sendMessage("auth_listening", null, "background"); + + const chunks: Chunk[] = []; + + // listen for chunks + onMessage("auth_chunk", ({ sender, data }) => { + // check data type + if ( + data.collectionID !== params.collectionID || + sender.context !== "background" || + data.type === "start" + ) { + return; + } + // end chunk stream + if (data.type === "end") { + const bytes = bytesFromChunks(chunks); + const signData = Buffer.from(bytes); + setDataToSign(signData); + } else if (data.type === "bytes") { + // add chunk + chunks.push(data); + } + }); + } })(); }, [params]); + useEffect(() => { + (async () => { + if (dataType === "DataItem" && !!dataToSign) { + await loadTransactionUR(); + setPage("qr"); + } + })(); + }, [dataType, dataToSign]); + // get auth utils - const { closeWindow, cancel } = useAuthUtils("signMessage", params?.authID); + const { closeWindow, cancel } = useAuthUtils("signKeystone", params?.authID); // authorize async function authorize(data?: any) { // reply to request - await replyToAuthRequest("signMessage", params.authID, undefined, data); + await replyToAuthRequest("signKeystone", params.authID, undefined, data); // close the window closeWindow(); @@ -69,11 +105,14 @@ export default function SignMessage() { async function loadTransactionUR() { if (wallet.type !== "hardware" || !dataToSign) return; - // load the ur data - const ur = await messageToUR(dataToSign, wallet.xfp); - - setTransactionUR(ur); + if (dataType === "DataItem") { + const ur = await dataItemToUR(dataToSign, wallet.xfp); + setTransactionUR(ur); + } else { + const ur = await messageToUR(dataToSign, wallet.xfp); + setTransactionUR(ur); + } } // loading @@ -106,7 +145,7 @@ export default function SignMessage() { // reply to request await replyToAuthRequest( - "signMessage", + "signKeystone", params.authID, "Failed to decode signature from keystone" ); @@ -133,7 +172,7 @@ export default function SignMessage() { allowOpen={false} /> - {(!page && dataToSign && ( + {(!page && dataToSign && dataType === "Message" && (
@@ -145,13 +184,13 @@ export default function SignMessage() { <> + onError={(error) => { setToast({ type: "error", duration: 2300, content: browser.i18n.getMessage(`keystone_${error}`) - }) - } + }); + }} /> diff --git a/src/tabs/auth.tsx b/src/tabs/auth.tsx index 0d416d35..54dd641e 100644 --- a/src/tabs/auth.tsx +++ b/src/tabs/auth.tsx @@ -16,7 +16,7 @@ import SignDataItem from "~routes/auth/signDataItem"; import Token from "~routes/auth/token"; import Sign from "~routes/auth/sign"; import Subscription from "~routes/auth/subscription"; -import SignMessage from "~routes/auth/signMessage"; +import SignKeystone from "~routes/auth/signKeystone"; export default function Auth() { const theme = useTheme(); @@ -36,7 +36,7 @@ export default function Auth() { - + diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 1ef31d14..03248ed9 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -8,11 +8,14 @@ import type { AuthResult } from "shim"; * Hook to parse auth params from the url */ export function useAuthParams() { + console.log("useAuthParams"); const [params, setParams] = useState(); // fetch params useEffect(() => { + console.log("here", window.location.href); const urlParams = window.location.href.split("?"); + console.log("here", urlParams); const params = objectFromUrlParams( urlParams[urlParams.length - 1].replace(window.location.hash, "") ); diff --git a/src/utils/data_item.ts b/src/utils/data_item.ts new file mode 100644 index 00000000..dc707bcd --- /dev/null +++ b/src/utils/data_item.ts @@ -0,0 +1,39 @@ +import { + ArweaveSigner, + DataItem, + Signer, + createData, + type DataItemCreateOptions +} from "arbundles"; + +export interface SignerConfig { + signatureType: number; + signatureLength: number; + ownerLength: number; + publicKey: Buffer; +} + +class DummySigner implements Signer { + publicKey: Buffer; + signatureType: number; + signatureLength: number; + ownerLength: number; + pem?: string | Buffer; + constructor(signerConfig: SignerConfig) { + this.publicKey = signerConfig.publicKey; + this.signatureLength = signerConfig.signatureLength; + this.signatureType = signerConfig.signatureType; + this.ownerLength = signerConfig.ownerLength; + } + sign(message: Uint8Array, _opts?: any): Uint8Array | Promise { + throw new Error("Method not implemented."); + } +} + +export const createDataItem = ( + binary: Uint8Array, + signerConfig: SignerConfig, + options: DataItemCreateOptions +): DataItem => { + return createData(binary, new DummySigner(signerConfig), options); +}; diff --git a/src/wallets/hardware/keystone.ts b/src/wallets/hardware/keystone.ts index d3e63e23..471a7e71 100644 --- a/src/wallets/hardware/keystone.ts +++ b/src/wallets/hardware/keystone.ts @@ -143,6 +143,24 @@ export async function messageToUR( return signRequest.toUR(); } +export async function dataItemToUR( + data: Uint8Array, + xfp: string, + options: SignatureOptions = { saltLength: 32 } +) { + const messageBuff = Buffer.from(data); + + // construct request + const signRequest = ArweaveSignRequest.constructArweaveRequest( + messageBuff, + xfp, + SignType.DataItem, + options.saltLength + ); + + return signRequest.toUR(); +} + /** * Decode cbor result from a keystone QR code * with an Arweave transaction From 208f144f1429136cba61fbbf85fb08aee8a9078c Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 12 Jul 2024 14:50:20 +0545 Subject: [PATCH 25/33] refactor: add long wait msg and auto-retry for seedphrase --- assets/_locales/en/messages.json | 12 +-- assets/_locales/zh_CN/messages.json | 12 +-- .../dashboard/subsettings/AddWallet.tsx | 38 +++++---- .../modals/WalletRetryCreationModal.tsx | 85 ------------------- src/routes/welcome/generate/backup.tsx | 52 ++---------- src/routes/welcome/generate/done.tsx | 42 ++++++++- src/routes/welcome/load/wallets.tsx | 12 +++ src/routes/welcome/setup.tsx | 17 +++- 8 files changed, 103 insertions(+), 167 deletions(-) delete mode 100644 src/components/modals/WalletRetryCreationModal.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 56467ae7..a9614de1 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -681,14 +681,6 @@ "message": "The key length of the wallet you are importing is less than the required length to transact on the AO network. It is recommended you create a new ArConnect wallet and transfer your assets to your new wallet.", "description": "Key Length Too Short error" }, - "generate_wallet_key_length_short_error_title": { - "message": "Error generating wallet", - "description": "Key Length Too Short error title" - }, - "generate_wallet_key_length_short_error": { - "message": "Click the retry button below to try again.", - "description": "Key Length Too Short error" - }, "generate_wallet_in_progress": { "message": "The wallet creation process can take 30-60 seconds.", "description": "Generate wallet in progresss text" @@ -807,6 +799,10 @@ "message": "Generating wallet...", "description": "Generating wallet in progress text" }, + "longer_than_usual": { + "message": "Please wait, this is taking longer than usual...", + "description": "Longer than usual text" + }, "keyfile": { "message": "Keyfile", "description": "Keyfile text" diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 85f919f7..2caba310 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -681,14 +681,6 @@ "message": "您导入的钱包的密钥长度小于在 AO 网络上进行交易所需的长度。建议您创建一个新的ArConnect钱包,并将您的资产转移到新钱包中。", "description": "Key Length Too Short error" }, - "generate_wallet_key_length_short_error_title": { - "message": "生成钱包时出错", - "description": "Key Length Too Short error title" - }, - "generate_wallet_key_length_short_error": { - "message": "单击下面的重试按钮再试一次。", - "description": "Key Length Too Short error" - }, "generate_wallet_in_progress": { "message": "钱包创建过程可能需要 30-60 秒。", "description": "Generate wallet in progresss text" @@ -807,6 +799,10 @@ "message": "生成钱包中...", "description": "Generating wallet in progress text" }, + "longer_than_usual": { + "message": "请稍等,这比平常需要更长的时间...", + "description": "Longer than usual text" + }, "keyfile": { "message": "密钥文件", "description": "Keyfile text" diff --git a/src/components/dashboard/subsettings/AddWallet.tsx b/src/components/dashboard/subsettings/AddWallet.tsx index 00f868f0..a1767f30 100644 --- a/src/components/dashboard/subsettings/AddWallet.tsx +++ b/src/components/dashboard/subsettings/AddWallet.tsx @@ -23,7 +23,6 @@ import Arweave from "arweave/web/common"; import styled from "styled-components"; import { defaultGateway } from "~gateways/gateway"; import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; -import { WalletRetryCreationModal } from "~components/modals/WalletRetryCreationModal"; export default function AddWallet() { // password input @@ -32,8 +31,8 @@ export default function AddWallet() { // wallet size error modal const walletModal = useModal(); - // wallet retry modal - const walletRetryModal = useModal(); + // wallet generation taking longer + const [showLongWaitMessage, setShowLongWaitMessage] = useState(false); // toasts const { setToast } = useToasts(); @@ -116,6 +115,7 @@ export default function AddWallet() { const finishUp = () => { // reset before unload window.onbeforeunload = null; + setShowLongWaitMessage(false); setLoading(false); }; @@ -135,6 +135,7 @@ export default function AddWallet() { } try { + const startTime = Date.now(); // load jwk from seedphrase input state let jwk = typeof providedWallet === "string" @@ -149,6 +150,7 @@ export default function AddWallet() { return; } else { while (expectedLength !== actualLength) { + setShowLongWaitMessage(Date.now() - startTime > 30000); jwk = await jwkFromMnemonic(providedWallet); ({ actualLength, expectedLength } = await getWalletKeyLength(jwk)); } @@ -195,18 +197,28 @@ export default function AddWallet() { }, []); // generate new wallet - async function generateWallet(retry?: boolean) { + async function generateWallet() { setGenerating(true); + const startTime = Date.now(); + // generate a seedphrase const seedphrase = await bip39.generateMnemonic(); setGeneratedWallet({ seedphrase }); // generate from seedphrase - const jwk = await jwkFromMnemonic(seedphrase); + let jwk = await jwkFromMnemonic(seedphrase); + + let { actualLength, expectedLength } = await getWalletKeyLength(jwk); + while (expectedLength !== actualLength) { + setShowLongWaitMessage(Date.now() - startTime > 30000); + jwk = await jwkFromMnemonic(seedphrase); + ({ actualLength, expectedLength } = await getWalletKeyLength(jwk)); + } setGeneratedWallet((val) => ({ ...val, jwk })); + setShowLongWaitMessage(false); setGenerating(false); return { jwk, seedphrase }; @@ -250,13 +262,6 @@ export default function AddWallet() { } try { - const { actualLength, expectedLength } = await getWalletKeyLength( - generatedWallet.jwk - ); - if (actualLength !== expectedLength) { - walletRetryModal.setOpen(true); - return; - } // add the wallet await addWallet(generatedWallet.jwk, passwordInput.state); @@ -355,12 +360,13 @@ export default function AddWallet() { {browser.i18n.getMessage("generate_wallet")} + {(generating || loading) && showLongWaitMessage && ( + + {browser.i18n.getMessage("longer_than_usual")} + + )}
-
); } diff --git a/src/components/modals/WalletRetryCreationModal.tsx b/src/components/modals/WalletRetryCreationModal.tsx deleted file mode 100644 index 8f58320a..00000000 --- a/src/components/modals/WalletRetryCreationModal.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { ButtonV2, ModalV2, Spacer } from "@arconnect/components"; -import { useRef, useState } from "react"; -import browser from "webextension-polyfill"; -import { ContentWrapper, Content, HeaderText, CenterText } from "./Components"; -import styled from "styled-components"; -import type { JWKInterface } from "arweave/web/lib/wallet"; -import { getWalletKeyLength } from "~wallets"; - -interface Props { - isOpen: boolean; - setOpen: (value: boolean) => void; - onRetry: ( - retry?: boolean - ) => Promise>; -} - -export const WalletRetryCreationModal = ({ - isOpen, - setOpen, - onRetry -}: Props) => { - const modalRef = useRef(null); - const [loading, setLoading] = useState(false); - - async function handleRetry() { - if (loading) return; - setLoading(true); - try { - const { jwk } = await onRetry(true); - const { actualLength, expectedLength } = await getWalletKeyLength(jwk); - if (actualLength === expectedLength) { - setOpen(false); - } - } catch {} - setLoading(false); - } - - return ( - } - > - - -
- - {browser.i18n.getMessage( - "generate_wallet_key_length_short_error_title" - )} - - - - {browser.i18n.getMessage( - "generate_wallet_key_length_short_error" - )} - - -
-
- - - {browser.i18n.getMessage("retry")} - - setOpen(false)}> - {browser.i18n.getMessage("cancel")} - - - - {loading && ( - - {browser.i18n.getMessage("generate_wallet_in_progress")} - - )} -
-
- ); -}; - -const ButtonsWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 8px; -`; diff --git a/src/routes/welcome/generate/backup.tsx b/src/routes/welcome/generate/backup.tsx index 366ccda7..38976d24 100644 --- a/src/routes/welcome/generate/backup.tsx +++ b/src/routes/welcome/generate/backup.tsx @@ -1,4 +1,4 @@ -import { ButtonV2, Spacer, Text, useModal } from "@arconnect/components"; +import { ButtonV2, Spacer, Text } from "@arconnect/components"; import { useLocation, useRoute } from "wouter"; import { useContext, useEffect, useRef, useState } from "react"; import { WalletContext } from "../setup"; @@ -14,21 +14,13 @@ import { EyeOffIcon } from "@iconicicons/react"; import { PageType, trackPage } from "~utils/analytics"; -import { getWalletKeyLength } from "~wallets"; -import { WalletRetryCreationModal } from "~components/modals/WalletRetryCreationModal"; export default function Backup() { // seed blur status const [shown, setShown] = useState(false); - // loading - const [loading, setLoading] = useState(false); - - // wallet retry modal - const walletRetryModal = useModal(); - // wallet context - const { wallet: generatedWallet, generateWallet } = useContext(WalletContext); + const { wallet: generatedWallet } = useContext(WalletContext); // ref to track the latest generated wallet const walletRef = useRef(generatedWallet); @@ -47,35 +39,6 @@ export default function Backup() { setTimeout(() => setCopyDisplay(true), 1050); } - async function handleNext() { - if (loading) return; - setLoading(true); - - try { - if (!walletRef.current.jwk) { - await new Promise((resolve) => { - const checkState = setInterval(() => { - if (walletRef.current.jwk) { - clearInterval(checkState); - resolve(null); - } - }, 500); - }); - } - - const { actualLength, expectedLength } = await getWalletKeyLength( - walletRef.current.jwk - ); - if (expectedLength !== actualLength) { - walletRetryModal.setOpen(true); - } else { - setLocation(`/${params.setup}/${Number(params.page) + 1}`); - } - } catch {} - - setLoading(false); - } - useEffect(() => { walletRef.current = generatedWallet; }, [generatedWallet]); @@ -99,14 +62,15 @@ export default function Backup() { {browser.i18n.getMessage("copySeed")} - + + setLocation(`/${params.setup}/${Number(params.page) + 1}`) + } + > {browser.i18n.getMessage("next")} - ); } diff --git a/src/routes/welcome/generate/done.tsx b/src/routes/welcome/generate/done.tsx index 8903811e..0a6f474c 100644 --- a/src/routes/welcome/generate/done.tsx +++ b/src/routes/welcome/generate/done.tsx @@ -5,7 +5,7 @@ import { formatAddress } from "~utils/format"; import Paragraph from "~components/Paragraph"; import browser from "webextension-polyfill"; import { addWallet } from "~wallets"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { EventType, PageType, @@ -23,6 +23,13 @@ import { addExpiration } from "~wallets/auth"; export default function Done() { // wallet context const { wallet } = useContext(WalletContext); + const walletRef = useRef(wallet); + + // loading + const [loading, setLoading] = useState(false); + + // wallet generation taking longer + const [showLongWaitMessage, setShowLongWaitMessage] = useState(false); const [, setLocation] = useLocation(); @@ -36,10 +43,27 @@ export default function Done() { // add generated wallet async function done() { + if (loading) return; + + const startTime = Date.now(); + + setLoading(true); // add wallet let nickname: string; - if (!wallet.address || !wallet.jwk) return; + if (!walletRef.current.address || !walletRef.current.jwk) { + await new Promise((resolve) => { + const checkState = setInterval(() => { + if (walletRef.current.jwk) { + clearInterval(checkState); + resolve(null); + } + if (!showLongWaitMessage) { + setShowLongWaitMessage(Date.now() - startTime > 10000); + } + }, 1000); + }); + } try { const ansProfile = (await getAnsProfile(wallet.address)) as AnsUser; @@ -68,6 +92,9 @@ export default function Done() { // redirect to getting started pages setLocation("/getting-started/1"); + + setShowLongWaitMessage(false); + setLoading(false); } useEffect(() => { @@ -90,6 +117,10 @@ export default function Done() { getLocation(); }, []); + useEffect(() => { + walletRef.current = wallet; + }, [wallet]); + // Segment useEffect(() => { trackPage(PageType.ONBOARD_COMPLETE); @@ -113,9 +144,14 @@ export default function Done() { {browser.i18n.getMessage("analytics_title")} - + {browser.i18n.getMessage("done")} + {loading && showLongWaitMessage && ( + + {browser.i18n.getMessage("longer_than_usual")} + + )} ); } diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx index c585ccdd..2b579101 100644 --- a/src/routes/welcome/load/wallets.tsx +++ b/src/routes/welcome/load/wallets.tsx @@ -34,6 +34,9 @@ export default function Wallets() { // password context const { password } = useContext(PasswordContext); + // wallet generation taking longer + const [showLongWaitMessage, setShowLongWaitMessage] = useState(false); + // migration available const [oldState] = useStorage({ key: OLD_STORAGE_NAME, @@ -110,6 +113,7 @@ export default function Wallets() { const finishUp = () => { // reset before unload window.onbeforeunload = null; + setShowLongWaitMessage(false); setLoading(false); }; @@ -135,6 +139,8 @@ export default function Wallets() { if (loadedWallet) { // load jwk from seedphrase input state + const startTime = Date.now(); + let jwk = typeof loadedWallet === "string" ? await jwkFromMnemonic(loadedWallet) @@ -148,6 +154,7 @@ export default function Wallets() { return; } else { while (expectedLength !== actualLength) { + setShowLongWaitMessage(Date.now() - startTime > 30000); jwk = await jwkFromMnemonic(loadedWallet); ({ actualLength, expectedLength } = await getWalletKeyLength( jwk @@ -223,6 +230,11 @@ export default function Wallets() { {browser.i18n.getMessage("next")} + {loading && showLongWaitMessage && ( + + {browser.i18n.getMessage("longer_than_usual")} + + )} ({ ...val, jwk: generatedKeyfile })); From 0968ff5ab462d196a47e22a2921cfda0bb41ece4 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 12 Jul 2024 15:19:42 +0545 Subject: [PATCH 26/33] fix: Use wallet ref --- src/routes/welcome/generate/done.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes/welcome/generate/done.tsx b/src/routes/welcome/generate/done.tsx index 0a6f474c..4ce95c30 100644 --- a/src/routes/welcome/generate/done.tsx +++ b/src/routes/welcome/generate/done.tsx @@ -66,7 +66,9 @@ export default function Done() { } try { - const ansProfile = (await getAnsProfile(wallet.address)) as AnsUser; + const ansProfile = (await getAnsProfile( + walletRef.current.address + )) as AnsUser; if (ansProfile) { nickname = ansProfile.currentLabel; @@ -75,7 +77,9 @@ export default function Done() { // add the wallet await addWallet( - nickname ? { nickname, wallet: wallet.jwk } : wallet.jwk, + nickname + ? { nickname, wallet: walletRef.current.jwk } + : walletRef.current.jwk, password ); From 37f4a03e565c02862d9b00b8dfc96d2cc910cdc4 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Fri, 12 Jul 2024 22:55:23 +0545 Subject: [PATCH 27/33] fix: Make sure tab cannot be closed before wallet is created --- src/routes/welcome/generate/done.tsx | 3 +++ src/routes/welcome/gettingStarted.tsx | 2 ++ src/routes/welcome/load/done.tsx | 3 +++ src/routes/welcome/setup.tsx | 2 -- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/welcome/generate/done.tsx b/src/routes/welcome/generate/done.tsx index 4ce95c30..89f2a93e 100644 --- a/src/routes/welcome/generate/done.tsx +++ b/src/routes/welcome/generate/done.tsx @@ -99,6 +99,9 @@ export default function Done() { setShowLongWaitMessage(false); setLoading(false); + + // reset before unload + window.onbeforeunload = null; } useEffect(() => { diff --git a/src/routes/welcome/gettingStarted.tsx b/src/routes/welcome/gettingStarted.tsx index cd74a154..a79d7a20 100644 --- a/src/routes/welcome/gettingStarted.tsx +++ b/src/routes/welcome/gettingStarted.tsx @@ -38,6 +38,8 @@ export default function GettingStarted({ page }) { if (pageNum < 5) { setLocation(`/getting-started/${pageNum}`); } else { + // reset before unload + window.onbeforeunload = null; window.top.close(); } }; diff --git a/src/routes/welcome/load/done.tsx b/src/routes/welcome/load/done.tsx index 329c7d86..fdbc4536 100644 --- a/src/routes/welcome/load/done.tsx +++ b/src/routes/welcome/load/done.tsx @@ -25,6 +25,9 @@ export default function Done() { await setAnalytics(false); } + // reset before unload + window.onbeforeunload = null; + // redirect to getting started pages setLocation("/getting-started/1"); } diff --git a/src/routes/welcome/setup.tsx b/src/routes/welcome/setup.tsx index 65f6e0f6..6162d630 100644 --- a/src/routes/welcome/setup.tsx +++ b/src/routes/welcome/setup.tsx @@ -147,8 +147,6 @@ export default function Setup({ setupMode, page }: Props) { }); } - // reset before unload - window.onbeforeunload = null; return {}; } From 1be6577b96735afa402ee1d7d027830e49979739 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Fri, 12 Jul 2024 11:56:36 -0700 Subject: [PATCH 28/33] Revert "feat: updated permissions screen" --- assets/_locales/en/messages.json | 34 +--- assets/_locales/zh_CN/messages.json | 34 +--- src/components/Checkbox.tsx | 27 ++- src/components/auth/App.tsx | 48 ++--- src/components/auth/Permissions.tsx | 134 ------------- src/routes/auth/connect.tsx | 292 ++++++---------------------- 6 files changed, 89 insertions(+), 480 deletions(-) delete mode 100644 src/components/auth/Permissions.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 2ff57639..1a2d6798 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -49,10 +49,6 @@ "message": "Hide", "description": "Hide text" }, - "save": { - "message": "Save", - "description": "Save button text" - }, "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" @@ -299,14 +295,6 @@ "message": "Permissions", "description": "Permissions title" }, - "edit_permissions": { - "message": "Edit permissions", - "description": "Edit permissions title" - }, - "app_permissions": { - "message": "App permissions", - "description": "App permissions title" - }, "allowance": { "message": "Allowance", "description": "Allowance title" @@ -941,27 +929,9 @@ "message": "Connect", "description": "Connect button text" }, - "always_allow": { - "message": "Always allow", - "description": "Always allow button" - }, - "always_ask_permission": { - "message": "Always ask permission", - "description": "Always allow button" - }, - "allow_selected_permissions": { - "message": "Allow selected permissions", - "description": "Always allow button" - }, "allow_these_permissions": { - "message": "$APPNAME$ wants to connect to your wallet with the following permissions", - "description": "Auth permissions label", - "placeholders": { - "appname": { - "content": "$1", - "example": "permafacts.arweave.dev" - } - } + "message": "Allow these permissions for the application:", + "description": "Auth permissions label" }, "reset_allowance": { "message": "Reset allowance", diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index a0f48189..a6988cad 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -49,10 +49,6 @@ "message": "隐藏", "description": "Hide text" }, - "save": { - "message": "节省", - "description": "Save button text" - }, "add_wallet": { "message": "添加钱包", "description": "Add a wallet text" @@ -299,14 +295,6 @@ "message": "权限", "description": "Permissions title" }, - "edit_permissions": { - "message": "编辑权限", - "description": "Edit permissions title" - }, - "app_permissions": { - "message": "应用权限", - "description": "App permissions title" - }, "allowance": { "message": "额度", "description": "Allowance title" @@ -937,27 +925,9 @@ "message": "连接", "description": "Connect button text" }, - "always_allow": { - "message": "始终允许", - "description": "Always allow button" - }, - "always_ask_permission": { - "message": "始终询问权限", - "description": "Always allow button" - }, - "allow_selected_permissions": { - "message": "允许选择的权限", - "description": "Always allow button" - }, "allow_these_permissions": { - "message": "$APPNAME$ 想要使用以下权限连接到您的钱包", - "description": "Auth permissions label", - "placeholders": { - "appname": { - "content": "$1", - "example": "permafacts.arweave.dev" - } - } + "message": "允许这些权限给应用程序:", + "description": "Auth permissions label" }, "reset_allowance": { "message": "重置额度", diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 26a7336c..22fcbfbd 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,10 +1,4 @@ -import { - useCallback, - useEffect, - useMemo, - useState, - type HTMLProps -} from "react"; +import { useEffect, useMemo, useState, type HTMLProps } from "react"; import styled from "styled-components"; export const Checkbox = ({ @@ -16,15 +10,18 @@ export const Checkbox = ({ const [state, setState] = useState(checked); const effectiveId = useMemo(() => id || generateUniqueId(), []); - const toggle = useCallback(async () => { - setState((prevState) => { - const newState = !prevState; - if (onChange) { - onChange(newState); - } - return newState; + async function toggle() { + let newVal = state; + + setState((val) => { + newVal = !val; + return newVal; }); - }, [onChange]); + + if (onChange) { + await onChange(newVal); + } + } useEffect(() => setState(checked), [checked]); diff --git a/src/components/auth/App.tsx b/src/components/auth/App.tsx index fd123ef0..1facd10f 100644 --- a/src/components/auth/App.tsx +++ b/src/components/auth/App.tsx @@ -2,8 +2,7 @@ import { type DisplayTheme, Section, Spacer, - Text, - ListItem + Text } from "@arconnect/components"; import { defaultGateway, type Gateway } from "~gateways/gateway"; import { useTheme as useDisplayTheme } from "~utils/theme"; @@ -23,8 +22,7 @@ export default function App({ appName, appUrl, gateway, - allowance, - showTitle = true + allowance }: Props) { // allowance spent in AR const spent = useMemo(() => { @@ -52,28 +50,16 @@ export default function App({ return ( <> - {showTitle && ( - <> - - - - - - )} + + + + - - {/* + {!appIcon && } @@ -105,7 +91,7 @@ export default function App({ {" AR"} )} - */} + ); @@ -117,9 +103,12 @@ const SidePaddingSection = styled(Section)` `; const Wrapper = styled.div<{ displayTheme: DisplayTheme }>` - border-radius: 10px; - padding-top: 1rem; - padding-bottom: 1rem; + background-color: rgb( + ${(props) => + props.displayTheme === "light" ? "0, 0, 0" : props.theme.cardBackground} + ); + border-radius: 27px; + padding: 1rem; display: flex; align-items: center; justify-content: space-between; @@ -175,5 +164,4 @@ interface Props { appUrl: string; gateway?: Gateway; allowance?: Allowance; - showTitle?: boolean; } diff --git a/src/components/auth/Permissions.tsx b/src/components/auth/Permissions.tsx deleted file mode 100644 index ce640e4f..00000000 --- a/src/components/auth/Permissions.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { ButtonV2, Section, Text } from "@arconnect/components"; -import browser from "webextension-polyfill"; -import styled from "styled-components"; -import { permissionData, type PermissionType } from "~applications/permissions"; -import Checkbox from "~components/Checkbox"; -import { useEffect, useState } from "react"; - -type PermissionsProps = { - requestedPermissions: PermissionType[]; - update: (updatedPermissions: PermissionType[]) => void; - closeEdit: (setEdit: boolean) => void; -}; - -export default function Permissions({ - requestedPermissions, - update, - closeEdit -}: PermissionsProps) { - const [permissions, setPermissions] = useState>( - new Map() - ); - - useEffect(() => { - setPermissions( - new Map(requestedPermissions.map((permission) => [permission, true])) - ); - }, []); - - return ( - -
-
- {browser.i18n.getMessage("permissions")} - - {Object.keys(permissionData).map( - (permissionName: PermissionType, i) => { - let formattedPermissionName = permissionName - .split("_") - .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) - .join(" "); - - if (permissionName === "SIGNATURE") { - formattedPermissionName = "Sign Data"; - } - - return ( -
- - { - const updated = new Map(permissions); - updated.set(permissionName, checked); - setPermissions(updated); - }} - checked={requestedPermissions.includes(permissionName)} - /> -
- - {formattedPermissionName} - - - {browser.i18n.getMessage( - permissionData[permissionName] - )} - -
-
-
- ); - } - )} -
-
-
-
- { - const updatedPermissions = Array.from(permissions.entries()) - .filter(([, value]) => value) - .map(([key]) => key); - update(updatedPermissions); - closeEdit(false); - }} - > - {browser.i18n.getMessage("save")} - -
-
- ); -} - -const Wrapper = styled.div` - display: flex; - width: 100vw; - flex-direction: column; - height: calc(100vh - 163px); - justify-content: space-between; -`; - -const Title = styled(Text).attrs({ - heading: true -})` - margin-bottom: 0.75em; - font-size: 1.125rem; -`; - -const PermissionsWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10px; -`; - -const Permission = styled.div` - display: flex; - align-items: center; - gap: 8px; -`; - -export const PermissionDescription = styled(Text).attrs({ - noMargin: true -})` - font-size: 0.625rem; - font-weight: 500; -`; - -export const PermissionTitle = styled(Text).attrs({ - noMargin: true, - heading: true -})` - font-size: 0.875rem; - font-weight: 500; -`; diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index 1ee48614..e513ee08 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -26,17 +26,12 @@ import WalletSwitcher from "~components/popup/WalletSwitcher"; import Wrapper from "~components/auth/Wrapper"; import browser from "webextension-polyfill"; import Label from "~components/auth/Label"; +import Head from "~components/popup/Head"; import App from "~components/auth/App"; import styled from "styled-components"; import { EventType, trackEvent } from "~utils/analytics"; import Application from "~applications/application"; import { defaultGateway, type Gateway } from "~gateways/gateway"; -import HeadV2 from "~components/popup/HeadV2"; -import { CheckIcon, CloseIcon } from "@iconicicons/react"; -import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; -import { defaultAllowance } from "~applications/allowance"; -import Arweave from "arweave"; -import Permissions from "../../components/auth/Permissions"; export default function Connect() { // active address @@ -45,16 +40,12 @@ export default function Connect() { instance: ExtensionStorage }); - const arweave = new Arweave(defaultGateway); - // wallet switcher open const [switcherOpen, setSwitcherOpen] = useState(false); // page const [page, setPage] = useState<"unlock" | "permissions">("unlock"); - const allowanceInput = useInput(); - // connect params const params = useAuthParams<{ url: string; @@ -82,12 +73,6 @@ export default function Connect() { PermissionType[] >([]); - // allowance for permissions - const [allowanceEnabled, setAllowanceEnabled] = useState(true); - - // state management for edit - const [edit, setEdit] = useState(false); - useEffect(() => { (async () => { if (!params) return; @@ -108,16 +93,9 @@ export default function Connect() { setRequestedPermissions( requested.filter((p) => Object.keys(permissionData).includes(p)) ); - setRequetedPermCopy( - requested.filter((p) => Object.keys(permissionData).includes(p)) - ); })(); }, [params]); - const [requestedPermCopy, setRequetedPermCopy] = useState( - [] - ); - // permissions to add const [permissions, setPermissions] = useState([]); @@ -169,14 +147,6 @@ export default function Connect() { permissions, name: appData.name, logo: appData.logo, - allowance: { - enabled: allowanceEnabled, - limit: - allowanceEnabled && allowanceInput.state - ? arweave.ar.arToWinston(allowanceInput.state) - : defaultAllowance.limit, - spent: "0" // in winstons - }, // TODO: wayfinder gateway: params.gateway || defaultGateway }); @@ -199,33 +169,23 @@ export default function Connect() { closeWindow(); } - useEffect(() => { - allowanceInput.setState(arweave.ar.winstonToAr(defaultAllowance.limit)); - }, []); - - const removedPermissions = useMemo(() => { - return requestedPermCopy.filter( - (permission) => !requestedPermissions.includes(permission) - ); - }, [requestedPermCopy, requestedPermissions]); - return (
- setEdit(false) : cancel} + back={cancel} /> + - + {page === "unlock" && ( @@ -271,128 +231,62 @@ export default function Connect() { )} {page === "permissions" && ( - <> - {!edit ? ( - -
- + +
+ + {browser.i18n.getMessage("allow_these_permissions")} + + {requestedPermissions.map((permission, i) => ( +
+ + setPermissions((val) => { + if (checked && val.includes(permission)) return val; + if (!checked && !val.includes(permission)) + return val; + if (checked && !val.includes(permission)) { + return [...val, permission]; + } + if (!checked && val.includes(permission)) { + return val.filter((p) => p !== permission); + } + }) + } + > {browser.i18n.getMessage( - "allow_these_permissions", - appData.name || appUrl + permissionData[permission.toUpperCase()] )} - - {params.url} - - - - {browser.i18n.getMessage("app_permissions")} - - { - setEdit(!edit); - }} - > - {browser.i18n.getMessage("edit_permissions")} - - - - {requestedPermissions.map((permission, i) => ( - - - - {browser.i18n.getMessage( - permissionData[permission.toUpperCase()] - )} - - - ))} - {requestedPermCopy - .filter( - (permission) => - !requestedPermissions.includes(permission) - ) - .map((permission, i) => ( - - - - {browser.i18n.getMessage( - permissionData[permission.toUpperCase()] - )} - - - ))} - - -
{browser.i18n.getMessage("allowance")}
- -
- {allowanceEnabled && ( - - AR} - type="number" - {...allowanceInput.bindings} - /> - +
+ {i !== requestedPermissions.length - 1 && ( + )} -
-
- ) : ( - <> - - - )} - +
+ ))} + + )}
- {!edit && ( -
- <> - { - if (page === "unlock") { - await unlock(); - } else { - await connect(); - } - }} - > - {browser.i18n.getMessage( - page === "unlock" - ? "sign_in" - : removedPermissions.length > 0 - ? "allow_selected_permissions" - : "always_allow" - )} - - - - {browser.i18n.getMessage( - page === "unlock" ? "cancel" : "always_ask_permission" - )} - - -
- )} +
+ { + if (page === "unlock") { + await unlock(); + } else { + await connect(); + } + }} + > + {browser.i18n.getMessage(page === "unlock" ? "sign_in" : "connect")} + + + + {browser.i18n.getMessage("cancel")} + +
); } @@ -401,23 +295,6 @@ const WalletSelectWrapper = styled.div` position: relative; `; -const StyledPermissions = styled.div` - padding-bottom: 1rem; -`; - -const Permission = styled.div` - margin: 0; - align-items: center; - display: flex; - gap: 8px; -`; - -const PermissionsTitle = styled.div` - display: flex; - width: 100%; - justify-content: space-between; -`; - const SelectIcon = styled(ChevronDownIcon)` font-size: 1rem; width: 1.375rem; @@ -426,65 +303,6 @@ const SelectIcon = styled(ChevronDownIcon)` transition: all 0.23s ease-in-out; `; -const Description = styled(Text)<{ alt?: boolean }>` - color: ${(props) => - props.alt ? `rgb(${props.theme.theme})` : props.theme.primaryTextv2}; - margin-bottom: 4px; - ${(props) => - props.alt && - ` - cursor: pointer; - `} -`; -const Url = styled(Text)` - color: ${(props) => props.theme.secondaryTextv2}; - font-size: 12px; -`; - -const StyledCheckIcon = styled(CheckIcon)` - width: 17px; - height: 17px; - min-width: 17px; - min-height: 17px; - flex-shrink: 0; - color: rgba(20, 209, 16, 1); -`; - -const StyledCloseIcon = styled(CloseIcon)` - width: 17px; - height: 17px; - min-width: 17px; - min-height: 17px; - flex-shrink: 0; - color: ${(props) => props.theme.fail}; -`; - -const AllowanceInput = styled(InputV2)` - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } -`; - -const PermissionItem = styled(Text)` - color: ${(props) => props.theme.primaryTextv2}; - margin: 0; - font-size: 14px; -`; - -const AllowanceSection = styled.div` - display: flex; - justify-content: space-between; - align-items: flex-end; - padding-top: 18px; - div { - color: ${(props) => props.theme.primaryTextv2}; - font-size: 18px; - font-weight: 00; - } -`; - const WalletSelect = styled(Card)<{ open: boolean }>` position: relative; display: flex; From 42df19038fd2aaa8948adfe4924afa92dea2a582 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Fri, 12 Jul 2024 12:47:52 -0700 Subject: [PATCH 29/33] fix: messages --- assets/_locales/en/messages.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index d1715162..da4cd9e7 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -49,7 +49,6 @@ "message": "Hide", "description": "Hide text" }, -<<<<<<< HEAD "close": { "message": "Close", "description": "Close button text" @@ -62,8 +61,6 @@ "message": "Save", "description": "Save button text" }, -======= ->>>>>>> a9e4eb0e7190179ffb372143a5cf6bf13d9e41a9 "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" From 93f1bf3f2fe7da6a6d127677901865576f78fff0 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Fri, 12 Jul 2024 17:52:12 -0700 Subject: [PATCH 30/33] chore: removed console logs --- src/tokens/aoTokens/ao.ts | 2 +- src/utils/auth.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index 9e296881..a79f5cf1 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -485,7 +485,7 @@ export const aoTokensCacheHandler = async (alarmInfo?: Alarms.Alarm) => { } } await ExtensionStorage.set("ao_tokens", updatedTokens); -} +}; export const sendAoTransferKeystone = async ( ao: AoInstance, diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 03248ed9..1ef31d14 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -8,14 +8,11 @@ import type { AuthResult } from "shim"; * Hook to parse auth params from the url */ export function useAuthParams() { - console.log("useAuthParams"); const [params, setParams] = useState(); // fetch params useEffect(() => { - console.log("here", window.location.href); const urlParams = window.location.href.split("?"); - console.log("here", urlParams); const params = objectFromUrlParams( urlParams[urlParams.length - 1].replace(window.location.hash, "") ); From ed8039c5b9a566a448373038e15aad45de4b0320 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Mon, 15 Jul 2024 10:26:15 -0700 Subject: [PATCH 31/33] fix: confirm button stuck on disabeled --- src/routes/popup/send/confirm.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index f2e53ab5..a32e82f1 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -541,7 +541,10 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { useEffect(() => { (async () => { setIsLoading(true); - if (!recipient?.address) return; + if (!recipient?.address) { + setIsLoading(false); + return; + } // get the tx from storage const prepared = await prepare(recipient.address); @@ -554,7 +557,10 @@ export default function Confirm({ tokenID, qty, subscription }: Props) { // check if the current wallet // is a hardware wallet - if (wallet?.type !== "hardware") return; + if (wallet?.type !== "hardware") { + setIsLoading(false); + return; + } if (isAo) { try { From e96cfce27c285e0e3064793742ff90a0c00ea332 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Mon, 15 Jul 2024 10:47:45 -0700 Subject: [PATCH 32/33] fix: arc 427 delay error messaging on send screen --- src/tokens/aoTokens/ao.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index a79f5cf1..53e7b50a 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -102,7 +102,7 @@ export function useAoTokens( () => tokens.map((token) => ({ ...token, - balance: balances.find((bal) => bal.id === token.id)?.balance ?? null + balance: balances.find((bal) => bal.id === token.id)?.balance })), [tokens, balances] ); From 2042aa71b51957b853e5a5552c1799f3e3b7cc7d Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Mon, 15 Jul 2024 10:53:34 -0700 Subject: [PATCH 33/33] fix: arc-426 update messaging --- assets/_locales/en/messages.json | 4 ++-- assets/_locales/zh_CN/messages.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index da4cd9e7..0b8d8cf9 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1937,11 +1937,11 @@ "description": "Popup description about ao token transfer learn more" }, "ao_degraded": { - "message": "AO Token Process Network Degraded.", + "message": "Unable to connect to AO Token Process", "description": "ao degraded title text" }, "ao_degraded_description": { - "message": "AO token process will be available when
the network issues are resolved.", + "message": "AO balance will be available when
network issues are resolved.", "description": "ao degraded description text" }, "network_issue": { diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index e892a7ec..770aa7c5 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1935,11 +1935,11 @@ "description": "Popup description about ao token transfer learn more" }, "ao_degraded": { - "message": "AO 代币处理网络降级。", + "message": "无法连接到 AO 令牌进程", "description": "ao degraded title text" }, "ao_degraded_description": { - "message": "网络问题解决后,AO 代币处理将可用。", + "message": "网络问题解决后,AO 余额将可用。", "description": "ao degraded description text" }, "network_issue": {