diff --git a/.env.example b/.env.example deleted file mode 100644 index aa406416..00000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -PLASMO_PUBLIC_APP_TYPE=extension diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 171e2933..5f20fd14 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1,4 +1,20 @@ { + "fallback": { + "message": "Oops, something isn't right. Try again later.", + "description": "Fallback error message" + }, + "reload": { + "message": "Reload", + "description": "Reload button text" + }, + "commonErrorInfo": { + "message": "Error Info", + "description": "Additional Fallback error message" + }, + "loading": { + "message": "Loading...", + "description": "Loading text" + }, "extensionDescription": { "message": "Secure wallet management for Arweave", "description": "Extension description" @@ -974,7 +990,7 @@ "description": "Wallet label" }, "enter_your_password": { - "message": "Enter your password...", + "message": "Enter your password", "description": "Enter password placeholder" }, "connect": { @@ -1345,13 +1361,31 @@ "message": "Sign items", "description": "Batch sign message popup title" }, + "titles_decrypt": { + "message": "Decrypt", + "description": "Request decrypt authorization password" + }, + "decrypt_description": { + "message": "$APPNAME$ wants to decrypt and access the following data:", + "description": "Description for decrypting some data", + "placeholders": { + "appname": { + "content": "$1", + "example": "permafacts.arweave.dev" + } + } + }, + "decrypt_authorize": { + "message": "Authorize", + "description": "Authorize button label" + }, "titles_signature": { "message": "Sign message", "description": "Sign message popup title" }, "sign_data_description": { "message": "$APPNAME$ wants to sign a transaction. Review the details below.", - "description": "Desription for signing an item containing a transfer", + "description": "Description for signing an item containing a transfer", "placeholders": { "appname": { "content": "$1", @@ -2310,6 +2344,10 @@ "message": "Adding token...", "description": "Loading message for AuthRequest of type token" }, + "decryptRequestLoading": { + "message": "Decrypting...", + "description": "Loading message for AuthRequest of type decrypt" + }, "signRequestLoading": { "message": "Signing...", "description": "Loading message for AuthRequest of type sign" @@ -2383,5 +2421,93 @@ "example": "10" } } + }, + "connect_to_app": { + "message": "Connect to $APPNAME$", + "description": "Connect to app title", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "always_ask": { + "message": "Always ask", + "description": "Always ask" + }, + "always_ask_description": { + "message": "Review and confirm every transaction and message.", + "description": "Always ask description" + }, + "ask_when_spending": { + "message": "Ask when spending", + "description": "Ask when spending" + }, + "ask_when_spending_description": { + "message": "Review and confirm transactions only when assets are being transferred.", + "description": "Ask when spending description" + }, + "auto_confirm": { + "message": "Auto-confirm", + "description": "Auto-confirm" + }, + "auto_confirm_description": { + "message": "Automatically sign every transaction and message.", + "description": "Auto-confirm description" + }, + "custom_permissions": { + "message": "Custom permissions", + "description": "Custom permissions" + }, + "set_custom_permissions": { + "message": "Set custom permissions", + "description": "Set custom permissions" + }, + "permission_settings": { + "message": "Permission Settings", + "description": "Permission settings title" + }, + "select_account": { + "message": "Select an account to connect to $APPNAME$", + "description": "Select account title", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "review": { + "message": "Review", + "description": "Review title" + }, + "connect_request_1": { + "message": "$APPNAME$ wants to connect with", + "description": "Connect request 1", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "connect_request_2": { + "message": "with the following permissions", + "description": "Connect request 2" + }, + "confirm_permissions": { + "message": "Confirm permissions for $APPNAME$", + "description": "Confirm permissions for app", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "change": { + "message": "Change", + "description": "Change text" } } diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index c931c6d0..b54e4a3b 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1,4 +1,20 @@ { + "fallback": { + "message": "哎呀,有些不对劲,稍后再试。", + "description": "Fallback error message" + }, + "reload": { + "message": "重新加载", + "description": "Reload button text" + }, + "commonErrorInfo": { + "message": "错误信息", + "description": "Additional Fallback error message" + }, + "loading": { + "message": "加载中...", + "description": "Loading text" + }, "extensionDescription": { "message": "Arweave 的安全钱包管理", "description": "Extension description" @@ -966,7 +982,7 @@ "description": "Wallet label" }, "enter_your_password": { - "message": "输入您的密码...", + "message": "输入您的密码", "description": "Enter password placeholder" }, "connect": { @@ -1333,6 +1349,24 @@ "message": "批量签署项目", "description": "批量签署消息弹出标题" }, + "titles_decrypt": { + "message": "Decrypt", + "description": "Request decrypt authorization password" + }, + "decrypt_description": { + "message": "$APPNAME$ wants to decrypt and access the following data:", + "description": "Description for decrypting some data", + "placeholders": { + "appname": { + "content": "$1", + "example": "permafacts.arweave.dev" + } + } + }, + "decrypt_authorize": { + "message": "授权", + "description": "Authorize button label" + }, "titles_signature": { "message": "签署消息", "description": "Sign message popup title" @@ -2296,6 +2330,10 @@ "message": "Adding token...", "description": "Loading message for AuthRequest of type token" }, + "decryptRequestLoading": { + "message": "Decrypting...", + "description": "Loading message for AuthRequest of type decrypt" + }, "signRequestLoading": { "message": "Signing...", "description": "Loading message for AuthRequest of type sign" @@ -2369,5 +2407,93 @@ "example": "10" } } + }, + "connect_to_app": { + "message": "连接到 $APPNAME$", + "description": "连接到应用标题", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "always_ask": { + "message": "始终询问", + "description": "Always ask" + }, + "always_ask_description": { + "message": "审核并确认每笔交易和消息。", + "description": "Always ask description" + }, + "ask_when_spending": { + "message": "花费时询问", + "description": "Ask when spending" + }, + "ask_when_spending_description": { + "message": "仅在资产被转移时审核并确认交易。", + "description": "Ask when spending description" + }, + "auto_confirm": { + "message": "自动确认", + "description": "Auto-confirm" + }, + "auto_confirm_description": { + "message": "自动签署每笔交易和消息。", + "description": "Auto-confirm description" + }, + "custom_permissions": { + "message": "自定义权限", + "description": "Custom permissions" + }, + "set_custom_permissions": { + "message": "设置自定义权限", + "description": "Set custom permissions" + }, + "permission_settings": { + "message": "权限设置", + "description": "Permission settings title" + }, + "select_account": { + "message": "选择一个账户连接到 $APPNAME$", + "description": "Select account title", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "review": { + "message": "审核", + "description": "Review title" + }, + "connect_request_1": { + "message": "$APPNAME$ 想要连接到", + "description": "Connect request 1", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "connect_request_2": { + "message": "以下权限", + "description": "Connect request 2" + }, + "confirm_permissions": { + "message": "确认 $APPNAME$ 的权限", + "description": "Confirm permissions for app", + "placeholders": { + "appname": { + "content": "$1", + "example": "ArConnect" + } + } + }, + "change": { + "message": "更换", + "description": "Change text" } } diff --git a/assets/ecosystem/arconnect.svg b/assets/ecosystem/arconnect.svg new file mode 100644 index 00000000..cbcfde43 --- /dev/null +++ b/assets/ecosystem/arconnect.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/ecosystem/liquidops.svg b/assets/ecosystem/liquidops.svg index 614c4e21..3cffbe56 100644 --- a/assets/ecosystem/liquidops.svg +++ b/assets/ecosystem/liquidops.svg @@ -1,23 +1,20 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/api/background/handlers/browser/install/install.handler.ts b/src/api/background/handlers/browser/install/install.handler.ts index c7a39a87..b3093364 100644 --- a/src/api/background/handlers/browser/install/install.handler.ts +++ b/src/api/background/handlers/browser/install/install.handler.ts @@ -5,6 +5,7 @@ import { initializeARBalanceMonitor } from "~utils/analytics"; import { updateAoToken } from "~utils/ao_import"; import { handleGatewayUpdateAlarm } from "~api/background/handlers/alarms/gateway-update/gateway-update-alarm.handler"; import { openOrSelectWelcomePage } from "~wallets"; +import { resetAllPermissions } from "~applications/permissions"; /** * On extension installed event handler @@ -15,6 +16,11 @@ export async function handleInstall(details: Runtime.OnInstalledDetailsType) { openOrSelectWelcomePage(true); } + if (details.reason === "update") { + // reset permissions + await resetAllPermissions(); + } + // init monthly AR await initializeARBalanceMonitor(); diff --git a/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts b/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts index 0c2fc890..c31b490e 100644 --- a/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts +++ b/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts @@ -4,7 +4,7 @@ import { ARCONNECT_PRINTER_ID } from "~api/background/handlers/browser/printer/p * Printer capabilities request callback type */ type PrinterInfoCallback = ( - capabilities: chrome.printerProvider.PrinterCapabilities + capabilities: chrome.printerProvider.PrinterCapabilities["capabilities"] ) => void; /** @@ -20,55 +20,53 @@ export function handleGetCapabilities( // mimic a regular printer's capabilities callback({ - capabilities: { - version: "1.0", - printer: { - supported_content_type: [ - { content_type: "application/pdf" }, - { content_type: "image/pwg-raster" } - ], - color: { - option: [ - { type: "STANDARD_COLOR", is_default: true }, - { type: "STANDARD_MONOCHROME" } - ] - }, - copies: { - default_copies: 1, - max_copies: 100 - }, - media_size: { - option: [ - { - name: "ISO_A4", - width_microns: 210000, - height_microns: 297000, - is_default: true - }, - { - name: "NA_LETTER", - width_microns: 215900, - height_microns: 279400 - } - ] - }, - page_orientation: { - option: [ - { - type: "PORTRAIT", - is_default: true - }, - { type: "LANDSCAPE" }, - { type: "AUTO" } - ] - }, - duplex: { - option: [ - { type: "NO_DUPLEX", is_default: true }, - { type: "LONG_EDGE" }, - { type: "SHORT_EDGE" } - ] - } + version: "1.0", + printer: { + supported_content_type: [ + { content_type: "application/pdf" }, + { content_type: "image/pwg-raster" } + ], + color: { + option: [ + { type: "STANDARD_COLOR", is_default: true }, + { type: "STANDARD_MONOCHROME" } + ] + }, + copies: { + default_copies: 1, + max_copies: 100 + }, + media_size: { + option: [ + { + name: "ISO_A4", + width_microns: 210000, + height_microns: 297000, + is_default: true + }, + { + name: "NA_LETTER", + width_microns: 215900, + height_microns: 279400 + } + ] + }, + page_orientation: { + option: [ + { + type: "PORTRAIT", + is_default: true + }, + { type: "LANDSCAPE" }, + { type: "AUTO" } + ] + }, + duplex: { + option: [ + { type: "NO_DUPLEX", is_default: true }, + { type: "LONG_EDGE" }, + { type: "SHORT_EDGE" } + ] } } }); diff --git a/src/api/modules/decrypt/decrypt.background.ts b/src/api/modules/decrypt/decrypt.background.ts index 57a1d1e8..e0303c84 100644 --- a/src/api/modules/decrypt/decrypt.background.ts +++ b/src/api/modules/decrypt/decrypt.background.ts @@ -10,6 +10,7 @@ import { isLocalWallet, isRawArrayBuffer } from "~utils/assertions"; +import { requestUserAuthorization } from "~utils/auth/auth.utils"; const background: BackgroundModuleFunction = async ( appData, @@ -41,6 +42,8 @@ const background: BackgroundModuleFunction = async ( // remove wallet from memory freeDecryptedWallet(decryptedWallet.keyfile); + let decryptedData: Uint8Array; + if (options.algorithm) { // validate isLegacyEncryptionOptions(options); @@ -78,7 +81,7 @@ const background: BackgroundModuleFunction = async ( ); // decrypt data - const res = await arweave.crypto.decrypt( + decryptedData = await arweave.crypto.decrypt( encryptedData, new Uint8Array(decryptedKey), options.salt @@ -91,10 +94,11 @@ const background: BackgroundModuleFunction = async ( if (options.salt) { const rawSalt = new TextEncoder().encode(options.salt); - return res.slice(0, res.length - rawSalt.length); + decryptedData = decryptedData.slice( + 0, + decryptedData.length - rawSalt.length + ); } - - return res; } else if (options.name) { // validate isEncryptionAlgorithm(options); @@ -110,18 +114,30 @@ const background: BackgroundModuleFunction = async ( false, ["decrypt"] ); + const decrypted = await crypto.subtle.decrypt(options, key, data); // remove wallet from memory freeDecryptedWallet(privateKey); - return new Uint8Array(decrypted); + decryptedData = new Uint8Array(decrypted); } else { // remove wallet from memory freeDecryptedWallet(privateKey); throw new Error("Invalid options passed", options); } + + // request "decrypt" popup + await requestUserAuthorization( + { + type: "decrypt", + message: Object.values(decryptedData) + }, + appData + ); + + return decryptedData; }; export default background; diff --git a/src/api/modules/dispatch/allowance.ts b/src/api/modules/dispatch/allowance.ts index 0f5c78d5..29f3cb69 100644 --- a/src/api/modules/dispatch/allowance.ts +++ b/src/api/modules/dispatch/allowance.ts @@ -36,9 +36,9 @@ export async function ensureAllowanceDispatch( ); } - if (allowance.enabled) { - await allowanceAuth(appData, allowance, price, alwaysAsk); - } + // if (allowance.enabled) { + // await allowanceAuth(appData, allowance, price, alwaysAsk); + // } } catch (e) { freeDecryptedWallet(keyfile); throw new Error(e?.message || e); diff --git a/src/api/modules/dispatch/dispatch.background.ts b/src/api/modules/dispatch/dispatch.background.ts index f506dbf4..464c4302 100644 --- a/src/api/modules/dispatch/dispatch.background.ts +++ b/src/api/modules/dispatch/dispatch.background.ts @@ -15,6 +15,9 @@ import Arweave from "arweave"; import { ensureAllowanceDispatch } from "./allowance"; import { updateAllowance } from "../sign/allowance"; import BigNumber from "bignumber.js"; +import { isError } from "~utils/error/error.utils"; +import { ERR_MSG_USER_CANCELLED_AUTH } from "~utils/auth/auth.constants"; +import { checkIfUserNeedsToSign } from "../sign/sign_policy"; type ReturnType = { arConfetti: string | false; @@ -70,7 +73,13 @@ const background: BackgroundModuleFunction = async ( const allowance = await app.getAllowance(); // always ask - const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); + // const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); + const signPolicy = await app.getSignPolicy(); + const alwaysAsk = checkIfUserNeedsToSign( + signPolicy, + transaction, + decryptedWallet?.type + ); // attempt to create a bundle try { @@ -95,7 +104,7 @@ const background: BackgroundModuleFunction = async ( await uploadDataToTurbo(dataEntry, await app.getBundler()); // update allowance spent amount (in winstons) - await updateAllowance(appData.url, price); + // await updateAllowance(appData.url, price); // show notification await signNotification(0, dataEntry.id, appData.url, "dispatch"); @@ -110,7 +119,14 @@ const background: BackgroundModuleFunction = async ( type: "BUNDLED" } }; - } catch { + } catch (err) { + if (isError(err) && err.message === ERR_MSG_USER_CANCELLED_AUTH) { + throw err; + } + + // TODO: If there's an error in the first request, the previous (already accepted) AuthRequest's UI should probably + // reflect that. Maybe we could even reuse the same AuthRequest item instead of creating a separated one. + // sign & post if there is something wrong with turbo // add ArConnect tags to the tx object for (const arcTag of signedTxTags) { @@ -138,7 +154,7 @@ const background: BackgroundModuleFunction = async ( } // update allowance spent amount (in winstons) - await updateAllowance(appData.url, price); + // await updateAllowance(appData.url, price); // show notification await signNotification(price, transaction.id, appData.url); diff --git a/src/api/modules/sign/sign.background.ts b/src/api/modules/sign/sign.background.ts index 6d750401..57ef992c 100644 --- a/src/api/modules/sign/sign.background.ts +++ b/src/api/modules/sign/sign.background.ts @@ -19,6 +19,7 @@ import browser from "webextension-polyfill"; import Arweave from "arweave"; import { EventType, trackDirect } from "~utils/analytics"; import BigNumber from "bignumber.js"; +import { checkIfUserNeedsToSign } from "./sign_policy"; const background: BackgroundModuleFunction = async ( appData, @@ -76,15 +77,23 @@ const background: BackgroundModuleFunction = async ( const price = BigNumber(transaction.reward).plus(transaction.quantity); // get allowance - const allowance = await getAllowance(appData.url); + // const allowance = await getAllowance(appData.url); // always ask - const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); + // const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); - // check if there is an allowance limit, if there is we need to check allowance - // if alwaysAsk is true, then we'll need to signAuth popup - // if allowance is disabled, proceed with signing - if (alwaysAsk || activeWallet.type === "hardware") { + const signPolicy = await app.getSignPolicy(); + + // check if user needs to sign + const userNeedsToSign = checkIfUserNeedsToSign( + signPolicy, + transaction, + activeWallet.type + ); + + // check if user needs to sign + // if userNeedsToSign is true, then we'll need to signAuth popup + if (userNeedsToSign) { // get address of keyfile const addr = activeWallet.type === "local" @@ -109,16 +118,16 @@ const background: BackgroundModuleFunction = async ( throw new Error("User failed to sign the transaction manually"); } - } else if (allowance.enabled && activeWallet.type === "local") { - // authenticate user if the allowance - // limit is reached - try { - await allowanceAuth(appData, allowance, price, alwaysAsk); - } catch (e) { - freeDecryptedWallet(keyfile); - throw new Error(e?.message || e); - } - } + } // else if (allowance.enabled && activeWallet.type === "local") { + // // authenticate user if the allowance + // // limit is reached + // try { + // await allowanceAuth(appData, allowance, price, alwaysAsk); + // } catch (e) { + // freeDecryptedWallet(keyfile); + // throw new Error(e?.message || e); + // } + // } // sign the transaction if local wallet if (activeWallet.type === "local") { @@ -133,7 +142,7 @@ const background: BackgroundModuleFunction = async ( await signNotification(price, transaction.id, appData.url); // update allowance spent amount (in winstons) - await updateAllowance(appData.url, price); + // await updateAllowance(appData.url, price); // de-construct the transaction: // remove "tags" and "data", so we don't have to diff --git a/src/api/modules/sign/sign_policy.ts b/src/api/modules/sign/sign_policy.ts new file mode 100644 index 00000000..5d2cdf4d --- /dev/null +++ b/src/api/modules/sign/sign_policy.ts @@ -0,0 +1,71 @@ +import Transaction from "arweave/web/lib/transaction"; +import BigNumber from "bignumber.js"; +import { DataItem } from "warp-arbundles"; +import type { RawDataItem } from "../sign_data_item/types"; + +export type SignPolicy = "always_ask" | "ask_when_spending" | "auto_confirm"; + +export function checkIfUserNeedsToSign( + signPolicy: SignPolicy, + transaction: Transaction | DataItem | RawDataItem, + walletType: "local" | "hardware" = "local" +) { + try { + // Hardware wallets always need manual signing + if (walletType === "hardware") return true; + + switch (signPolicy) { + case "always_ask": + // Always require manual authorization + return true; + + case "ask_when_spending": + const tags = transaction?.tags || []; + let isAo = false, + isTransfer = false, + hasQuantity = false; + + for (const tag of tags) { + switch (tag.name) { + case "Data-Protocol": + isAo = tag.value === "ao"; + break; + case "Action": + isTransfer = tag.value === "Transfer"; + break; + case "Quantity": + hasQuantity = true; + break; + } + if (isAo && isTransfer && hasQuantity) break; + } + + if (isAo && (isTransfer || hasQuantity)) { + return true; + } + + // Require auth if transaction spends AR (quantity > 0) or has network fees + const quantity = + "quantity" in transaction + ? new BigNumber(transaction.quantity) + : new BigNumber(0); + + const reward = + "reward" in transaction + ? new BigNumber(transaction.reward) + : new BigNumber(0); + + return !quantity.isZero() || !reward.isZero(); + + case "auto_confirm": + // Never require manual authorization + return false; + + default: + // Default to always asking if policy is undefined + return true; + } + } catch { + return true; + } +} 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 88f9ab7c..332deede 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 @@ -4,16 +4,13 @@ import type { BackgroundModuleFunction } from "~api/background/background-module import { ArweaveSigner, createData } from "arbundles"; import Application from "~applications/application"; import { getActiveKeyfile, getActiveWallet } from "~wallets"; -import { - signAuth, - signAuthKeystone, - type AuthKeystoneData -} from "../sign/sign_auth"; +import { signAuthKeystone, type AuthKeystoneData } from "../sign/sign_auth"; import Arweave from "arweave"; import { requestUserAuthorization } from "../../../utils/auth/auth.utils"; import BigNumber from "bignumber.js"; import { createDataItem } from "~utils/data_item"; import { EventType, trackDirect } from "~utils/analytics"; +import { checkIfUserNeedsToSign } from "../sign/sign_policy"; const background: BackgroundModuleFunction = async ( appData, @@ -27,8 +24,8 @@ const background: BackgroundModuleFunction = async ( } const app = new Application(appData.url); - const allowance = await app.getAllowance(); - const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); + // const allowance = await app.getAllowance(); + // const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); let isTransferTx = false; let amount = "0"; @@ -59,26 +56,17 @@ const background: BackgroundModuleFunction = async ( throw new Error("Quantity must be a valid positive non-zero number."); } } - try { - await requestUserAuthorization( - { - type: "signDataItem", - data: dataItem - }, - appData - ); - } catch { - throw new Error("User rejected the sign data item request"); - } } // grab the user's keyfile const decryptedWallet = await getActiveKeyfile(appData); - // create app - - // create arweave client - const arweave = new Arweave(await app.getGatewayConfig()); + const signPolicy = await app.getSignPolicy(); + const alwaysAsk = checkIfUserNeedsToSign( + signPolicy, + dataItem, + decryptedWallet.type + ); // get options and data const { data, ...options } = dataItem; @@ -96,16 +84,12 @@ const background: BackgroundModuleFunction = async ( // allowance or sign auth try { if (alwaysAsk) { - // get address - const address = await arweave.wallets.jwkToAddress( - decryptedWallet.keyfile - ); - - await signAuth( - appData, - // @ts-expect-error - dataEntry.toJSON(), - address + await requestUserAuthorization( + { + type: "signDataItem", + data: dataItem + }, + appData ); } } catch (e) { diff --git a/src/api/modules/sign_message/sign_message.background.ts b/src/api/modules/sign_message/sign_message.background.ts index 08e3b4d5..292f8f65 100644 --- a/src/api/modules/sign_message/sign_message.background.ts +++ b/src/api/modules/sign_message/sign_message.background.ts @@ -8,6 +8,7 @@ import { } from "~utils/assertions"; import { signAuthKeystone, type AuthKeystoneData } from "../sign/sign_auth"; import Arweave from "arweave"; +import { requestUserAuthorization } from "~utils/auth/auth.utils"; const background: BackgroundModuleFunction = async ( appData, @@ -23,6 +24,14 @@ const background: BackgroundModuleFunction = async ( isArrayBuffer(dataToSign); + await requestUserAuthorization( + { + type: "signature", + message: data + }, + appData + ); + // hash the message const hash = await crypto.subtle.digest(options.hashAlgorithm, dataToSign); diff --git a/src/applications/application.ts b/src/applications/application.ts index f3eddacf..8d7c3da2 100644 --- a/src/applications/application.ts +++ b/src/applications/application.ts @@ -119,6 +119,15 @@ export default class Application { return permissions.length > 0; } + /** + * Check if the app is present in the storage + */ + async isAppPresent() { + const settings = await this.#getSettings(); + + return Object.keys(settings).length > 0; + } + /** * Gateway config for each individual app */ @@ -153,6 +162,17 @@ export default class Application { }; } + /** + * Sign policy for the app + */ + async getSignPolicy(): Promise< + "always_ask" | "ask_when_spending" | "auto_confirm" + > { + const settings = await this.#getSettings(); + + return settings.signPolicy || "always_ask"; + } + /** * Blocked from interacting with ArConnect */ @@ -204,6 +224,16 @@ export interface AppLogoInfo extends AppInfo { placeholder?: string; } +/** + * Sign policy for the app + * - **always_ask**: always ask the user to sign + * - **ask_when_spending**: ask the user to sign when user assets are being spent or has network fees to be paid else auto sign + * - **auto_confirm**: automatically sign every transactions + * + * @default "always_ask" + */ +export type SignPolicy = "always_ask" | "ask_when_spending" | "auto_confirm"; + /** * Params to add an app with */ @@ -214,4 +244,5 @@ export interface InitAppParams extends AppInfo { allowance?: Allowance; blocked?: boolean; bundler?: string; + signPolicy?: SignPolicy; } diff --git a/src/applications/permissions.ts b/src/applications/permissions.ts index 095dd724..a83fef95 100644 --- a/src/applications/permissions.ts +++ b/src/applications/permissions.ts @@ -1,3 +1,6 @@ +import { ExtensionStorage } from "~utils/storage"; +import Application from "./application"; + /** * ArConnect permissions */ @@ -47,3 +50,77 @@ export function getMissingPermissions( return missing; } + +const IS_PERMISSIONS_RESET = "is_permissions_reset"; + +// Add a memory flag to prevent multiple executions even within the same session +let isResetInProgress = false; + +/** + * Reset all permissions for all apps + */ +export const resetAllPermissions = async (): Promise => { + try { + const isPermissionsReset = await ExtensionStorage.get(IS_PERMISSIONS_RESET); + // Check both storage and memory flags + if (isPermissionsReset || isResetInProgress) { + return; + } + + // Set the in-progress flag + isResetInProgress = true; + + // Get and validate connected apps + const connectedApps = (await ExtensionStorage.get("apps")) || []; + if (!Array.isArray(connectedApps) || connectedApps.length === 0) { + await ExtensionStorage.set(IS_PERMISSIONS_RESET, true); + return; + } + + // Process apps in batches to prevent overwhelming the system + const BATCH_SIZE = 5; + const validApps = connectedApps.filter( + (appUrl): appUrl is string => + Boolean(appUrl) && typeof appUrl === "string" + ); + + for (let i = 0; i < validApps.length; i += BATCH_SIZE) { + const batch = validApps.slice(i, i + BATCH_SIZE); + const results = await Promise.allSettled( + batch.map(async (appUrl) => { + try { + const app = new Application(appUrl); + await app.updateSettings((val) => ({ + ...val, + permissions: [] + })); + } catch (error) { + console.error(`Failed to reset permissions for ${appUrl}:`, error); + throw error; + } + }) + ); + + // Log failures for this batch + results.forEach((result, index) => { + if (result.status === "rejected") { + console.error(`Failed to process app ${i + index}:`, result.reason); + } + }); + } + + // Mark as complete + await ExtensionStorage.set(IS_PERMISSIONS_RESET, true); + } catch (error) { + console.error("Error in resetAllPermissions:", error); + } finally { + // Always reset the in-progress flag + isResetInProgress = false; + } +}; + +export const signPolicyOptions = [ + "always_ask", + "ask_when_spending", + "auto_confirm" +] as const; diff --git a/src/components/HeadAuth.tsx b/src/components/HeadAuth.tsx index bd00b80f..959271dc 100644 --- a/src/components/HeadAuth.tsx +++ b/src/components/HeadAuth.tsx @@ -1,6 +1,6 @@ import type React from "react"; import { useEffect, useState } from "react"; -import styled from "styled-components"; +import styled, { type DefaultTheme } from "styled-components"; import type { AppInfo, AppLogoInfo } from "~applications/application"; import Application from "~applications/application"; import HeadV2 from "~components/popup/HeadV2"; @@ -13,12 +13,14 @@ export interface HeadAuthProps { title?: string; back?: () => void; appInfo?: AppInfo; + showHead?: boolean; } export const HeadAuth: React.FC = ({ title, back, - appInfo: appInfoProp = { name: "ArConnect" } + appInfo: appInfoProp = { name: "ArConnect" }, + showHead = true }) => { const [areLogsExpanded, setAreLogsExpanded] = useState(false); const { authRequests, currentAuthRequestIndex, setCurrentAuthRequestIndex } = @@ -29,13 +31,17 @@ export const HeadAuth: React.FC = ({ // fallback value: const { name: fallbackName, logo: fallbackLogo } = appInfoProp; - const { tabID = null, url = "" } = - authRequests[currentAuthRequestIndex] || {}; + const { + tabID = null, + url = "", + type + } = authRequests[currentAuthRequestIndex] || {}; + const isConnectType = type === "connect"; const [appLogoInfo, setAppLogoInfo] = useState(appInfoProp); useEffect(() => { async function loadAppInfo() { - if (!url) return; + if (!url || isConnectType) return; const app = new Application(url); const appInfo = await app.getAppData(); @@ -53,7 +59,7 @@ export const HeadAuth: React.FC = ({ } loadAppInfo(); - }, [url, fallbackName, fallbackLogo]); + }, [url, fallbackName, fallbackLogo, isConnectType]); const handleAppInfoClicked = tabID ? () => { @@ -67,14 +73,16 @@ export const HeadAuth: React.FC = ({ return ( <> - + {showHead && ( + + )} {process.env.NODE_ENV === "development" && authRequests.length > 0 ? ( @@ -137,34 +145,48 @@ const DivTransactionsList = styled.div` display: flex; gap: 8px; padding: 16px; - border-bottom: 1px solid rgb(31, 30, 47); + border-bottom: 1px solid + ${({ theme }) => + theme.displayTheme === "dark" ? "rgb(31, 30, 47)" : "rgb(224, 225, 208)"}; height: 12px; `; interface AuthRequestIndicatorProps { isCurrent: boolean; status: AuthRequestStatus; + theme: DefaultTheme; } -const colorsByStatus: Record = { - pending: "white", - accepted: "green", - rejected: "red", - aborted: "grey", - error: "red" +const colorsByStatus: Record< + AuthRequestStatus, + { light: string; dark: string } +> = { + pending: { light: "black", dark: "white" }, + accepted: { light: "green", dark: "green" }, + rejected: { light: "red", dark: "red" }, + aborted: { light: "grey", dark: "grey" }, + error: { light: "red", dark: "red" } }; function getAuthRequestButtonIndicatorBorderColor({ - status + status, + theme }: AuthRequestIndicatorProps) { - return colorsByStatus[status]; + return theme.displayTheme === "dark" + ? colorsByStatus[status].dark + : colorsByStatus[status].light; } function getAuthRequestButtonIndicatorBackgroundColor({ isCurrent, - status + status, + theme }: AuthRequestIndicatorProps) { - return isCurrent ? colorsByStatus[status] : "transparent"; + return isCurrent + ? theme.displayTheme === "dark" + ? colorsByStatus[status].dark + : colorsByStatus[status].light + : "transparent"; } const ButtonTransactionButton = styled.button` @@ -177,27 +199,34 @@ const ButtonTransactionButton = styled.button` `; const DivTransactionButtonSpacer = styled.button` - background: rgba(255, 255, 255, 0.125); + background: ${({ theme }) => + theme.displayTheme === "dark" + ? "rgba(255, 255, 255, 0.125)" + : "rgba(0, 0, 0, 0.125)"}; border-radius: 128px; flex: 1 0 auto; `; const ButtonExpandLogs = styled.button` - border: 2px solid white; + border: 2px solid + ${({ theme }) => (theme.displayTheme === "dark" ? "white" : "black")}; border-radius: 128px; width: 12px; `; const DivLogWrapper = styled.div` position: absolute; - background: black; + background: ${({ theme }) => + theme.displayTheme === "dark" ? "black" : "white"}; top: 100%; left: 0; right: 0; height: 50vh; overflow: scroll; z-index: 1; - border-bottom: 1px solid rgb(31, 30, 47); + border-bottom: 1px solid + ${({ theme }) => + theme.displayTheme === "dark" ? "rgb(31, 30, 47)" : "rgb(224, 225, 208)"}; `; function getAuthRequestLogIndicatorStyles(props: AuthRequestIndicatorProps) { diff --git a/src/components/auth/App.tsx b/src/components/auth/App.tsx index 9acee5c0..2e2a9aee 100644 --- a/src/components/auth/App.tsx +++ b/src/components/auth/App.tsx @@ -66,11 +66,13 @@ export default function App({ )} {appName || appUrl}} img={appIcon} - description={`${browser.i18n.getMessage("gateway")}: ${ - gateway?.host || "" - }`} + description={ + {`${browser.i18n.getMessage("gateway")}: ${ + gateway?.host || "" + }`} + } style={{ pointerEvents: "none" }} /> {/* @@ -169,6 +171,18 @@ const AllowanceSpent = styled(AppName)` color: #ffb800; `; +const PrimaryText = styled.span` + font-size: 1.25rem; + font-weight: 600; + color: ${(props) => props.theme.primaryTextv2}; +`; + +const SecondaryText = styled.span` + font-size: 0.875rem; + font-weight: 600; + color: ${(props) => props.theme.secondaryTextv2}; +`; + interface Props { appIcon?: string; appName?: string; diff --git a/src/components/auth/AuthButtons.tsx b/src/components/auth/AuthButtons.tsx index a3033bba..a9cc3aa6 100644 --- a/src/components/auth/AuthButtons.tsx +++ b/src/components/auth/AuthButtons.tsx @@ -15,6 +15,7 @@ export interface AuthButtonsProps { authRequest?: AuthRequest; primaryButtonProps?: AuthButtonProps; secondaryButtonProps?: AuthButtonProps; + showAuthStatus?: boolean; } // TODO: Consider creating a similar component without the `authRequest` to be reused everywhere where the "continue" @@ -23,7 +24,8 @@ export interface AuthButtonsProps { export function AuthButtons({ authRequest, primaryButtonProps, - secondaryButtonProps + secondaryButtonProps, + showAuthStatus = true }: AuthButtonsProps) { const showPrimaryButton = !!primaryButtonProps?.onClick; const showSecondaryButton = !!secondaryButtonProps?.onClick; @@ -51,7 +53,7 @@ export function AuthButtons({ return ( <> - {authRequest ? ( + {showAuthStatus && authRequest ? ( {browser.i18n.getMessage(`${authRequest.status}TransactionStatusAt`) + " "} @@ -89,6 +91,7 @@ export function AuthButtons({ const PStatusLabel = styled.p<{ status: AuthRequestStatus }>` margin: 0; padding: 16px; - background: ${({ theme }) => theme.backgroundv2}; + background: ${(props) => + props.theme.displayTheme === "light" ? "#f5f5f5" : "#191919"}; border-radius: 10px; `; diff --git a/src/components/auth/Permissions.tsx b/src/components/auth/Permissions.tsx index 00000f95..aa380865 100644 --- a/src/components/auth/Permissions.tsx +++ b/src/components/auth/Permissions.tsx @@ -80,6 +80,7 @@ export default function Permissions({
; + throw new Error( + setting.type + ? ErrorTypes.MissingSettingsType + : ErrorTypes.UnexpectedSettingsType + ); } } diff --git a/src/components/dashboard/subsettings/AppSettings.tsx b/src/components/dashboard/subsettings/AppSettings.tsx index 59b491b0..bd290e3c 100644 --- a/src/components/dashboard/subsettings/AppSettings.tsx +++ b/src/components/dashboard/subsettings/AppSettings.tsx @@ -1,5 +1,9 @@ import { InputWithBtn, InputWrapper } from "~components/arlocal/InputWrapper"; -import { permissionData, type PermissionType } from "~applications/permissions"; +import { + permissionData, + signPolicyOptions, + type PermissionType +} from "~applications/permissions"; import { defaultAllowance } from "~applications/allowance"; import { CheckIcon, EditIcon } from "@iconicicons/react"; import { useEffect, useMemo, useState } from "react"; @@ -10,6 +14,7 @@ import PermissionCheckbox, { import { removeApp } from "~applications"; import { ButtonV2, + Checkbox, InputV2, ModalV2, SelectV2, @@ -27,6 +32,8 @@ import styled from "styled-components"; import Arweave from "arweave"; import { defaultGateway, suggestedGateways, testnets } from "~gateways/gateway"; import type { CommonRouteProps } from "~wallets/router/router.types"; +import { ErrorTypes } from "~utils/error/error.utils"; +import { LoadingView } from "~components/page/common/loading/loading.view"; export interface AppSettingsDashboardViewParams { url: string; @@ -107,8 +114,9 @@ export function AppSettingsDashboardView({ // remove modal const removeModal = useModal(); - // TODO: Should this be a redirect? - if (!settings) return <>; + if (!settings) { + return ; + } return ( <> @@ -169,7 +177,31 @@ export function AppSettingsDashboardView({ ); })} - {browser.i18n.getMessage("allowance")} + {browser.i18n.getMessage("permission_settings")} +
+ {signPolicyOptions.map((option) => ( + + updateSettings((val) => ({ ...val, signPolicy: option })) + } + > + + updateSettings((val) => ({ ...val, signPolicy: option })) + } + checked={settings?.signPolicy === option} + /> +
+ + {browser.i18n.getMessage(option)} + +
+
+ ))} +
+ {/* {browser.i18n.getMessage("allowance")} { setEditingLimit(false); @@ -254,7 +286,7 @@ export function AppSettingsDashboardView({ onClick={() => setEditingLimit((val) => !val)} /> - + */} {browser.i18n.getMessage("gateway")} ` + color: ${(props) => props.theme.primaryTextv2}; + font-size: ${(props) => props.fontSize || 14}px; + font-weight: ${(props) => props.fontWeight || 500}; + text-align: ${(props) => props.textAlign || "left"}; +`; diff --git a/src/components/dashboard/subsettings/WalletSettings.tsx b/src/components/dashboard/subsettings/WalletSettings.tsx index be96e07b..f2d56fac 100644 --- a/src/components/dashboard/subsettings/WalletSettings.tsx +++ b/src/components/dashboard/subsettings/WalletSettings.tsx @@ -25,6 +25,7 @@ import styled from "styled-components"; import copy from "copy-to-clipboard"; import { formatAddress } from "~utils/format"; import type { CommonRouteProps } from "~wallets/router/router.types"; +import { LoadingView } from "~components/page/common/loading/loading.view"; export interface WalletSettingsDashboardViewParams { address: string; @@ -167,8 +168,9 @@ export function WalletSettingsDashboardView({ } } - // TODO: Should this be a redirect? - if (!wallet) return <>; + if (!wallet) { + return ; + } return ( diff --git a/src/components/page/common/Fallback/fallback.view.tsx b/src/components/page/common/Fallback/fallback.view.tsx new file mode 100644 index 00000000..392daee1 --- /dev/null +++ b/src/components/page/common/Fallback/fallback.view.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import styled from "styled-components"; +import browser from "webextension-polyfill"; +import { ButtonV2, Text } from "@arconnect/components"; +import { navigate } from "wouter/use-browser-location"; + +interface FallbackViewProps { + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +function handleReload() { + if (process.env.PLASMO_PUBLIC_APP_TYPE === "extension") { + browser.runtime.reload(); + } +} + +export const FallbackView: React.FC = ({ + error, + errorInfo +}) => { + const isDEV = process.env.NODE_ENV === "development"; + + return ( + + {browser.i18n.getMessage("fallback")} + {isDEV && ( + + {error?.toString()} + {`${browser.i18n.getMessage("commonErrorInfo")}: ${ + errorInfo?.componentStack + }`} + + )} + + {browser.i18n.getMessage("reload")} + + + ); +}; + +const DivWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +`; +const ErrorWrapper = styled(DivWrapper)` + margin: 10vh; + height: 20vh; +`; diff --git a/src/components/page/common/loading/loading.view.tsx b/src/components/page/common/loading/loading.view.tsx index 353299b6..2c03ea32 100644 --- a/src/components/page/common/loading/loading.view.tsx +++ b/src/components/page/common/loading/loading.view.tsx @@ -1,8 +1,8 @@ import { Loading } from "@arconnect/components"; import styled from "styled-components"; -import type { CommonRouteProps } from "~wallets/router/router.types"; +import browser from "webextension-polyfill"; -export interface LoadingViewProps extends CommonRouteProps { +export interface LoadingViewProps { label?: string; } @@ -10,7 +10,7 @@ export const LoadingView = ({ label }: LoadingViewProps) => { return ( - {label || "Loading..."} + {label || browser.i18n.getMessage("loading")} ); }; diff --git a/src/components/popup/HeadV2.tsx b/src/components/popup/HeadV2.tsx index f450d2d5..d3aab13b 100644 --- a/src/components/popup/HeadV2.tsx +++ b/src/components/popup/HeadV2.tsx @@ -8,7 +8,6 @@ import { Avatar, CloseLayer, NoAvatarIcon } from "./WalletHeader"; import { AnimatePresence } from "framer-motion"; import { useTheme } from "~utils/theme"; import { useStorage } from "@plasmohq/storage/hook"; -import { ArrowLeftIcon } from "@iconicicons/react"; import { useAnsProfile } from "~lib/ans"; import { ExtensionStorage } from "~utils/storage"; import HardwareWalletIcon, { @@ -23,6 +22,7 @@ import { svgie } from "~utils/svgies"; import type { AppLogoInfo } from "~applications/application"; import Squircle from "~components/Squircle"; import { useLocation } from "~wallets/router/router.utils"; +import { ArrowNarrowLeft } from "@untitled-ui/icons-react"; export interface HeadV2Props { title: string; @@ -164,7 +164,7 @@ export default function HeadV2({ open={isOpen} close={() => setOpen(false)} exactTop={true} - showOptions={showOptions} + showOptions={false} /> {isOpen && setOpen(false)} />} @@ -210,10 +210,10 @@ const HeadWrapper = styled(Section)<{ const BackButton = styled.button` position: absolute; - width: 32px; - height: 32px; + width: 24px; + height: 24px; top: 50%; - left: 0; + left: 12px; transform: translate(0, -50%); display: flex; align-items: center; @@ -241,10 +241,10 @@ const BackButton = styled.button` } `; -const BackButtonIcon = styled(ArrowLeftIcon)` +const BackButtonIcon = styled(ArrowNarrowLeft)` font-size: 1rem; - width: 1em; - height: 1em; + width: 1.5em; + height: 1.5em; color: rgb(${(props) => props.theme.primaryText}); z-index: 2; diff --git a/src/components/popup/WalletSwitcher.tsx b/src/components/popup/WalletSwitcher.tsx index 1c288d8e..c31ebb6f 100644 --- a/src/components/popup/WalletSwitcher.tsx +++ b/src/components/popup/WalletSwitcher.tsx @@ -34,7 +34,8 @@ export default function WalletSwitcher({ close, showOptions = true, exactTop = false, - noPadding = false + noPadding = false, + maxHeight }: Props) { const { navigate } = useLocation(); @@ -174,6 +175,7 @@ export default function WalletSwitcher({ { e.stopPropagation(); e.preventDefault(); @@ -317,8 +319,8 @@ const Wrapper = styled(Section)<{ noPadding: boolean }>` padding: 0 15px; `; -const WalletsCard = styled(Card)` - max-height: 80vh; +const WalletsCard = styled(Card)<{ maxHeight?: number }>` + max-height: ${(props) => (props.maxHeight ? `${props.maxHeight}px` : "80vh")}; overflow-y: auto; padding: 0; `; @@ -469,6 +471,7 @@ interface Props { showOptions?: boolean; exactTop?: boolean; noPadding?: boolean; + maxHeight?: number; } interface DisplayedWallet { diff --git a/src/popup.tsx b/src/popup.tsx index 5b409c88..794dbc4c 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -7,6 +7,8 @@ import { useExtensionLocation } from "~wallets/router/extension/extension-router import { WalletsProvider } from "~utils/wallets/wallets.provider"; import { useEffect } from "react"; import { handleSyncLabelsAlarm } from "~api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler"; +import { ErrorBoundary } from "~utils/error/ErrorBoundary/errorBoundary"; +import { FallbackView } from "~components/page/common/Fallback/fallback.view"; export function ArConnectBrowserExtensionApp() { useEffect(() => { @@ -24,11 +26,13 @@ export function ArConnectBrowserExtensionApp() { export function ArConnectBrowserExtensionAppRoot() { return ( - - - - - + + + + + + + ); } diff --git a/src/routes/auth/connect.tsx b/src/routes/auth/connect.tsx index 65effe77..5d011478 100644 --- a/src/routes/auth/connect.tsx +++ b/src/routes/auth/connect.tsx @@ -1,21 +1,22 @@ import { - Card, + Button, InputV2, - LabelV2, Section, Spacer, Text, - TooltipV2, useInput, useToasts } from "@arconnect/components"; -import { permissionData, type PermissionType } from "~applications/permissions"; +import { + permissionData, + signPolicyOptions, + type PermissionType +} from "~applications/permissions"; import { useCurrentAuthRequest } from "~utils/auth/auth.hooks"; -import { CloseLayer } from "~components/popup/WalletHeader"; import { AnimatePresence, motion } from "framer-motion"; import { unlock as globalUnlock } from "~wallets/auth"; import { useEffect, useMemo, useState } from "react"; -import { ChevronDownIcon } from "@iconicicons/react"; +import { InformationIcon } from "@iconicicons/react"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; import { formatAddress } from "~utils/format"; @@ -27,20 +28,20 @@ import Label from "~components/auth/Label"; import App from "~components/auth/App"; import styled from "styled-components"; import { EventType, trackEvent } from "~utils/analytics"; -import Application from "~applications/application"; +import Application, { type SignPolicy } from "~applications/application"; import { defaultGateway } from "~gateways/gateway"; import { CheckIcon, CloseIcon } from "@iconicicons/react"; -import { - InfoCircle, - ToggleSwitch -} from "~routes/popup/subscriptions/subscriptionDetails"; -import { defaultAllowance } from "~applications/allowance"; -import Arweave from "arweave"; import Permissions from "../../components/auth/Permissions"; -import { Flex } from "~routes/popup/settings/apps/[url]"; import { HeadAuth } from "~components/HeadAuth"; import { AuthButtons } from "~components/auth/AuthButtons"; -import type { CommonRouteProps } from "~wallets/router/router.types"; +import arconnectLogo from "url:/assets/ecosystem/arconnect.svg"; +import Squircle from "~components/Squircle"; +import { useActiveWallet } from "~wallets/hooks"; +import Checkbox from "~components/Checkbox"; +import { Eye, EyeOff } from "@untitled-ui/icons-react"; +import { CloseLayer } from "~components/popup/WalletHeader"; + +type Page = "unlock" | "connect" | "permissions" | "review" | "confirm"; export function ConnectAuthRequestView() { // active address @@ -49,7 +50,12 @@ export function ConnectAuthRequestView() { instance: ExtensionStorage }); - const arweave = new Arweave(defaultGateway); + const [signPolicy, setSignPolicy] = useState("always_ask"); + + // permissions to add + const [permissions, setPermissions] = useState([]); + + const wallet = useActiveWallet(); const { authRequest, acceptRequest, rejectRequest } = useCurrentAuthRequest("connect"); @@ -65,9 +71,13 @@ export function ConnectAuthRequestView() { const [switcherOpen, setSwitcherOpen] = useState(false); // page - const [page, setPage] = useState<"unlock" | "permissions">("unlock"); + const [page, setPage] = useState("connect"); - const allowanceInput = useInput(); + // password input + const passwordInput = useInput(); + + // toasts + const { setToast } = useToasts(); // requested permissions const [requestedPermissions, setRequestedPermissions] = useState< @@ -78,50 +88,25 @@ export function ConnectAuthRequestView() { [] ); - // allowance for permissions - const [allowanceEnabled, setAllowanceEnabled] = useState(true); - - // state management for edit - const [edit, setEdit] = useState(false); + const [showPassword, setShowPassword] = useState(false); - useEffect(() => { - (async () => { - const requested: PermissionType[] = authRequestPermissions; + const isCustomPermissions = useMemo(() => { + if (requestedPermissions.length !== requestedPermCopy.length) return true; - // add existing permissions - if (url) { - const app = new Application(url); - const existing = await app.getPermissions(); + // Create sorted copies to ensure order doesn't matter + const sortedRequested = [...requestedPermissions].sort(); + const sortedInitial = [...requestedPermCopy].sort(); - for (const existingP of existing) { - if (requested.includes(existingP)) continue; - requested.push(existingP); - } - } - - setRequestedPermissions( - requested.filter((p) => Object.keys(permissionData).includes(p)) - ); - - setRequestedPermCopy( - requested.filter((p) => Object.keys(permissionData).includes(p)) - ); - })(); - }, [url, authRequestPermissions]); - - // permissions to add - const [permissions, setPermissions] = useState([]); - - useEffect(() => setPermissions(requestedPermissions), [requestedPermissions]); - - // password input - const passwordInput = useInput(); + // Compare each element + return sortedRequested.some( + (permission, index) => permission !== sortedInitial[index] + ); + }, [requestedPermissions, requestedPermCopy]); - // toasts - const { setToast } = useToasts(); + // connect + async function connect() { + if (!url) return; - // unlock - async function unlock() { const unlockRes = await globalUnlock(passwordInput.state); if (!unlockRes) { @@ -133,50 +118,22 @@ export function ConnectAuthRequestView() { }); } - setPage("permissions"); - - // listen for enter to connect - window.addEventListener("keydown", async (e) => { - if (e.key !== "Enter") return; - await connect(); - }); - } - - // connect - async function connect(alwaysAsk: boolean = false) { - if (!url) return; - - if ( - allowanceEnabled && - Number(allowanceInput.state) < 0.001 && - !alwaysAsk - ) { - return setToast({ - type: "error", - content: browser.i18n.getMessage("invalid_qty_error"), - duration: 2200 - }); - } - // get existing permissions const app = new Application(url); - const existingPermissions = await app.getPermissions(); + const isAppPresent = await app.isAppPresent(); - if (existingPermissions.length === 0) { + if (!isAppPresent) { // add the app await addApp({ url, permissions, name: appInfo.name, logo: appInfo.logo, + signPolicy, // alwaysAsk, allowance: { - enabled: alwaysAsk || allowanceEnabled, - limit: alwaysAsk // if it's always ask set the limit to 0 - ? "0" - : allowanceEnabled - ? arweave.ar.arToWinston(allowanceInput.state) // If allowance is enabled and a new limit is set, use the new limit - : Number.MAX_SAFE_INTEGER.toString(), // If allowance is disabled set it to max number + enabled: false, + limit: "0", spent: "0" // in winstons }, // TODO: wayfinder @@ -186,18 +143,13 @@ export function ConnectAuthRequestView() { // update existing permissions, if the app // has already been added - const allowance = await app.getAllowance(); - await app.updateSettings({ + signPolicy, permissions, // alwaysAsk, allowance: { - enabled: alwaysAsk ?? allowanceEnabled, - limit: alwaysAsk // if it's always ask set the limit to 0 - ? "0" - : allowanceEnabled - ? arweave.ar.arToWinston(allowanceInput.state) // If allowance is enabled and a new limit is set, use the new limit - : Number.MAX_SAFE_INTEGER.toString(), // If allowance is disabled set it to max number + enabled: false, + limit: "0", spent: "0" // in winstons } }); @@ -212,208 +164,336 @@ export function ConnectAuthRequestView() { acceptRequest(); } + async function handleBack() { + if (page === "review") { + setPage("connect"); + } else if (page === "confirm") { + setPage("review"); + } else if (page === "permissions") { + setPage("confirm"); + } + } + + async function handlePrimaryOnClick() { + if (page === "connect") { + setPage("review"); + } else if (page === "review") { + setPage("confirm"); + } else if (page === "confirm") { + setPage("unlock"); + } else if (page === "unlock") { + await connect(); + } + } + useEffect(() => { - allowanceInput.setState(arweave.ar.winstonToAr(defaultAllowance.limit)); - }, []); + (async () => { + const requested: PermissionType[] = authRequestPermissions; - const removedPermissions = useMemo(() => { - return requestedPermCopy.filter( - (permission) => !requestedPermissions.includes(permission) - ); - }, [requestedPermCopy, requestedPermissions]); + // add existing permissions + if (url) { + const app = new Application(url); + const existing = await app.getPermissions(); + + for (const existingP of existing) { + if (requested.includes(existingP)) continue; + requested.push(existingP); + } + } + + setRequestedPermissions( + requested.filter((p) => Object.keys(permissionData).includes(p)) + ); + + setRequestedPermCopy( + requested.filter((p) => Object.keys(permissionData).includes(p)) + ); + })(); + }, [url, authRequestPermissions]); + + useEffect(() => setPermissions(requestedPermissions), [requestedPermissions]); + + const UnlockPage = () => ( + +
+
+ + + + + +
+ + {browser.i18n.getMessage("enter_your_password")} + + + {browser.i18n.getMessage("gateway")}:{" "} + {(gateway || defaultGateway)?.host || ""} + +
+
+ setShowPassword(false)} + /> + ) : ( + setShowPassword(true)} + /> + ) + } + fullWidth + {...passwordInput.bindings} + autoFocus + onKeyDown={(e) => { + if (e.key !== "Enter") return; + connect(); + }} + /> +
+
+ ); + + const PermissionsPage = () => ( + setPage("confirm")} + /> + ); + + const ConnectPage = () => ( + + + + + + + + +
+ + {browser.i18n.getMessage("connect_to_app", [appInfo.name || url])} + + + {browser.i18n.getMessage("gateway")}:{" "} + {(gateway || defaultGateway)?.host || ""} + +
+
+
+ + {browser.i18n.getMessage("select_account", [appInfo.name || url])}: + + + +
+ + + {wallet?.nickname?.charAt(0) || "A"} + + +
+ {wallet?.nickname} + + {formatAddress(activeAddress || "", 4)} + +
+
+ setSwitcherOpen((prev) => !prev)}> + {browser.i18n.getMessage("change")} + + setSwitcherOpen(false)} + showOptions={false} + exactTop={true} + noPadding={true} + maxHeight={180} + /> + {switcherOpen && ( + setSwitcherOpen(false)} /> + )} +
+
+
+
+ ); + + const ReviewPage = () => ( + +
+ + {browser.i18n.getMessage("connect_request_1", [appInfo.name || url])} + + {" "} + {wallet?.nickname} ({formatAddress(activeAddress || "", 4)}){" "} + + {browser.i18n.getMessage("connect_request_2")} + + {url} +
+ {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()] + )} + + + ))} +
+
+
+ ); + + const ConfirmPage = () => ( + +
+
+ + {browser.i18n.getMessage("confirm_permissions", [ + appInfo.name || url + ])} + + {url} +
+ + {signPolicyOptions.map((option) => ( + setSignPolicy(option)}> + setSignPolicy(option)} + checked={signPolicy === option} + /> +
+ + {browser.i18n.getMessage(option)} + +
+
+ ))} +
+ setPage("permissions")}> + + {browser.i18n.getMessage( + isCustomPermissions + ? "custom_permissions" + : "set_custom_permissions" + )} + + + +
+ +
+ + {browser.i18n.getMessage(`${signPolicy}_description`)} + +
+
+
+ ); return ( -
+ <> setEdit(false) : undefined} + showHead={!["connect", "unlock"].includes(page)} + title={browser.i18n.getMessage(page)} + back={handleBack} appInfo={appInfo} /> - + {!["connect", "unlock"].includes(page) && ( + + )} - + - {page === "unlock" && ( - -
- {browser.i18n.getMessage("wallet")} - - - setSwitcherOpen(true)} - open={switcherOpen} - > -
- {formatAddress(activeAddress || "", 10)} -
- -
- {switcherOpen && ( - setSwitcherOpen(false)} /> - )} - setSwitcherOpen(false)} - showOptions={false} - exactTop={true} - noPadding={true} - /> -
- - { - if (e.key !== "Enter") return; - unlock(); - }} - /> -
-
- )} - - {page === "permissions" && ( - <> - {!edit ? ( - -
- - {browser.i18n.getMessage( - "allow_these_permissions", - appInfo.name || url - )} - - {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} - /> - - )} -
-
- ) : ( - - )} - - )} + {page === "connect" && } + {page === "review" && } + {page === "confirm" && } + {page === "unlock" && } + {page === "permissions" && }
-
+ - {!edit && ( + {page !== "permissions" && (
0 - ? "allow_selected_permissions" - : "always_allow" + ? "connect" + : page !== "confirm" + ? "next" + : "confirm" ), - onClick: async () => { - if (page === "unlock") { - await unlock(); - } else { - await connect(); - } - } + onClick: handlePrimaryOnClick }} secondaryButtonProps={{ - label: browser.i18n.getMessage( - page === "unlock" ? "cancel" : "always_ask_permission" - ), - onClick: () => - page === "unlock" ? rejectRequest() : connect(true) + label: browser.i18n.getMessage("cancel"), + onClick: () => rejectRequest() }} />
@@ -422,21 +502,6 @@ export function ConnectAuthRequestView() { ); } -const InfoText: React.ReactNode = ( -
- Set the amount you want
- ArConnect to automatically transfer -
-); - -const WalletSelectWrapper = styled.div` - position: relative; -`; - -const StyledPermissions = styled.div` - padding-bottom: 1rem; -`; - const Permission = styled.div` margin: 0; align-items: center; @@ -444,35 +509,6 @@ const Permission = styled.div` gap: 8px; `; -const PermissionsTitle = styled.div` - display: flex; - width: 100%; - justify-content: space-between; -`; - -const SelectIcon = styled(ChevronDownIcon)` - font-size: 1rem; - width: 1.375rem; - height: 1.375 rem; - color: ${(props) => props.theme.primaryTextv2}; - 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; @@ -491,101 +527,185 @@ const StyledCloseIcon = styled(CloseIcon)` 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` +const ContentWrapper = 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; - } + flex: 1; + width: max-content; `; -const WalletSelect = styled(Card)<{ open: boolean }>` - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - background-color: transparent; - padding: 0.844rem 0.9375rem; - border: 1.5px solid ${(props) => props.theme.inputField}; - border-radius: 10px; - transition: all 0.23s ease-in-out; - - ${SelectIcon} { - transform: ${(props) => (props.open ? "rotate(180deg)" : "rotate(0)")}; +const UnlockWrapper = styled(motion.div).attrs({ + exit: { opacity: 0 }, + transition: { + type: "easeInOut", + duration: 0.2 } +})` + width: 100vw; - &:focus-within, - &: hover { - ${(props) => "border: 1.5px solid " + props.theme.primaryTextv2}; + ${Label} { + font-weight: 500; } +`; - &:active { - border-color: ${(props) => props.theme.inputField}; - color: rgb(${(props) => props.theme.theme}); - } +const IconWrapper = styled.img` + height: 48px; + width: 48px; + overflow: hidden; + border-radius: 48px; `; -const Address = styled(Text).attrs({ - noMargin: true, - title: false +const AppIconsWrapper = styled.div``; + +const ConnectPageContent = styled.div` + width: 100vw; + flex: 1; + display: flex; + flex-direction: column; +`; + +const ConnectToApp = styled(Text).attrs({ + noMargin: true })` - font-size: 16px; - line-height: 22px; - font-weight: 500; + font-size: 22px; + font-weight: 600; color: ${(props) => props.theme.primaryTextv2}; + line-height: 120%; `; -const ContentWrapper = styled.div` +const Gateway = styled(Text).attrs({ + noMargin: true +})` + color: ${(props) => props.theme.secondaryTextv2}; + font-size: 14px; + font-weight: 500; + line-height: 150%; +`; + +const ConnectWalletWrapper = styled.div` display: flex; - width: max-content; + padding: 8px; + justify-content: space-between; + align-items: center; + align-self: stretch; + border-radius: 10px; + background: ${(props) => props.theme.backgroundSecondary}; `; -const UnlockWrapper = styled(motion.div).attrs({ - exit: { opacity: 0 }, - transition: { - type: "easeInOut", - duration: 0.2 - } +export const AccountSquircle = styled(Squircle)` + position: relative; + width: 40px; + height: 40px; + flex-shrink: 0; + justify-content: center; + align-items: center; + color: rgba(${(props) => props.theme.theme}); +`; + +export const AccountInitial = styled.span` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #fff; + text-align: center; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const WalletName = styled(Text).attrs({ + noMargin: true })` - width: 100vw; + font-size: 18px; + font-weight: 500; + color: ${(props) => props.theme.primaryTextv2}; +`; - ${Label} { +const SecondaryText = styled(Text).attrs({ + noMargin: true +})<{ fontSize?: number }>` + color: ${(props) => props.theme.secondaryTextv2}; + font-size: ${(props) => props.fontSize || 14}px; + font-weight: 500; + + span { + color: ${(props) => props.theme.primaryTextv2}; + font-size: ${(props) => props.fontSize || 14}px; font-weight: 500; } `; -const PermissionsContent = styled(motion.div).attrs({ - initial: { - opacity: 0, - y: 50 - }, - animate: { - opacity: 1, - y: 0 - }, - transition: { - type: "easeInOut", - duration: 0.2 - } +const PrimaryText = styled(Text).attrs({ + noMargin: true +})<{ fontSize?: number; fontWeight?: number; textAlign?: string }>` + color: ${(props) => props.theme.primaryTextv2}; + font-size: ${(props) => props.fontSize || 14}px; + font-weight: ${(props) => props.fontWeight || 500}; + text-align: ${(props) => props.textAlign || "left"}; +`; + +const ChangeText = styled(Text).attrs({ + noMargin: true })` - width: 100vw; + color: ${(props) => props.theme.primary}; + font-size: 14px; + font-weight: 500; + cursor: pointer; +`; + +const PolicyOptionContainer = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const PolicyOption = styled.div` + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +`; + +const CustomPermissionsButton = styled(Button)` + display: flex; + padding: 8px; + justify-content: center; + align-items: center; + align-self: stretch; + border-radius: 8px; + background: ${(props) => props.theme.backgroundSecondary}; +`; + +const CustomPermissionsInfo = styled.div` + display: flex; + padding: 12px; + justify-content: center; + align-items: center; + gap: 10px; + align-self: stretch; + border: 1px solid ${(props) => props.theme.backgroundSecondary}; + border-radius: 8px; +`; + +const ConnectPageSection = styled(Section)` + display: flex; + flex: 1; + flex-direction: column; + gap: 24px; + padding-bottom: 0px; +`; + +const ConnectPageSectionHeader = styled.div` + text-align: center; + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; `; diff --git a/src/routes/auth/decrypt.tsx b/src/routes/auth/decrypt.tsx new file mode 100644 index 00000000..cb5725ca --- /dev/null +++ b/src/routes/auth/decrypt.tsx @@ -0,0 +1,58 @@ +import { Section, Text } from "@arconnect/components"; +import Message from "~components/auth/Message"; +import Wrapper from "~components/auth/Wrapper"; +import browser from "webextension-polyfill"; +import { useEffect } from "react"; +import { useCurrentAuthRequest } from "~utils/auth/auth.hooks"; +import { HeadAuth } from "~components/HeadAuth"; +import { AuthButtons } from "~components/auth/AuthButtons"; + +export function DecryptAuthRequestView() { + const { authRequest, acceptRequest, rejectRequest } = + useCurrentAuthRequest("decrypt"); + + const { authID, url, message } = authRequest; + + // listen for enter to reset + useEffect(() => { + const listener = async (e: KeyboardEvent) => { + if (e.key !== "Enter") return; + await acceptRequest(); + }; + + window.addEventListener("keydown", listener); + + return () => window.removeEventListener("keydown", listener); + }, [authID]); + + return ( + +
+ + +
+ + {browser.i18n.getMessage("decrypt_description", url)} + + +
+ +
+
+
+ +
+ acceptRequest() + }} + secondaryButtonProps={{ + onClick: () => rejectRequest() + }} + /> +
+
+ ); +} diff --git a/src/routes/popup/settings/apps/[url]/index.tsx b/src/routes/popup/settings/apps/[url]/index.tsx index 5eb53415..0fed3326 100644 --- a/src/routes/popup/settings/apps/[url]/index.tsx +++ b/src/routes/popup/settings/apps/[url]/index.tsx @@ -26,6 +26,10 @@ import HeadV2 from "~components/popup/HeadV2"; import { ToggleSwitch } from "~routes/popup/subscriptions/subscriptionDetails"; import type { CommonRouteProps } from "~wallets/router/router.types"; import { useLocation } from "~wallets/router/router.utils"; +import Checkbox from "~components/Checkbox"; +import { ErrorTypes } from "~utils/error/error.utils"; +import { LoadingView } from "~components/page/common/loading/loading.view"; +import { signPolicyOptions } from "~applications/permissions"; export interface AppSettingsViewParams { url: string; @@ -42,24 +46,8 @@ export function AppSettingsView({ params: { url } }: AppSettingsViewProps) { const arweave = new Arweave(defaultGateway); // allowance spent qty - const spent = useMemo(() => { - const val = settings?.allowance?.spent; - - if (!val) return "0"; - return val.toString(); - }, [settings]); - - const isAllowanceDisabled = useMemo(() => { - return !settings?.allowance?.enabled; - }, [settings?.allowance?.enabled]); // allowance limit - const limit = useMemo(() => { - const val = settings?.allowance?.limit; - - if (!val) return arweave.ar.arToWinston("0.1"); - return val.toString(); - }, [settings]); // active gateway const gateway = useMemo(() => { @@ -87,8 +75,6 @@ export function AppSettingsView({ params: { url } }: AppSettingsViewProps) { // custom gateway input const customGatewayInput = useInput(); - const limitInput = useInput(); - useEffect(() => { if (!isCustom || !settings.gateway) return; @@ -102,8 +88,9 @@ export function AppSettingsView({ params: { url } }: AppSettingsViewProps) { // remove modal const removeModal = useModal(); - // TODO: Should this be a redirect? - if (!settings) return <>; + if (!settings) { + return ; + } return ( <> @@ -134,7 +121,7 @@ export function AppSettingsView({ params: { url } }: AppSettingsViewProps) { - + {/* {browser.i18n.getMessage("allowance")} + */} + {browser.i18n.getMessage("permission_settings")} +
+ {signPolicyOptions.map((option) => ( + + updateSettings((val) => ({ ...val, signPolicy: option })) + } + > + + updateSettings((val) => ({ ...val, signPolicy: option })) + } + checked={settings?.signPolicy === option} + /> +
+ + {browser.i18n.getMessage(option)} + +
+
+ ))} +
{browser.i18n.getMessage("gateway")} props.theme.secondaryTextv2}; -`; - const TitleV1 = styled(Text).attrs({ heading: true })` @@ -408,20 +418,6 @@ const TitleV2 = styled(Text).attrs({ font-weight: 500; `; -const NumberInputV2 = styled(InputV2)` - /* Chrome, Safari, Edge, Opera */ - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - /* Firefox */ - &[type="number"] { - -moz-appearance: textfield; - } -`; - const ResetButton = styled.span` border-bottom: 1px solid rgba(${(props) => props.theme.primaryText}, 0.8); margin-left: 0.37rem; @@ -443,3 +439,19 @@ export const Flex = styled.div<{ alignItems: string; justifyContent: string }>` align-items: ${(props) => props.alignItems}; justify-content: ${(props) => props.justifyContent}; `; + +const PolicyOption = styled.div` + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +`; + +const PrimaryText = styled(Text).attrs({ + noMargin: true +})<{ fontSize?: number; fontWeight?: number; textAlign?: string }>` + color: ${(props) => props.theme.primaryTextv2}; + font-size: ${(props) => props.fontSize || 14}px; + font-weight: ${(props) => props.fontWeight || 500}; + text-align: ${(props) => props.textAlign || "left"}; +`; diff --git a/src/routes/popup/settings/apps/[url]/permissions.tsx b/src/routes/popup/settings/apps/[url]/permissions.tsx index 808ade18..af8ff494 100644 --- a/src/routes/popup/settings/apps/[url]/permissions.tsx +++ b/src/routes/popup/settings/apps/[url]/permissions.tsx @@ -7,6 +7,8 @@ import { permissionData, type PermissionType } from "~applications/permissions"; import Checkbox from "~components/Checkbox"; import type { CommonRouteProps } from "~wallets/router/router.types"; import { useLocation } from "~wallets/router/router.utils"; +import { ErrorTypes } from "~utils/error/error.utils"; +import { LoadingView } from "~components/page/common/loading/loading.view"; export interface AppPermissionsViewParams { url: string; @@ -24,8 +26,9 @@ export function AppPermissionsView({ const app = new Application(decodeURIComponent(url)); const [settings, updateSettings] = app.hook(); - // TODO: Should this be a redirect? - if (!settings) return <>; + if (!settings) { + return ; + } return ( <> diff --git a/src/routes/popup/settings/tokens/[id]/index.tsx b/src/routes/popup/settings/tokens/[id]/index.tsx index 2bf89ab9..ae824d85 100644 --- a/src/routes/popup/settings/tokens/[id]/index.tsx +++ b/src/routes/popup/settings/tokens/[id]/index.tsx @@ -20,6 +20,8 @@ import { CopyButton } from "~components/dashboard/subsettings/WalletSettings"; import HeadV2 from "~components/popup/HeadV2"; import type { CommonRouteProps } from "~wallets/router/router.types"; import { useLocation } from "~wallets/router/router.utils"; +import { ErrorTypes } from "~utils/error/error.utils"; +import { LoadingView } from "~components/page/common/loading/loading.view"; export interface TokenSettingsParams { id: string; @@ -82,8 +84,9 @@ export function TokenSettingsView({ params: { id } }: TokenSettingsProps) { }); } - // TODO: Should this be a redirect? - if (!token) return null; + if (!token) { + return ; + } return ( <> diff --git a/src/routes/popup/settings/wallets/[address]/export.tsx b/src/routes/popup/settings/wallets/[address]/export.tsx index 48758b59..c15415bc 100644 --- a/src/routes/popup/settings/wallets/[address]/export.tsx +++ b/src/routes/popup/settings/wallets/[address]/export.tsx @@ -17,6 +17,8 @@ import styled from "styled-components"; import HeadV2 from "~components/popup/HeadV2"; import type { CommonRouteProps } from "~wallets/router/router.types"; import { useLocation } from "~wallets/router/router.utils"; +import { ErrorTypes } from "~utils/error/error.utils"; +import { LoadingView } from "~components/page/common/loading/loading.view"; export interface ExportWalletViewParams { address: string; @@ -87,8 +89,9 @@ export function ExportWalletView({ } } - // TODO: Should this be a redirect? - if (!wallet) return <>; + if (!wallet) { + return ; + } return ( <> diff --git a/src/routes/popup/settings/wallets/[address]/index.tsx b/src/routes/popup/settings/wallets/[address]/index.tsx index 151b6f5a..db40257c 100644 --- a/src/routes/popup/settings/wallets/[address]/index.tsx +++ b/src/routes/popup/settings/wallets/[address]/index.tsx @@ -26,6 +26,8 @@ import { formatAddress } from "~utils/format"; import HeadV2 from "~components/popup/HeadV2"; import type { CommonRouteProps } from "~wallets/router/router.types"; import { useLocation } from "~wallets/router/router.utils"; +import { ErrorTypes } from "~utils/error/error.utils"; +import { LoadingView } from "~components/page/common/loading/loading.view"; export interface WalletViewParams { address: string; @@ -126,8 +128,9 @@ export function WalletView({ params: { address } }: WalletViewProps) { // wallet remove modal const removeModal = useModal(); - // TODO: Should this be a redirect? - if (!wallet) return <>; + if (!wallet) { + return ; + } return ( <> diff --git a/src/routes/popup/transaction/[id].tsx b/src/routes/popup/transaction/[id].tsx index 8e81e406..7440b3d2 100644 --- a/src/routes/popup/transaction/[id].tsx +++ b/src/routes/popup/transaction/[id].tsx @@ -57,6 +57,7 @@ import type { ArConnectRoutePath, CommonRouteProps } from "~wallets/router/router.types"; +import { ErrorTypes } from "~utils/error/error.utils"; // pull contacts and check if to address is in contacts @@ -81,8 +82,9 @@ export function TransactionView({ const { navigate, back } = useLocation(); const { back: backPath } = useSearchParams<{ back?: string }>(); - // TODO: Should this be a redirect? - if (!id) return <>; + if (!id) { + throw new Error(ErrorTypes.MissingTxId); + } // fetch tx data const [transaction, setTransaction] = useState(); diff --git a/src/tabs/auth.tsx b/src/tabs/auth.tsx index a85e29a0..ea170df2 100644 --- a/src/tabs/auth.tsx +++ b/src/tabs/auth.tsx @@ -8,6 +8,8 @@ import { WalletsProvider } from "~utils/wallets/wallets.provider"; import { useExtensionStatusOverride } from "~wallets/router/extension/extension-router.hook"; import { useEffect } from "react"; import { handleSyncLabelsAlarm } from "~api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler"; +import { ErrorBoundary } from "~utils/error/ErrorBoundary/errorBoundary"; +import { FallbackView } from "~components/page/common/Fallback/fallback.view"; export function AuthApp() { useEffect(() => { @@ -20,13 +22,15 @@ export function AuthApp() { export function AuthAppRoot() { return ( - - - - - - - + + + + + + + + + ); } diff --git a/src/tabs/dashboard.tsx b/src/tabs/dashboard.tsx index 83a2b890..f3d1b206 100644 --- a/src/tabs/dashboard.tsx +++ b/src/tabs/dashboard.tsx @@ -6,6 +6,8 @@ import { ArConnectThemeProvider } from "~components/hardware/HardwareWalletTheme import { useEffect } from "react"; import { handleSyncLabelsAlarm } from "~api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler"; import { WalletsProvider } from "~utils/wallets/wallets.provider"; +import { ErrorBoundary } from "~utils/error/ErrorBoundary/errorBoundary"; +import { FallbackView } from "~components/page/common/Fallback/fallback.view"; export function DashboardApp() { useEffect(() => { @@ -20,11 +22,13 @@ export function DashboardApp() { export function DashboardAppRoot() { return ( - - - - - + + + + + + + ); } diff --git a/src/tabs/welcome.tsx b/src/tabs/welcome.tsx index a09c35ca..6350ab83 100644 --- a/src/tabs/welcome.tsx +++ b/src/tabs/welcome.tsx @@ -7,6 +7,8 @@ import { BodyScroller } from "~wallets/router/router.utils"; import { AnimatePresence } from "framer-motion"; import { Routes } from "~wallets/router/routes.component"; import { WELCOME_ROUTES } from "~wallets/router/welcome/welcome.routes"; +import { ErrorBoundary } from "~utils/error/ErrorBoundary/errorBoundary"; +import { FallbackView } from "~components/page/common/Fallback/fallback.view"; export function ArConnectWelcomeApp() { return ; @@ -17,13 +19,14 @@ export function ArConnectWelcomeAppRoot() { return ( - - - - - - - + + + + + + + + ); } diff --git a/src/utils/auth/auth.types.ts b/src/utils/auth/auth.types.ts index 52f8d328..88225e6c 100644 --- a/src/utils/auth/auth.types.ts +++ b/src/utils/auth/auth.types.ts @@ -79,6 +79,13 @@ export interface TokenAuthRequestData { dre?: string; } +// DECRYPT + +export interface DecryptAuthRequestData { + type: "decrypt"; + message: number[]; +} + // SIGN: export interface SignAuthRequestData { @@ -88,12 +95,6 @@ export interface SignAuthRequestData { collectionID: string; } -// SUBSCRIPTION: - -export interface SubscriptionAuthRequestData extends SubscriptionData { - type: "subscription"; -} - // SIGN KEYSTONE: export interface SignKeystoneAuthRequestData { @@ -123,6 +124,12 @@ export interface BatchSignDataItemAuthRequestData { data: RawDataItem; } +// SUBSCRIPTION: + +export interface SubscriptionAuthRequestData extends SubscriptionData { + type: "subscription"; +} + // AuthRequestMessageData: export type ConnectAuthRequestMessageData = ConnectAuthRequestData & @@ -131,9 +138,9 @@ export type AllowanceAuthRequestMessageData = AllowanceAuthRequestData & CommonAuthRequestProps; export type TokenAuthRequestMessageData = TokenAuthRequestData & CommonAuthRequestProps; -export type SignAuthRequestMessageData = SignAuthRequestData & +export type DecryptAuthRequestMessageData = DecryptAuthRequestData & CommonAuthRequestProps; -export type SubscriptionAuthRequestMessageData = SubscriptionAuthRequestData & +export type SignAuthRequestMessageData = SignAuthRequestData & CommonAuthRequestProps; export type SignKeystoneAuthRequestMessageData = SignKeystoneAuthRequestData & CommonAuthRequestProps; @@ -143,20 +150,21 @@ export type SignDataItemAuthRequestMessageData = SignDataItemAuthRequestData & CommonAuthRequestProps; export type BatchSignDataItemAuthRequestMessageData = BatchSignDataItemAuthRequestData & CommonAuthRequestProps; +export type SubscriptionAuthRequestMessageData = SubscriptionAuthRequestData & + CommonAuthRequestProps; // AuthRequest: export type ConnectAuthRequest = ConnectAuthRequestMessageData; export type AllowanceAuthRequest = AllowanceAuthRequestMessageData; export type TokenAuthRequest = TokenAuthRequestMessageData; +export type DecryptAuthRequest = DecryptAuthRequestMessageData; export interface SignAuthRequest extends Omit { transaction: SplitTransaction | Transaction; } -export type SubscriptionAuthRequest = SubscriptionAuthRequestMessageData; - export interface SignKeystoneAuthRequest extends SignKeystoneAuthRequestMessageData { data?: Buffer; @@ -167,6 +175,8 @@ export type SignDataItemAuthRequest = SignDataItemAuthRequestMessageData; export type BatchSignDataItemAuthRequest = BatchSignDataItemAuthRequestMessageData; +export type SubscriptionAuthRequest = SubscriptionAuthRequestMessageData; + // Unions & Misc: export type AuthType = AuthRequestData["type"]; @@ -175,43 +185,47 @@ export type AuthRequestData = | ConnectAuthRequestData | AllowanceAuthRequestData | TokenAuthRequestData + | DecryptAuthRequestData | SignAuthRequestData - | SubscriptionAuthRequestData | SignKeystoneAuthRequestData | SignatureAuthRequestData | SignDataItemAuthRequestData - | BatchSignDataItemAuthRequestData; + | BatchSignDataItemAuthRequestData + | SubscriptionAuthRequestData; export type AuthRequestMessageData = | ConnectAuthRequestMessageData | AllowanceAuthRequestMessageData | TokenAuthRequestMessageData + | DecryptAuthRequestMessageData | SignAuthRequestMessageData - | SubscriptionAuthRequestMessageData | SignKeystoneAuthRequestMessageData | SignatureAuthRequestMessageData | SignDataItemAuthRequestMessageData - | BatchSignDataItemAuthRequestMessageData; + | BatchSignDataItemAuthRequestMessageData + | SubscriptionAuthRequestMessageData; export type AuthRequest = | ConnectAuthRequest | AllowanceAuthRequest | TokenAuthRequest + | DecryptAuthRequest | SignAuthRequest - | SubscriptionAuthRequest | SignKeystoneAuthRequest | SignatureAuthRequest | SignDataItemAuthRequest - | BatchSignDataItemAuthRequest; + | BatchSignDataItemAuthRequest + | SubscriptionAuthRequest; export type AuthRequestByType = { connect: ConnectAuthRequest; allowance: AllowanceAuthRequest; token: TokenAuthRequest; + decrypt: DecryptAuthRequest; sign: SignAuthRequest; - subscription: SubscriptionAuthRequest; signKeystone: SignKeystoneAuthRequest; signature: SignatureAuthRequest; signDataItem: SignDataItemAuthRequest; batchSignDataItem: BatchSignDataItemAuthRequest; + subscription: SubscriptionAuthRequest; }; diff --git a/src/utils/error/ErrorBoundary/errorBoundary.tsx b/src/utils/error/ErrorBoundary/errorBoundary.tsx new file mode 100644 index 00000000..348baabf --- /dev/null +++ b/src/utils/error/ErrorBoundary/errorBoundary.tsx @@ -0,0 +1,48 @@ +import React, { Component, type ReactNode } from "react"; + +interface ErrorBoundaryProps { + fallback: React.ComponentType<{ + error: Error | null; + errorInfo: React.ErrorInfo | null; + }>; + onError?: (error: Error, errorInfo: React.ErrorInfo) => void; + children?: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error, errorInfo: null }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + this.setState({ error, errorInfo }); + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + console.error("ErrorBoundary caught an error:", error, errorInfo); + } + + render(): ReactNode { + if (this.state.hasError) { + const { error, errorInfo } = this.state; + const FallbackComponent = this.props.fallback; + return ; + } + + return this.props.children; + } +} diff --git a/src/utils/error/error.utils.ts b/src/utils/error/error.utils.ts index 30659e6d..bb6e6d27 100644 --- a/src/utils/error/error.utils.ts +++ b/src/utils/error/error.utils.ts @@ -1,3 +1,26 @@ export function isError(data: unknown): data is Error { return data instanceof Error; } + +/** + * For future reference, here are some error types that can be used: + * + * "Not found": Something was queried/mapped/loaded but was not there. + * "Missing": Something should have been provided/set, but wasn't. + * "Unexpected": Something was provided, but the value wasn't right. + * */ + +export enum ErrorTypes { + Error = "Error", + RangeError = "RangeError", + ReferenceError = "ReferenceError", + SyntaxError = "SyntaxError", + URIError = "URIError", + PageNotFound = "Page not found", + MissingSettingsType = "Missing settings type", + UnexpectedSettingsType = "Unexpected settings type", + SettingsNotFound = "Settings not found", + WalletNotFound = "Wallet not found", + TokenNotFound = "Token not found", + MissingTxId = "Transaction ID not found" +} diff --git a/src/wallets/router/auth/auth.routes.ts b/src/wallets/router/auth/auth.routes.ts index e41989b7..edc3dda9 100644 --- a/src/wallets/router/auth/auth.routes.ts +++ b/src/wallets/router/auth/auth.routes.ts @@ -1,6 +1,7 @@ import { AllowanceAuthRequestView } from "~routes/auth/allowance"; import { BatchSignDataItemAuthRequestView } from "~routes/auth/batchSignDataItem"; import { ConnectAuthRequestView } from "~routes/auth/connect"; +import { DecryptAuthRequestView } from "~routes/auth/decrypt"; import { LoadingAuthRequestView } from "~routes/auth/loading"; import { SignAuthRequestView } from "~routes/auth/sign"; import { SignatureAuthRequestView } from "~routes/auth/signature"; @@ -17,23 +18,25 @@ export type AuthRoutePath = | `/connect/${string}` | `/allowance/${string}` | `/token/${string}` + | `/decrypt/${string}` | `/sign/${string}` | `/signKeystone/${string}` | `/signature/${string}` - | `/subscription/${string}` | `/signDataItem/${string}` - | `/batchSignDataItem/${string}`; + | `/batchSignDataItem/${string}` + | `/subscription/${string}`; export const AuthPaths = { Connect: "/connect/:authID", Allowance: "/allowance/:authID", Token: "/token/:authID", + Decrypt: "/decrypt/:authID", Sign: "/sign/:authID", SignKeystone: "/signKeystone/:authID", Signature: "/signature/:authID", - Subscription: "/subscription/:authID", SignDataItem: "/signDataItem/:authID", - BatchSignDataItem: "/batchSignDataItem/:authID" + BatchSignDataItem: "/batchSignDataItem/:authID", + Subscription: "/subscription/:authID" } as const satisfies Record; export const AUTH_ROUTES = [ @@ -53,6 +56,10 @@ export const AUTH_ROUTES = [ path: AuthPaths.Token, component: TokenAuthRequestView }, + { + path: AuthPaths.Decrypt, + component: DecryptAuthRequestView + }, { path: AuthPaths.Sign, component: SignAuthRequestView @@ -65,10 +72,6 @@ export const AUTH_ROUTES = [ path: AuthPaths.Signature, component: SignatureAuthRequestView }, - { - path: AuthPaths.Subscription, - component: SubscriptionAuthRequestView - }, { path: AuthPaths.SignDataItem, component: SignDataItemAuthRequestView @@ -76,5 +79,9 @@ export const AUTH_ROUTES = [ { path: AuthPaths.BatchSignDataItem, component: BatchSignDataItemAuthRequestView + }, + { + path: AuthPaths.Subscription, + component: SubscriptionAuthRequestView } ] as const satisfies RouteConfig[];