diff --git a/src/components/hive/PublicKey.vue b/src/components/hive/PublicKey.vue index f3f06e5aefe45d17d55642c51889bb613efc3c80..ba3a0159c97c50ec936115496971a4ac82c8a8bb 100644 --- a/src/components/hive/PublicKey.vue +++ b/src/components/hive/PublicKey.vue @@ -6,6 +6,7 @@ const props = defineProps<{ afterValue?: string; context?: number; disableCopy?: boolean; + class?: string; }>(); const context = props.context ?? 6; @@ -16,8 +17,8 @@ const value = String(props.value); <template> <div class="flex items-center"> <span class="font-mono pt-[2px] mr-1"> - <span v-if="context > 0">{{ value.slice(0, context) }}...{{ value.slice(-context) }}</span> - <span v-else>{{ value }}</span> + <span v-if="context > 0" :class="props.class">{{ value.slice(0, context) }}...{{ value.slice(-context) }}</span> + <span v-else :class="props.class">{{ value }}</span> <span class="ml-2" v-if="props.afterValue">{{ props.afterValue }}</span> </span> <Button v-if="!props.disableCopy" :value="value"/> diff --git a/src/components/navigation/AppSidebar.vue b/src/components/navigation/AppSidebar.vue index d879c593133f24021156ee7f68f27375340d0d9a..14850a546c29abb35afdbf343e2eb5ba1cd33b47 100644 --- a/src/components/navigation/AppSidebar.vue +++ b/src/components/navigation/AppSidebar.vue @@ -1,13 +1,20 @@ <script setup lang="ts"> -import { mdiHomeOutline, mdiMessageLockOutline, mdiFileSign, mdiAccountPlusOutline, mdiAccountArrowUpOutline } from "@mdi/js" +import { mdiHomeOutline, mdiMessageLockOutline, mdiFileSign, mdiAccountPlusOutline, mdiAccountArrowUpOutline, mdiAccountReactivateOutline } from "@mdi/js" import { Sidebar, SidebarContent, SidebarHeader, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar" import { useSidebar } from "@/components/ui/sidebar"; import { useRouter } from "vue-router"; +import { Button } from "@/components/ui/button"; +import { useWalletStore } from "@/stores/wallet.store"; +import { computed } from "vue"; const router = useRouter(); const { toggleSidebar, isMobile } = useSidebar(); +const walletStore = useWalletStore(); + +const isDisabledMenuButton = computed(() => !walletStore.hasWallet); + const groups = [{ title: "Account management", items: [ @@ -16,15 +23,22 @@ const groups = [{ url: "/", icon: mdiHomeOutline, }, + { + title: "Request Account Creation", + url: "/account/request", + icon: mdiAccountPlusOutline, + }, { title: "Process Account Creation", url: "/account/create", - icon: mdiAccountPlusOutline, + icon: mdiAccountReactivateOutline, + disabled: isDisabledMenuButton, }, { title: "Process Authority Update", url: "/account/update", icon: mdiAccountArrowUpOutline, + disabled: isDisabledMenuButton, }, ] }, { @@ -42,6 +56,13 @@ const groups = [{ }, ] }]; + +const navigateTo = (url: string) => { + router.push(url); + + if (isMobile.value) + toggleSidebar(); +}; </script> <template> @@ -59,10 +80,10 @@ const groups = [{ <SidebarMenu> <SidebarMenuItem v-for="item in group.items" :key="item.title"> <SidebarMenuButton asChild :class="{ 'bg-primary/5': router.currentRoute.value.path === item.url }"> - <RouterLink @click="isMobile && toggleSidebar()" :to="item.url"> + <Button variant="ghost" :disabled="item.disabled?.value" @click="navigateTo(item.url)" class="flex justify-start"> <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsl(var(--foreground))" :d="item.icon"/></svg> <span class="text-foreground/80">{{item.title}}</span> - </RouterLink> + </Button> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue index b71a366ba719edcd1f9be8744bc22ed1ffbe2f93..40e946ad5ed33e8428f5939cff0303cd9762d730 100644 --- a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue +++ b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue @@ -154,8 +154,12 @@ const generateAccountUpdateTransaction = async(): Promise<string | void> => { toastError("Failed to generate account update transaction", error); } }; + +const hasCopiedCreateSignLink = ref(false); + const getAccountCreateSigningLink = (): string => { const accountName = createAccountNameOperation.value!.startsWith('@') ? createAccountNameOperation.value!.slice(1) : createAccountNameOperation.value!; + hasCopiedCreateSignLink.value = true; return `${window.location.protocol}//${window.location.host}/account/create?acc=${accountName}&posting=${ metamaskPublicKeys.value!.find(node => node.role === "posting")!.publicKey }&active=${ @@ -166,6 +170,9 @@ const getAccountCreateSigningLink = (): string => { metamaskPublicKeys.value!.find(node => node.role === "memo")!.publicKey }`; }; + +const hasCopiedUpdateSignLink = ref(false); + const getAuthorityUpdateSigningLink = (): string => { const accountName = updateAccountNameOperation.value!.startsWith('@') ? updateAccountNameOperation.value!.slice(1) : updateAccountNameOperation.value!; const url = new URL(`${window.location.protocol}//${window.location.host}/account/update?acc=${accountName}`); @@ -173,6 +180,8 @@ const getAuthorityUpdateSigningLink = (): string => { if (updateAuthType[key as TRole]) url.searchParams.set(key, metamaskPublicKeys.value!.find(node => node.role === key)!.publicKey); + hasCopiedUpdateSignLink.value = true; + return url.toString(); }; @@ -247,6 +256,9 @@ const updateAccountName = (value: string | any) => { <Button :disabled="isLoading || !updateAccountNameOperation" :copy="getAuthorityUpdateSigningLink" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy signing link</span> </Button> + <p v-if="hasCopiedUpdateSignLink" class="mt-4"> + Now send this link to someone who has an account to execute this operation in blockchain + </p> <Separator label="Or" class="mt-8" /> <div class="flex justify-center mt-4"> <Button :disabled="isLoading" :copy="generateAccountUpdateTransaction" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> @@ -256,7 +268,7 @@ const updateAccountName = (value: string | any) => { </div> </div> <div v-else-if="showCreateAccountModal"> - <p class="mb-4">Step 6: Fill in this form in order to prepare the operation to create an account with requested metadata. Then please copy the signing link, and send it to someone who already has an account to execute this operation in blockchain:</p> + <p class="mb-4">Step 6: Fill in this form in order to prepare the operation to request account creation</p> <div class="grid mb-2 w-full max-w-sm items-center gap-1.5"> <Label for="metamask_createAuth_account">New account name</Label> <Input v-model="createAccountNameOperation!" @update:model-value="validateAccountName()" id="metamask_createAuth_account" /> @@ -273,6 +285,9 @@ const updateAccountName = (value: string | any) => { <Button :copy="getAccountCreateSigningLink" :disabled="isLoading || !createAccountNameOperation || !accountNameValid" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy signing link</span> </Button> + <p v-if="hasCopiedCreateSignLink" class="mt-4"> + Now send this link to someone who has an account to execute this operation in blockchain + </p> </div> </div> <div v-else-if="accountsMatchingKeys.length === 0"> diff --git a/src/components/utilcards/ConfirmCreateAccountCard.vue b/src/components/utilcards/ConfirmCreateAccountCard.vue index bc31d8324d4b0c533ba60bd7333414b2d918078a..8c3384f55c39d9483d7c664925367a2a9df35dca 100644 --- a/src/components/utilcards/ConfirmCreateAccountCard.vue +++ b/src/components/utilcards/ConfirmCreateAccountCard.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { mdiAccountPlusOutline } from '@mdi/js'; +import { mdiAccountReactivateOutline } from '@mdi/js'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Input } from '@/components/ui/input' @@ -124,7 +124,7 @@ const createAccount = async() => { <CardHeader> <CardTitle class="inline-flex items-center justify-between"> <span>Process Account Creation</span> - <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsla(var(--foreground) / 80%)" :d="mdiAccountPlusOutline"/></svg> + <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsla(var(--foreground) / 80%)" :d="mdiAccountReactivateOutline"/></svg> </CardTitle> <CardDescription class="mr-8">Use this module to process account creation request sent by other users</CardDescription> </CardHeader> diff --git a/src/components/utilcards/RequestAccountCreate.vue b/src/components/utilcards/RequestAccountCreate.vue new file mode 100644 index 0000000000000000000000000000000000000000..4ce863b5c7459226a89ff3f5c6b022f138f19c1a --- /dev/null +++ b/src/components/utilcards/RequestAccountCreate.vue @@ -0,0 +1,137 @@ +<script setup lang="ts"> +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { mdiAccountPlusOutline } from '@mdi/js'; +import { computed, ref } from 'vue'; +import { toastError } from '@/utils/parse-error'; +import { getWax } from '@/stores/wax.store'; +import { useWalletStore } from '@/stores/wallet.store'; +import { useMetamaskStore } from '@/stores/metamask.store'; +import type { TRole } from '@hiveio/wax/vite'; +import { toast } from 'vue-sonner'; + +const walletStore = useWalletStore(); +const metamaskStore = useMetamaskStore(); + +const publicKeys = ref<Record<TRole, string>>({ + owner: "", + active: "", + posting: "", + memo: "" +}); +const accountNameValid = ref(false); +const createAccountNameOperation = ref(''); + +const isLoading = ref(false); +const hasCopiedCreateSignLink = ref(false); + +const validateAccountName = async() => { + try { + if(!createAccountNameOperation.value) + return accountNameValid.value = false; + + const accountName = createAccountNameOperation.value.startsWith("@") ? createAccountNameOperation.value.slice(1) : createAccountNameOperation.value; + if (!accountName) + return accountNameValid.value = false; + + const wax = await getWax(); + return accountNameValid.value = wax.isValidAccountName(accountName); + } catch (error) { + toastError("Failed to validate account name", error); + } +} + +const parseMetamaskPublicKeys = async() => { + const toastToDismiss = toast.loading("Metamask detected. Parsing public keys..."); + + try { + isLoading.value = true; + + try { + await metamaskStore.connect(); + } catch { + toast.error("Metamask is not installed or not connected"); + + return; + } + + const { publicKeys: metamaskPublicKeys } = await metamaskStore.call("hive_getPublicKeys", { + keys: [{ + role: "owner" + },{ + role: "active" + },{ + role: "posting" + },{ + role: "memo" + }] + }) as any; + + for(const publicKey of metamaskPublicKeys) + publicKeys.value[publicKey.role as TRole] = publicKey.publicKey; + + toast.success("Successfully parsed Metamask public keys"); + } catch (error) { + toastError("Failed to parse Metamask public keys", error); + + throw error; // Make sure this method throws to handle sonner toast properly + } finally { + isLoading.value = false; + + toast.dismiss(toastToDismiss); + } +}; + +const hasMetamaskWithSnap = computed(() => walletStore.walletsStatus.metamask && metamaskStore.isInstalled); + +if(hasMetamaskWithSnap) + void parseMetamaskPublicKeys(); + +const getAccountCreateSigningLink = (): string => { + const accountName = createAccountNameOperation.value!.startsWith('@') ? createAccountNameOperation.value!.slice(1) : createAccountNameOperation.value!; + hasCopiedCreateSignLink.value = true; + return `${window.location.protocol}//${window.location.host}/account/create?acc=${accountName}&${Object.values(publicKeys.value).map((key, index) => `key${index + 1}=${key}`).join('&')}`; +}; +</script> + +<template> + <Card class="w-full max-w-[600px]"> + <CardHeader> + <CardTitle class="inline-flex items-center justify-between"> + <span>Request account creation</span> + <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsla(var(--foreground) / 80%)" :d="mdiAccountPlusOutline"/></svg> + </CardTitle> + <CardDescription class="mr-8">Fill in this form in order to prepare the operation to request account creation</CardDescription> + </CardHeader> + <CardContent> + <div class="space-y-4" v-if="hasMetamaskWithSnap"> + <div class="grid mb-2 w-full items-center gap-1.5"> + <Label for="metamask_createAuth_account_card">New account name</Label> + <Input class="w-full" v-model="createAccountNameOperation!" @update:model-value="validateAccountName()" id="metamask_createAuth_account_card" /> + <span class="text-red-400" v-if="createAccountNameOperation && !accountNameValid">Invalid account name</span> + </div> + <div v-for="(_key, role) in publicKeys" :key="role" class="grid mb-2 w-full items-center gap-1.5"> + <Label :for="`metamask_createAuth_account_key_${role}_card`">{{ role[0].toUpperCase() }}{{ role.slice(1) }} key</Label> + <Input class="w-full" v-model="publicKeys[role]" :id="`metamask_createAuth_account_key_${role}_card`" /> + </div> + <Button :copy="getAccountCreateSigningLink" :disabled="isLoading || !createAccountNameOperation || !accountNameValid"> + <span class="text-md font-bold">Copy signing link</span> + </Button> + <p v-if="hasCopiedCreateSignLink"> + Now send this link to someone who has an account to execute this operation in blockchain + </p> + </div> + <div v-else class="space-y-4"> + <div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert"> + <p class="font-bold">Wallet required</p> + <p>You have to connect to Metamask wallet before continuing!</p> + </div> + <Button @click="walletStore.openWalletSelectModal()" class="w-full font-bold"> + Connect to Metamask wallet + </Button> + </div> + </CardContent> + </Card> +</template> \ No newline at end of file diff --git a/src/pages/account/request.vue b/src/pages/account/request.vue new file mode 100644 index 0000000000000000000000000000000000000000..6912eeaa65b9318658c70a2acc1cb63db5afa160 --- /dev/null +++ b/src/pages/account/request.vue @@ -0,0 +1,9 @@ +<script setup lang="ts"> +import RequestAccountCreate from '@/components/utilcards/RequestAccountCreate.vue'; +</script> + +<template> + <div class="flex p-8"> + <RequestAccountCreate /> + </div> +</template> diff --git a/src/utils/router.ts b/src/utils/router.ts index 86dac7267ba999a1dfc5573c001609793b3870c2..43a534894aff3b1cf649defb9eda27f1b79e302c 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -2,6 +2,7 @@ import Index from "@/pages/index.vue"; import SignTransaction from "@/pages/sign/transaction.vue"; import SignMessage from "@/pages/sign/message.vue"; import AccountCreate from "@/pages/account/create.vue"; +import RequestCreate from "@/pages/account/request.vue"; import AccountUpdate from "@/pages/account/update.vue"; export const routes = [ @@ -9,5 +10,6 @@ export const routes = [ { path: '/sign/transaction', component: SignTransaction }, { path: '/sign/message', component: SignMessage }, { path: '/account/create', component: AccountCreate }, + { path: '/account/request', component: RequestCreate }, { path: '/account/update', component: AccountUpdate } ];