mirror of
https://github.com/andrei0x309/clear-wallet.git
synced 2024-11-18 23:41:10 +00:00
commit
d4e93f5f87
51
package.json
51
package.json
@ -16,39 +16,38 @@
|
|||||||
"pub": "yarn build && yarn release && yarn tsx ./release-scripts/create-release.ts"
|
"pub": "yarn build && yarn release && yarn tsx ./release-scripts/create-release.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ionic/vue": "^7.8.6",
|
"@ionic/vue": "^8.2.6",
|
||||||
"@ionic/vue-router": "^7.8.6",
|
"@ionic/vue-router": "^8.2.6",
|
||||||
"core-js": "^3.37.1",
|
"core-js": "^3.38.0",
|
||||||
"ethers": "^6.13.1",
|
"ethers": "^6.13.2",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "^5.2.3",
|
"@crxjs/vite-plugin": "2.0.0-beta.25",
|
||||||
"@crxjs/vite-plugin": "^2.0.0-beta.23",
|
"@types/archiver": "^6.0.2",
|
||||||
"@types/archiver": "^5.3.4",
|
"@types/chrome": "^0.0.269",
|
||||||
"@types/chrome": "^0.0.268",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/jest": "^29.5.3",
|
"@types/node": "^22.2.0",
|
||||||
"@types/node": "^20.14.5",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.0",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^7.0.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^9.9.0",
|
||||||
"eslint-plugin-vue": "^9.26.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"http-browserify": "^1.7.0",
|
"http-browserify": "^1.7.0",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"jest": "^29.6.2",
|
"jest": "^29.7.0",
|
||||||
"sass": "^1.77.6",
|
"sass": "^1.77.8",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.2.4",
|
||||||
"tsx": "^4.15.6",
|
"tsx": "^4.17.0",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.5.4",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.4.0",
|
||||||
"vue-tsc": "^2.0.21",
|
"vue-tsc": "^2.0.29",
|
||||||
"yarn-upgrade-all": "^0.7.2"
|
"yarn-upgrade-all": "^0.7.4"
|
||||||
},
|
},
|
||||||
"disabledNativeDependencies": {
|
"disabledNativeDependencies": {
|
||||||
"@capacitor/app": "^5.0.6",
|
"@capacitor/app": "^5.0.6",
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
"name": "__MSG_appName__",
|
"name": "__MSG_appName__",
|
||||||
"description": "__MSG_appDesc__",
|
"description": "__MSG_appDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"version_name": "1.4.0",
|
"version_name": "1.4.1",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "assets/extension-icon/wallet_16.png",
|
"16": "assets/extension-icon/wallet_16.png",
|
||||||
"32": "assets/extension-icon/wallet_32.png",
|
"32": "assets/extension-icon/wallet_32.png",
|
||||||
|
@ -32,6 +32,10 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/request-network/:rid/:param',
|
path: '/request-network/:rid/:param',
|
||||||
component: () => import('@/views/RequestNetwork.vue'),
|
component: () => import('@/views/RequestNetwork.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/farcaster-actions',
|
||||||
|
component: () => import('@/views/FarcasterActions.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tabs/',
|
path: '/tabs/',
|
||||||
component: AppTabs,
|
component: AppTabs,
|
||||||
|
21
src/utils/abis.ts
Normal file
21
src/utils/abis.ts
Normal 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
159
src/utils/farcaster.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import { getSelectedAccount, getSelectedNetwork, numToHexStr } from '@/utils/platform';
|
import { getSelectedAccount, getSelectedNetwork, numToHexStr } from '@/utils/platform';
|
||||||
import { ethers } from "ethers"
|
import { ethers } from "ethers"
|
||||||
|
import { mainNets } from '@/utils/networks';
|
||||||
|
|
||||||
let provider: ethers.JsonRpcProvider | null = null
|
let provider: ethers.JsonRpcProvider | null = null
|
||||||
|
|
||||||
@ -16,6 +17,11 @@ export const getCurrentProvider = async () => {
|
|||||||
return {provider, network}
|
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) => {
|
const convertReceipt = (receipt: ethers.TransactionReceipt | null) => {
|
||||||
if(!receipt) return null
|
if(!receipt) return null
|
||||||
const newReceipt = {...receipt} as any
|
const newReceipt = {...receipt} as any
|
||||||
|
161
src/utils/warpcast-auth.ts
Normal file
161
src/utils/warpcast-auth.ts
Normal 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
|
||||||
|
}
|
468
src/views/FarcasterActions.vue
Normal file
468
src/views/FarcasterActions.vue
Normal 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>
|
@ -137,6 +137,11 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ion-item>
|
</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
|
<ion-loading
|
||||||
:is-open="loading"
|
:is-open="loading"
|
||||||
@ -368,6 +373,10 @@ export default defineComponent({
|
|||||||
router.push("/tabs/add-network");
|
router.push("/tabs/add-network");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goToFarcasterActions = () => {
|
||||||
|
router.push("/farcaster-actions");
|
||||||
|
};
|
||||||
|
|
||||||
const changeSelectedAccount = async (address: string) => {
|
const changeSelectedAccount = async (address: string) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const findIndex = accounts.value.findIndex((a) => a.address == address);
|
const findIndex = accounts.value.findIndex((a) => a.address == address);
|
||||||
@ -421,6 +430,7 @@ export default defineComponent({
|
|||||||
openTab,
|
openTab,
|
||||||
settings,
|
settings,
|
||||||
version,
|
version,
|
||||||
|
goToFarcasterActions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -143,6 +143,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
const onSign = async () => {
|
const onSign = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
const selectedAccount = await getSelectedAccount();
|
const selectedAccount = await getSelectedAccount();
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if ((selectedAccount.pk ?? "").length !== 66) {
|
if ((selectedAccount.pk ?? "").length !== 66) {
|
||||||
|
@ -270,6 +270,29 @@ export default defineComponent({
|
|||||||
signTxData.value = JSON.stringify(paramsWithoutZeros, null, 2);
|
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 openModal = async () => {
|
||||||
const modal = await modalController.create({
|
const modal = await modalController.create({
|
||||||
component: UnlockModal,
|
component: UnlockModal,
|
||||||
@ -285,6 +308,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
const onSign = async () => {
|
const onSign = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
const selectedAccount = await getSelectedAccount();
|
const selectedAccount = await getSelectedAccount();
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if ((selectedAccount.pk ?? "").length !== 66) {
|
if ((selectedAccount.pk ?? "").length !== 66) {
|
||||||
@ -370,28 +396,7 @@ export default defineComponent({
|
|||||||
await newGasData();
|
await newGasData();
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(setItervalFn, 1000) as any;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setGasLimit = () => {
|
const setGasLimit = () => {
|
||||||
|
@ -154,14 +154,29 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
requestAnimationFrame(async () => {
|
console.log("rendered");
|
||||||
if (passInput.value) {
|
if (passInput.value) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
console.log("passInput.value", passInput.value);
|
|
||||||
passInput.value.$el.setFocus();
|
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 {
|
return {
|
||||||
|
Loading…
Reference in New Issue
Block a user