chore: add changes for 1.4.1 with experimental farcaster actions

This commit is contained in:
Andrei O 2024-08-16 12:50:15 +03:00
parent f028919d68
commit 9dfd146615
No known key found for this signature in database
GPG Key ID: B961E5B68389457E
12 changed files with 884 additions and 32 deletions

View File

@ -3,8 +3,8 @@
"name": "__MSG_appName__",
"description": "__MSG_appDesc__",
"default_locale": "en",
"version": "1.4.0",
"version_name": "1.4.0",
"version": "1.4.1",
"version_name": "1.4.1",
"icons": {
"16": "assets/extension-icon/wallet_16.png",
"32": "assets/extension-icon/wallet_32.png",

View File

@ -32,6 +32,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/request-network/:rid/:param',
component: () => import('@/views/RequestNetwork.vue'),
},
{
path: '/farcaster-actions',
component: () => import('@/views/FarcasterActions.vue'),
},
{
path: '/tabs/',
component: AppTabs,

21
src/utils/abis.ts Normal file
View File

@ -0,0 +1,21 @@
export const FARCASTER_PARTIAL_KEY_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "idOf",
"outputs": [
{
"internalType": "uint256",
"name": "fid",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]

159
src/utils/farcaster.ts Normal file
View File

@ -0,0 +1,159 @@
import { signMsg, getSelectedAddress, getOptimismProvider } from './wallet'
import { FARCASTER_PARTIAL_KEY_ABI } from './abis'
import { ethers } from 'ethers'
import { getUrl } from './platform'
import { generateApiToken } from './warpcast-auth'
const WARPCAST_BASE = 'https://client.warpcast.com/v2/'
const EP_SIGNIN = `${WARPCAST_BASE}sign-in-with-farcaster`
const FC_ID_REGISTRY_CONTRACT = '0x00000000fc6c5f01fc30151999387bb99a9f489b'
export const extractLinkData = (link: string) => {
const url = new URL(link);
const channelToken = url.searchParams.get('channelToken');
const nonce = url.searchParams.get('nonce');
const siweUri = url.searchParams.get('siweUri');
const domain = url.searchParams.get('domain');
const notBefore = url.searchParams.get('notBefore');
const expirationTime = url.searchParams.get('expirationTime');
return {
channelToken,
nonce,
siweUri,
domain,
notBefore,
expirationTime,
} as {
channelToken: string,
nonce: string,
siweUri: string,
domain: string,
notBefore: string,
expirationTime: string,
}
}
export const validateLinkData = (link: string) => {
const { channelToken, nonce, siweUri, domain, notBefore, expirationTime } = extractLinkData(link);
if (!channelToken || !nonce || !siweUri || !domain || !notBefore || !expirationTime) {
return false;
}
return true;
}
export const constructWarpcastSWIEMsg = ({
siweUri,
domain,
nonce,
notBefore,
expirationTime,
fid,
custodyAddress
}: {
siweUri: string,
domain: string,
nonce: string,
notBefore: string,
expirationTime: string,
fid: number,
custodyAddress: string
}) => {
return `${domain} wants you to sign in with your Ethereum account:\n${custodyAddress}\n\nFarcaster Auth\n\nURI: ${siweUri}\nVersion: 1\nChain ID: 10\nNonce: ${nonce}\nIssued At: ${notBefore}\nExpiration Time: ${expirationTime}\nNot Before: ${notBefore}\nResources:\n- farcaster://fid/${fid}`
}
export const signInWithFarcaster = async ({
channelToken,
message,
signature,
authToken
} : {
channelToken: string,
message: string,
signature: string,
authToken: string
}) => {
const response = await fetch(`${EP_SIGNIN}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
channelToken,
message,
signature,
})
});
return response.json();
}
const noFidNotification = () => {
const messageId = Math.floor(Math.random() * 1000000);
chrome.notifications.create('no-fid', {
type: 'basic',
iconUrl: getUrl('assets/extension-icon/wallet_128.png'),
title: 'Error',
message: 'This addres does not own any FID please select custody address that owns your FID.\nMessage ID: ' + messageId
});
}
export const getFidFromAddress = async (address: string) : Promise<number | null> => {
const provider = await getOptimismProvider();
const contract = new ethers.Contract(FC_ID_REGISTRY_CONTRACT, FARCASTER_PARTIAL_KEY_ABI, provider);
const FID = await contract.idOf(address);
if (FID > 0) {
return FID;
}
noFidNotification();
return 0;
}
export const doSignInWithFarcaster = async ({
link
}: {
link: string
}) => {
const { channelToken, nonce, siweUri, domain, notBefore, expirationTime } = extractLinkData(link);
const custodyAddress = (await getSelectedAddress())?.[0] || '';
const fid = custodyAddress && await getFidFromAddress(custodyAddress);
if (!fid) {
return -1;
}
const message = constructWarpcastSWIEMsg({
siweUri,
domain,
nonce,
notBefore,
expirationTime,
fid,
custodyAddress
});
const genToken = await generateApiToken();
let authToken = '';
if(genToken.success) {
authToken = genToken.data;
}
console.log('authToken', authToken);
if (!authToken) {
return -2;
}
const signature = await signMsg(message);
await signInWithFarcaster({
channelToken,
message,
signature,
authToken
});
return 1
}

View File

@ -315,4 +315,4 @@ export const openTab = (url: string) => {
});
}
export const getVersion = () => chrome?.runtime?.getManifest()?.version ?? ''
export const getVersion = () => chrome?.runtime?.getManifest()?.version ?? ''

View File

@ -1,5 +1,6 @@
import { getSelectedAccount, getSelectedNetwork, numToHexStr } from '@/utils/platform';
import { ethers } from "ethers"
import { mainNets } from '@/utils/networks';
let provider: ethers.JsonRpcProvider | null = null
@ -16,6 +17,11 @@ export const getCurrentProvider = async () => {
return {provider, network}
}
export const getOptimismProvider = async () => {
const network = mainNets[10]
return new ethers.JsonRpcProvider(network.rpc, ethers.Network.from(network.chainId), { staticNetwork: true, batchMaxCount: 6, polling: false })
}
const convertReceipt = (receipt: ethers.TransactionReceipt | null) => {
if(!receipt) return null
const newReceipt = {...receipt} as any

161
src/utils/warpcast-auth.ts Normal file
View File

@ -0,0 +1,161 @@
import { signMsg } from './wallet'
import { getBytes } from 'ethers';
import bufferLib from 'buffer';
const EIP_191_PREFIX = "eip191:";
const WARPCAST_API = 'https://client.warpcast.com/v2'
const NO_WALLET = 'NO_WALLET'
const SIG_DENIED = 'SIG_DENIED'
const NO_AUTH_TOKEN = 'NO_AUTH_TOKEN'
const AUTH_SUCCESS = 'AUTH_SUCCESS'
type T_RESULT_GEN_AUTH_TOKEN = {
success: boolean;
data: typeof SIG_DENIED | typeof NO_AUTH_TOKEN | typeof AUTH_SUCCESS | typeof NO_WALLET | string;
}
type T_IDDB_VALUE = {
secret: string;
expiresAt: number;
}
function serialize (object: any) {
if (typeof object === 'number' && isNaN(object)) {
throw new Error('NaN is not allowed');
}
if (typeof object === 'number' && !isFinite(object)) {
throw new Error('Infinity is not allowed');
}
if (object === null || typeof object !== 'object') {
return JSON.stringify(object);
}
if (object.toJSON instanceof Function) {
return serialize(object.toJSON());
}
if (Array.isArray(object)) {
const values: any = object.reduce((t, cv, ci) => {
const comma = ci === 0 ? '' : ',';
const value = cv === undefined || typeof cv === 'symbol' ? null : cv;
return `${t}${comma}${serialize(value)}`;
}, '');
return `[${values}]`;
}
const values: any = Object.keys(object).sort().reduce((t, cv) => {
if (object[cv] === undefined ||
typeof object[cv] === 'symbol') {
return t;
}
const comma = t.length === 0 ? '' : ',';
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`;
}, '');
return `{${values}}`;
};
function createWarpMessage (data: any) {
return { message: serialize(data) }
}
export const generateApiToken = async (): Promise<T_RESULT_GEN_AUTH_TOKEN> => {
try {
const timestamp = Date.now();
const payload = {
method: "generateToken",
params: {
timestamp,
expiresAt: 1777046287381
},
};
const msgToSign = createWarpMessage(payload);
const sig = await signMsg(msgToSign.message);
const Buffer = bufferLib.Buffer;
const sigBase64 = Buffer.from(getBytes(sig)).toString('base64');
const cusAuth = EIP_191_PREFIX + sigBase64
const req = await fetch(`${WARPCAST_API}/auth`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cusAuth}`,
},
body: JSON.stringify(payload),
});
if (req.ok) {
const data = await req.json();
const token = data?.result?.token?.secret;
if (token) {
return { success: true, data: token }
}
return { success: false, data: NO_AUTH_TOKEN }
}
return { success: false, data: NO_AUTH_TOKEN }
} catch (error) {
console.error('Failed to generate api token', error)
return { success: false, data: NO_AUTH_TOKEN}
}
}
export const addWarpAuthToken = async (value: T_IDDB_VALUE): Promise<unknown> => {
const dbName = 'localforage'
const storeName = 'keyvaluepairs'
const key = 'auth-token'
const version = 2
let resolve = (a = false) => {}
const result = new Promise((res) => {
resolve = res
})
try {
const dbRequest = indexedDB.open(dbName, version);
dbRequest.onupgradeneeded = (event: any) => {
const db = event.target.result;
db.createObjectStore(storeName);
};
dbRequest.onsuccess = (event: any) => {
const db = dbRequest.result
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const request = store.put(value, key);
request.onsuccess = (event: any) => {
console.log("Successfully added data:", event.target.result);
window?.location?.reload()
resolve?.()
}
request.onerror = (event: any) => {
console.error("Error adding data:", event.target.error);
resolve?.()
}
};
dbRequest.onerror = (event: any) => {
console.error("Error adding data:", event.target.error);
resolve?.()
};
} catch (error) {
console.error("Error accessing IndexedDB:", error);
resolve?.()
}
return result
}

View File

@ -0,0 +1,468 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="onCancel">Back</ion-button>
</ion-buttons>
<ion-title>Farcaster Actions</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label style="opacity: 0.9; font-size: 0.85rem"
>Selected address needs to own a FID in order to work, this address is also
known as custody address.
</ion-label>
</ion-item>
<ion-item>
<ion-label style="opacity: 0.9; font-size: 0.85rem"
>These are experimental features RE from Warpcast might not work in all cases
and might break if WC makes changes.</ion-label
>
</ion-item>
<div
style="border: 1px solid var(--ion-color-medium-contrast);
margin: 0.6rem;
}"
>
<ion-item>
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label>
<ion-button
@click="
() => {
accountsModal = true;
toastState = false;
}
"
>Select</ion-button
>
</ion-item>
<ion-item button>
<p style="font-size: 0.7rem; color: coral">{{ selectedAccount?.address }}</p>
</ion-item>
</div>
<ion-item>
<ion-label style="opacity: 0.9; font-size: 0.85rem"
>Used for sign in with farcaster/warpcast QR you'll need to paste the deep link
in next screen</ion-label
></ion-item
>
<ion-item>
<ion-button
@click="swiwModal = true"
color="light"
style="
margin: auto;
transform: scale(1.2);
filter: hue-rotate(59deg) saturate(1.5) sepia(0.1);
"
>Sign in with farcaster</ion-button
>
</ion-item>
<ion-item>
<ion-label style="opacity: 0.9; font-size: 0.85rem"
>Used to login on warpcast.com without needing a mobile device</ion-label
>
</ion-item>
<ion-item>
<ion-button
@click="promptForSignIn"
style="
margin: auto;
transform: scale(1.2);
filter: hue-rotate(59deg) saturate(1.5) sepia(0.1);
"
color="light"
>Login on Warpcast.com</ion-button
>
</ion-item>
<ion-alert
:is-open="alertOpen"
header="Error"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
<ion-modal :is-open="accountsModal">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="accountsModal = false">Close</ion-button>
</ion-buttons>
<ion-title>Select</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list style="margin-bottom: 4rem">
<ion-radio-group :value="selectedAccount?.address ?? ''">
<ion-list-header>
<ion-label>Accounts</ion-label>
</ion-list-header>
<ion-list
@click="changeSelectedAccount(account.address)"
class="ion-padding"
v-for="account of accounts"
:key="account.address"
button
>
<ion-item>
<ion-radio
:aria-label="account.name"
slot="start"
:value="account.address"
>{{ account.name }}</ion-radio
>
</ion-item>
<ion-item>
<ion-text style="font-size: 0.7rem; color: coral">{{
account.address
}}</ion-text>
</ion-item>
</ion-list>
</ion-radio-group>
</ion-list>
</ion-content>
</ion-modal>
<ion-modal :is-open="swiwModal" @didDismiss="deepLink = ''">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="swiwModal = false">Close</ion-button>
</ion-buttons>
<ion-title>Paste Link To Authorize</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label
>Enter deep-link from Sign in with farcaster QR EX:
<span style="font-size: 0.8rem; opacity: 0.8">
https://warpcast.com/~/sign-in-with-farcaster?channelToken=4a8d3f27-....
</span></ion-label
>
</ion-item>
<ion-item>
<ion-textarea
style="overflow-y: scroll; width: 100%"
aria-label="Enter deep link from Sign in with farcaste QR"
:rows="10"
:cols="10"
v-model="deepLink"
></ion-textarea>
</ion-item>
<ion-item>
<ion-button @click="swiwModal = false" color="light">Cancel</ion-button>
<ion-button @click="farcasterSWIWAithorize">Authorize</ion-button>
</ion-item>
</ion-content>
<ion-loading
:is-open="swloading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${swloading}`"
@didDismiss="swloading = false"
>
</ion-loading>
<ion-loading
:is-open="warpcastLoading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${warpcastLoading}`"
@didDismiss="warpcastLoading = false"
>
</ion-loading>
</ion-modal>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonIcon,
onIonViewWillEnter,
modalController,
IonModal,
IonButtons,
IonTextarea,
} from "@ionic/vue";
import { saveSelectedAccount, paste, replaceAccounts } from "@/utils/platform";
import router from "@/router";
import type { Account } from "@/extension/types";
import UnlockModal from "@/views/UnlockModal.vue";
import { triggerListner } from "@/extension/listners";
import { copyOutline } from "ionicons/icons";
import { clipboardOutline } from "ionicons/icons";
import {
doSignInWithFarcaster,
validateLinkData,
getFidFromAddress,
} from "@/utils/farcaster";
import { getAccounts, getSelectedAccount, unBlockLockout } from "@/utils/platform";
import { addWarpAuthToken, generateApiToken } from "@/utils/warpcast-auth";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonIcon,
IonModal,
IonButtons,
IonTextarea,
},
setup: () => {
const name = ref("");
const pk = ref("");
const alertOpen = ref(false);
const alertMsg = ref("");
const swiwModal = ref(false);
const deepLink = ref("");
const swloading = ref(false);
const warpcastLoading = ref(false);
const loading = ref(false);
const accounts = ref([]) as Ref<Account[]>;
const accountsModal = ref(false) as Ref<boolean>;
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
const toastState = ref(false);
const loadData = () => {
loading.value = true;
const pAccounts = getAccounts();
const pSelectedAccount = getSelectedAccount();
Promise.all([pAccounts, pSelectedAccount]).then((res) => {
accounts.value = res[0];
selectedAccount.value = res[1];
loading.value = false;
});
};
onIonViewWillEnter(() => {
loadData();
});
const onCancel = () => {
router.push("/tabs/home");
};
const changeSelectedAccount = async (address: string) => {
loading.value = true;
const findIndex = accounts.value.findIndex((a) => a.address == address);
if (findIndex > -1) {
selectedAccount.value = accounts.value[findIndex];
accounts.value = accounts.value.filter((a) => a.address !== address);
accounts.value.unshift(selectedAccount.value);
const newAccounts = [...accounts.value];
await Promise.all([
saveSelectedAccount(selectedAccount.value),
replaceAccounts(newAccounts),
]);
triggerListner("accountsChanged", [newAccounts.map((a) => a.address)?.[0]]);
}
accountsModal.value = false;
loading.value = false;
};
const farcasterSWIWAithorize = async () => {
if (!deepLink.value) {
alertMsg.value = "Please enter the deep link";
alertOpen.value = true;
return;
}
const linkData = validateLinkData(deepLink.value);
if (!linkData) {
alertMsg.value = "Invalid deep link";
alertOpen.value = true;
return;
}
if ((selectedAccount.value.pk ?? "").length !== 66) {
const modalResult = await openModal();
if (modalResult) {
unBlockLockout();
loading.value = true;
} else {
onCancel();
}
} else {
unBlockLockout();
}
swloading.value = true;
try {
const result = await doSignInWithFarcaster({
link: deepLink.value,
});
if (result === -1) {
alertMsg.value =
"Selected account does not own a FID please select an account that owns a FID";
alertOpen.value = true;
swloading.value = false;
return;
} else if (result === -2) {
alertMsg.value = "Optimism RCP is not available";
alertOpen.value = true;
swloading.value = false;
return;
}
} catch (e) {
alertMsg.value = String(e);
alertOpen.value = true;
}
swloading.value = false;
router.push("/tabs/home");
};
const promptForSignIn = async () => {
const targetUrl = "warpcast.com";
chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) {
const lastTab = tabs[0];
if (!lastTab) {
alertMsg.value = "No active tab found";
alertOpen.value = true;
return;
}
if (!lastTab?.url?.includes(targetUrl)) {
alertOpen.value = true;
alertMsg.value = "You are not on warpcast.com page";
return;
}
if (!lastTab.id) {
alertMsg.value = "No active tab found";
alertOpen.value = true;
return;
}
if ((selectedAccount.value.pk ?? "").length !== 66) {
const modalResult = await openModal();
if (modalResult) {
unBlockLockout();
loading.value = true;
} else {
onCancel();
}
} else {
unBlockLockout();
}
warpcastLoading.value = true;
let hasFid = 0 as number | null;
try {
hasFid = await getFidFromAddress(selectedAccount.value.address);
} catch (e) {
alertMsg.value = String(e);
alertOpen.value = true;
warpcastLoading.value = false;
return;
}
if (!hasFid) {
alertMsg.value =
"Selected account does not own a FID please select an account that owns a FID";
alertOpen.value = true;
warpcastLoading.value = false;
return;
}
let token = "";
try {
const data = await generateApiToken();
if (data.success) {
token = data.data;
} else {
alertMsg.value = `Error in generating Auth token: ${data.data}`;
alertOpen.value = true;
warpcastLoading.value = false;
return;
}
} catch (e) {
alertMsg.value = String(e);
alertOpen.value = true;
warpcastLoading.value = false;
return;
}
const arg = { secret: token, expiresAt: 1777046287381 };
chrome.scripting.executeScript({
target: { tabId: lastTab.id },
func: addWarpAuthToken,
args: [arg],
});
window.close();
});
};
const openModal = async () => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: "transaction",
},
});
modal.present();
const { role } = await modal.onWillDismiss();
if (role === "confirm") return true;
return false;
};
return {
name,
pk,
onCancel,
alertOpen,
alertMsg,
clipboardOutline,
paste,
accountsModal,
changeSelectedAccount,
selectedAccount,
accounts,
copyOutline,
toastState,
deepLink,
swiwModal,
farcasterSWIWAithorize,
swloading,
promptForSignIn,
warpcastLoading,
};
},
});
</script>

View File

@ -137,6 +137,11 @@
</p>
</div>
</ion-item>
<ion-item style="margin-top: 0.3rem; margin-bottom: 0.3rem; text-align: center">
<ion-button @click="goToFarcasterActions" expand="block"
>Experimental Farcaster Wallet Actions</ion-button
>
</ion-item>
<ion-loading
:is-open="loading"
@ -368,6 +373,10 @@ export default defineComponent({
router.push("/tabs/add-network");
};
const goToFarcasterActions = () => {
router.push("/farcaster-actions");
};
const changeSelectedAccount = async (address: string) => {
loading.value = true;
const findIndex = accounts.value.findIndex((a) => a.address == address);
@ -421,6 +430,7 @@ export default defineComponent({
openTab,
settings,
version,
goToFarcasterActions,
};
},
});

View File

@ -143,6 +143,9 @@ export default defineComponent({
const onSign = async () => {
loading.value = true;
if (interval) {
clearInterval(interval);
}
const selectedAccount = await getSelectedAccount();
loading.value = false;
if ((selectedAccount.pk ?? "").length !== 66) {

View File

@ -270,6 +270,29 @@ export default defineComponent({
signTxData.value = JSON.stringify(paramsWithoutZeros, null, 2);
}
const setItervalFn = async () => {
if (timerReject.value <= 0) {
onCancel();
return;
}
if (gasPriceReFetch.value) {
timerFee.value -= 1;
if (timerFee.value <= 0) {
timerFee.value = 20;
loading.value = true;
const { feed, price } = await getGasPrice();
gasFeed = feed;
gasPrice.value = parseFloat(price.toString() ?? 0.1);
await newGasData();
loading.value = false;
}
}
timerReject.value -= 1;
bars.value++;
walletPing();
};
const openModal = async () => {
const modal = await modalController.create({
component: UnlockModal,
@ -285,6 +308,9 @@ export default defineComponent({
const onSign = async () => {
loading.value = true;
if (interval) {
clearInterval(interval);
}
const selectedAccount = await getSelectedAccount();
loading.value = false;
if ((selectedAccount.pk ?? "").length !== 66) {
@ -370,28 +396,7 @@ export default defineComponent({
await newGasData();
loading.value = false;
interval = setInterval(async () => {
if (timerReject.value <= 0) {
onCancel();
return;
}
if (gasPriceReFetch.value) {
timerFee.value -= 1;
if (timerFee.value <= 0) {
timerFee.value = 20;
loading.value = true;
const { feed, price } = await getGasPrice();
gasFeed = feed;
gasPrice.value = parseFloat(price.toString() ?? 0.1);
await newGasData();
loading.value = false;
}
}
timerReject.value -= 1;
bars.value++;
walletPing();
}, 1000) as any;
interval = setInterval(setItervalFn, 1000) as any;
});
const setGasLimit = () => {

View File

@ -154,14 +154,29 @@ export default defineComponent({
}
};
onMounted(() => {
requestAnimationFrame(async () => {
if (passInput.value) {
await new Promise((resolve) => setTimeout(resolve, 50));
console.log("passInput.value", passInput.value);
onMounted(async () => {
console.log("rendered");
if (passInput.value) {
await new Promise((resolve) => setTimeout(resolve, 50));
passInput.value.$el.setFocus();
passInput.value.$el.addEventListener("keyup", (e: any) => {
if (e.key === "Enter") {
unlock();
}
});
passInput.value.$el.addEventListener("blur", () => {
passInput.value.$el.setFocus();
}
});
passInput.value.$el.selectionStart = passInput.value?.$el.value.length;
});
}
// requestAnimationFrame(async () => {
// if (passInput.value) {
// await new Promise((resolve) => setTimeout(resolve, 50));
// console.log("passInput.value", passInput.value);
// passInput.value.$el.setFocus();
// }
// });
});
return {