From 4abab21230f9e021474ab4704d5e57686915a844 Mon Sep 17 00:00:00 2001
From: mtyszczak <mateusz.tyszczak@gmail.com>
Date: Wed, 19 Mar 2025 16:50:51 +0100
Subject: [PATCH] Add request account creation

---
 src/components/hive/PublicKey.vue             |   5 +-
 src/components/navigation/AppSidebar.vue      |  29 +++-
 .../wallets/metamask/MetamaskConnect.vue      |  17 ++-
 .../utilcards/ConfirmCreateAccountCard.vue    |   4 +-
 .../utilcards/RequestAccountCreate.vue        | 137 ++++++++++++++++++
 src/pages/account/request.vue                 |   9 ++
 src/utils/router.ts                           |   2 +
 7 files changed, 194 insertions(+), 9 deletions(-)
 create mode 100644 src/components/utilcards/RequestAccountCreate.vue
 create mode 100644 src/pages/account/request.vue

diff --git a/src/components/hive/PublicKey.vue b/src/components/hive/PublicKey.vue
index f3f06e5..ba3a015 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 d879c59..14850a5 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 b71a366..40e946a 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 bc31d83..8c3384f 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 0000000..4ce863b
--- /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 0000000..6912eea
--- /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 86dac72..43a5348 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 }
 ];
-- 
GitLab