From 15a70bd87884cd67427e68a1c98896b032ed19ce Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:58:38 +0100 Subject: [PATCH] feat: better mobile feedback --- .github/workflows/deploy.yml | 6 ++-- .knip.jsonc | 2 +- .../read-claim-data-from-url.ts | 29 ++++++++++------ static/scripts/rewards/toaster.ts | 10 +++--- static/scripts/rewards/web3/connect-wallet.ts | 28 +++++++++++---- static/scripts/rewards/web3/erc20-permit.ts | 34 +++++++++++++------ 6 files changed, 72 insertions(+), 37 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b78ea45..09b6dad8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,7 +58,7 @@ jobs: script: | const { owner, repo } = context.repo; const sha = "${{ github.event.workflow_run.head_sha }}"; - + const response = await github.rest.search.issuesAndPullRequests({ q: `repo:${owner}/${repo} is:pr sha:${sha}`, per_page: 1, @@ -82,11 +82,11 @@ jobs: repo, issue_number, }); - + // Find the comment to update or create a new one if not found let existingComment = comments.data.find(comment => comment.user.login === 'github-actions[bot]'); let body = '| Preview Deployment |\n| ------------------ |\n'; - + // If the comment exists, update its body if (existingComment) { // Check if the SHA already exists in the comment body to avoid duplicates diff --git a/.knip.jsonc b/.knip.jsonc index 3b28c2af..27f07c24 100644 --- a/.knip.jsonc +++ b/.knip.jsonc @@ -1,5 +1,5 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "entry": ["static/scripts/rewards/init.ts"], - "ignore": ["lib/**"] + "ignore": ["lib/**"], } diff --git a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts index 95927e08..74189ec9 100644 --- a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts +++ b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts @@ -3,7 +3,7 @@ import { Value } from "@sinclair/typebox/value"; import { createClient } from "@supabase/supabase-js"; import { AppState, app } from "../app-state"; import { useFastestRpc } from "../rpc-optimization/get-optimal-provider"; -import { buttonController, toaster } from "../toaster"; +import { toaster } from "../toaster"; import { connectWallet } from "../web3/connect-wallet"; import { checkRenderInvalidatePermitAdminControl, checkRenderMakeClaimControl } from "../web3/erc20-permit"; import { verifyCurrentNetwork } from "../web3/verify-current-network"; @@ -31,25 +31,32 @@ export async function readClaimDataFromUrl(app: AppState) { app.claims = decodeClaimData(base64encodedTxData).flat(); app.claimTxs = await getClaimedTxs(app); + try { app.provider = await useFastestRpc(app); } catch (e) { toaster.create("error", `${e}`); } - if (window.ethereum) { - try { - app.signer = await connectWallet(); - } catch (error) { - /* empty */ - } - window.ethereum.on("accountsChanged", () => { + + try { + app.signer = await connectWallet(); + } catch (error) { + /* empty */ + } + + try { + // this would throw on mobile browsers & non-web3 browsers + window?.ethereum.on("accountsChanged", () => { checkRenderMakeClaimControl(app).catch(console.error); checkRenderInvalidatePermitAdminControl(app).catch(console.error); }); - } else { - buttonController.hideAll(); - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); + } catch (err) { + /* + * handled feedback upstream already + * buttons are hidden and non-web3 infinite toast exists + */ } + displayRewardDetails(); displayRewardPagination(); diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index f5ed7b60..3e455593 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -19,10 +19,10 @@ export const viewClaimButton = document.getElementById("view-claim") as HTMLButt const notifications = document.querySelector(".notifications") as HTMLUListElement; export const buttonController = new ButtonController(controls); -function createToast(meaning: keyof typeof toaster.icons, text: string) { +function createToast(meaning: keyof typeof toaster.icons, text: string, timeout: number = 5000) { if (meaning != "info") buttonController.hideLoader(); const toastDetails = { - timer: 5000, + timer: timeout, } as { timer: number; timeoutId?: NodeJS.Timeout; @@ -43,8 +43,10 @@ function createToast(meaning: keyof typeof toaster.icons, text: string) { notifications.appendChild(toastContent); // Append the toast to the notification ul - // Setting a timeout to remove the toast after the specified duration - toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); + if (timeout !== Infinity) { + // Setting a timeout to remove the toast after the specified duration + toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); + } } function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts index 81a29cb7..3a2c8563 100644 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -20,14 +20,28 @@ export async function connectWallet(): Promise { return signer; } catch (error: unknown) { - if (error instanceof Error) { - console.error(error); - if (error?.message?.includes("missing provider")) { - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); - } else { - toaster.create("info", "Please connect your wallet to collect this reward."); + connectErrorHandler(error); + } + return null; +} + +function connectErrorHandler(error: unknown) { + if (error instanceof Error) { + console.error(error); + if (error?.message?.includes("missing provider")) { + // mobile browsers don't really support window.ethereum + const mediaQuery = window.matchMedia("(max-width: 768px)"); + + if (mediaQuery.matches) { + toaster.create("warning", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward", Infinity); + } else if (!window.ethereum) { + toaster.create("warning", "Please use a web3 enabled browser to collect this reward.", Infinity); + buttonController.hideAll(); } + } else { + toaster.create("error", error.message); } - return null; + } else { + toaster.create("error", "An unknown error occurred."); } } diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 858694ac..253e2a08 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -6,6 +6,7 @@ import { permit2Address } from "../constants"; import { supabase } from "../render-transaction/read-claim-data-from-url"; import { Erc20Permit, Erc721Permit } from "../render-transaction/tx-type"; import { MetaMaskError, buttonController, errorToast, getMakeClaimButton, toaster } from "../toaster"; +import { connectWallet } from "./connect-wallet"; export async function fetchTreasury( permit: Erc20Permit | Erc721Permit @@ -60,6 +61,9 @@ async function checkPermitClaimability(app: AppState): Promise { async function transferFromPermit(permit2Contract: Contract, app: AppState) { const reward = app.reward; + const signer = app.signer; + if (!signer) return null; + try { const tx = await permit2Contract.permitTransferFrom(reward.permit, reward.transferDetails, reward.owner, reward.signature); toaster.create("info", `Transaction sent`); @@ -91,6 +95,7 @@ async function waitForTransaction(tx: TransactionResponse) { buttonController.hideLoader(); buttonController.hideMakeClaim(); console.log(receipt.transactionHash); + return receipt; } catch (error: unknown) { if (error instanceof Error) { @@ -103,21 +108,23 @@ async function waitForTransaction(tx: TransactionResponse) { export function claimErc20PermitHandlerWrapper(app: AppState) { return async function claimErc20PermitHandler() { + const signer = await connectWallet(); + if (!signer) { + return; + } + buttonController.hideMakeClaim(); buttonController.showLoader(); const isPermitClaimable = await checkPermitClaimability(app); if (!isPermitClaimable) return; - const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, app.signer); + const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); if (!permit2Contract) return; const tx = await transferFromPermit(permit2Contract, app); if (!tx) return; - // buttonController.showLoader(); - // buttonController.hideMakeClaim(); - const receipt = await waitForTransaction(tx); if (!receipt) return; @@ -168,9 +175,10 @@ async function checkPermitClaimable(app: AppState): Promise { return false; } - let user: string; + let user: string | undefined; try { - user = (await app.signer.getAddress()).toLowerCase(); + const address = await app.signer?.getAddress(); + user = address?.toLowerCase(); } catch (error: unknown) { console.error("Error in signer.getAddress: ", error); return false; @@ -188,8 +196,8 @@ async function checkPermitClaimable(app: AppState): Promise { export async function checkRenderMakeClaimControl(app: AppState) { try { - const address = await app.signer.getAddress(); - const user = address.toLowerCase(); + const address = await app.signer?.getAddress(); + const user = address?.toLowerCase(); if (app.reward) { const beneficiary = app.reward.transferDetails.to.toLowerCase(); @@ -207,8 +215,8 @@ export async function checkRenderMakeClaimControl(app: AppState) { export async function checkRenderInvalidatePermitAdminControl(app: AppState) { try { - const address = await app.signer.getAddress(); - const user = address.toLowerCase(); + const address = await app.signer?.getAddress(); + const user = address?.toLowerCase(); if (app.reward) { const owner = app.reward.owner.toLowerCase(); @@ -266,7 +274,11 @@ async function isNonceClaimed(app: AppState): Promise { return bit.and(flipped).eq(0); } -async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { +async function invalidateNonce(signer: JsonRpcSigner | null, nonce: BigNumberish): Promise { + if (!signer) { + console.error("Signer is null"); + return; + } const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); const { wordPos, bitPos } = nonceBitmap(nonce); // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428