diff --git a/src/App.vue b/src/App.vue index ba8345896dc89b45e1de2fc87c9e865c96254e53..31d2b941a3509fe0e4f4fdf19007bf63f3403c57 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,8 +6,9 @@ import AppSidebar from '@/components/sidebar'; import { SidebarProvider } from '@/components/ui/sidebar'; import ToggleSidebar from './components/sidebar/ToggleSidebar.vue'; import { Toaster } from 'vue-sonner'; -import { useUserStore } from './stores/user.store'; -import { getWax } from './stores/wax.store'; +import { useUserStore } from '@/stores/user.store'; +import { getWax } from '@/stores/wax.store'; +import AppHeader from '@/components/AppHeader.vue'; const WalletOnboarding = defineAsyncComponent(() => import('@/components/onboarding/index')); @@ -49,8 +50,8 @@ const complete = async(data: { account: string; wallet: UsedWallet }) => { <ToggleSidebar class="m-3" /> <RouterView /> </main> - <aside v-if="settingsStore.isLoaded && !hasUser" class="fixed inset-0 flex items-center justify-center z-20"> - <WalletOnboarding @complete="complete" /> + <aside v-if="walletStore.isWalletSelectModalOpen" class="fixed inset-0 flex items-center justify-center z-20"> + <WalletOnboarding @close="walletStore.closeWalletSelectModal()" @complete="complete" /> </aside> </SidebarProvider> <Toaster theme="dark" closeButton richColors /> diff --git a/src/components/onboarding/OnboardingWalletButton.vue b/src/components/onboarding/OnboardingWalletButton.vue index c23034c37ed792fbe8089c364ba513036eb334aa..0ee361ff056325ed57f34a84c9fd7b85604cff76 100644 --- a/src/components/onboarding/OnboardingWalletButton.vue +++ b/src/components/onboarding/OnboardingWalletButton.vue @@ -11,6 +11,7 @@ const props = defineProps<{ description: string; disabled?: boolean; downloadUrl: string; + downloadUrlTriggersClick?: boolean; }>(); const emit = defineEmits(['click']); @@ -28,7 +29,7 @@ const emit = defineEmits(['click']); <TooltipProvider :delayDuration="200" disableHoverableContent> <Tooltip> <TooltipTrigger class="absolute right-4 top-1/2 transform -translate-y-1/2 w-8 h-8"> - <a :href="props.downloadUrl" v-if="props.disabled" target="_blank"> + <a :href="props.downloadUrl" @click="props.downloadUrlTriggersClick && emit('click')" v-if="props.disabled" target="_blank"> <Button variant="ghost" class="w-8 h-8 p-0"> <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsl(var(--foreground))" :d="mdiDownload"/></svg> </Button> diff --git a/src/components/onboarding/SelectWallet.vue b/src/components/onboarding/SelectWallet.vue index ebb956b6d38cb25a860a56ed165a1fdd83bae0ff..06094a17fba9f0d5f92fe0d8e2cc19449e9c38a9 100644 --- a/src/components/onboarding/SelectWallet.vue +++ b/src/components/onboarding/SelectWallet.vue @@ -1,52 +1,45 @@ <script setup lang="ts"> import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import OnboardingButton from "@/components/onboarding/OnboardingWalletButton.vue"; -import { onMounted, onUnmounted, ref } from 'vue'; -import { useMetamaskStore } from "@/stores/metamask.store"; +import { Button } from "@/components/ui/button"; import { getWalletIcon, UsedWallet } from "@/stores/settings.store"; +import { useWalletStore } from "@/stores/wallet.store"; +import { mdiClose } from "@mdi/js"; +import { computed } from "vue"; -const hasMetamask = ref(false); -const hasKeychain = ref(false); -const hasPeakVault = ref(false); -let timeoutId: number; +const walletStore = useWalletStore(); +const walletsStatus = computed(() => walletStore.walletsStatus); -const metamaskStore = useMetamaskStore(); - -const checkForWallets = () => { - if (!hasMetamask.value) - metamaskStore.connect().then(() => hasMetamask.value = true).catch(console.error); - if (!hasKeychain.value) - hasKeychain.value = "hive_keychain" in window; - if (!hasPeakVault.value) - hasPeakVault.value = "peakvault" in window; -}; - -onMounted(() => { - timeoutId = setTimeout(() => checkForWallets(), 1500) as unknown as number; - checkForWallets(); -}); - -onUnmounted(() => { - clearTimeout(timeoutId); -}); - -const emit = defineEmits(["walletSelect"]); +const emit = defineEmits(["walletSelect", "close"]); const useWallet = (type: UsedWallet) => { emit("walletSelect", type); }; + +const close = () => { + emit("close"); +}; </script> <template> <Card class="w-[350px]"> <CardHeader> - <CardTitle>Select wallet</CardTitle> - <CardDescription>We support multiple on-chain wallets</CardDescription> + <CardTitle> + <div class="inline-flex justify-between w-full"> + <div class="inline-flex items-center"> + <span class="mt-[2px]">Select wallet</span> + </div> + <Button variant="ghost" size="sm" class="px-2" @click="close"> + <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsl(var(--foreground))" :d="mdiClose"/></svg> + </Button> + </div> + </CardTitle> + <CardDescription>If your wallet is not detected, try unlocking it and refreshing the page</CardDescription> </CardHeader> <CardContent class="space-y-2"> - <OnboardingButton downloadUrl="https://docs.metamask.io/snaps/get-started/install-flask/" :disabled="!hasMetamask" @click="useWallet(UsedWallet.METAMASK)" :logoUrl="getWalletIcon(UsedWallet.METAMASK)" name="Metamask" description="Use your derived keys"/> - <OnboardingButton downloadUrl="https://hive-keychain.com/" :disabled="!hasKeychain" @click="useWallet(UsedWallet.KEYCHAIN)" :logoUrl="getWalletIcon(UsedWallet.KEYCHAIN)" name="Keychain" description="Use already imported accounts"/> - <OnboardingButton downloadUrl="https://vault.peakd.com/peakvault/guide.html#installation" :disabled="!hasPeakVault" @click="useWallet(UsedWallet.PEAKVAULT)" :logoUrl="getWalletIcon(UsedWallet.PEAKVAULT)" name="PeakVault" description="Use already imported accounts"/> + <OnboardingButton downloadUrl="https://docs.metamask.io/snaps/get-started/install-flask/" :disabled="!walletsStatus.metamask" @click="useWallet(UsedWallet.METAMASK)" :logoUrl="getWalletIcon(UsedWallet.METAMASK)" name="Metamask" description="Use your derived keys"/> + <OnboardingButton downloadUrl="https://hive-keychain.com/" :disabled="!walletsStatus.keychain" @click="useWallet(UsedWallet.KEYCHAIN)" :logoUrl="getWalletIcon(UsedWallet.KEYCHAIN)" name="Keychain" description="Use already imported accounts"/> + <OnboardingButton downloadUrl="https://vault.peakd.com/peakvault/guide.html#installation" :disabled="!walletsStatus.peakvault" @click="useWallet(UsedWallet.PEAKVAULT)" :logoUrl="getWalletIcon(UsedWallet.PEAKVAULT)" name="PeakVault" description="Use already imported accounts"/> </CardContent> <CardFooter></CardFooter> </Card> diff --git a/src/components/onboarding/WalletOnboarding.vue b/src/components/onboarding/WalletOnboarding.vue index 0a0135befa92462f8a68d99b754d3e85e2f09b0d..7c77292f8515ffeca756d4cea512a3b4ee638e78 100644 --- a/src/components/onboarding/WalletOnboarding.vue +++ b/src/components/onboarding/WalletOnboarding.vue @@ -7,7 +7,7 @@ import KeychainConnect from '@/components/onboarding/wallets/keychain/KeychainCo import MetamaskConnect from '@/components/onboarding/wallets/metamask/MetamaskConnect.vue'; import ThankYou from '@/components/onboarding/ThankYou.vue'; -const emit = defineEmits(["complete"]); +const emit = defineEmits(["complete", "close"]); const selectedWallet = ref<UsedWallet | null>(null); const selectedAccount = ref<string | null>(null); @@ -49,7 +49,7 @@ const backToStage1 = () => { <template> <div class="bg-black/30 backdrop-blur-sm h-full w-full z-50 flex items-center justify-center"> <div class="onboarding-container"> - <SelectWallet v-if="stage_1_SelectWallet" @walletSelect="walletSelect" /> + <SelectWallet v-if="stage_1_SelectWallet" @close="emit('close')" @walletSelect="walletSelect" /> <div v-if="stage_2_ConnectWallet"> <KeychainConnect v-if="selectedWallet === UsedWallet.KEYCHAIN" @close="backToStage1" @setaccount="setAccount" /> <PeakVaultConnect v-if="selectedWallet === UsedWallet.PEAKVAULT" @close="backToStage1" @setaccount="setAccount" /> diff --git a/src/components/sidebar/AppSidebar.vue b/src/components/sidebar/AppSidebar.vue index 72ae1ef5568e47bf6261ed5fc4c48477f25ce3ca..896dad44e6ae80ead362b60582eca08f17ce1678 100644 --- a/src/components/sidebar/AppSidebar.vue +++ b/src/components/sidebar/AppSidebar.vue @@ -8,6 +8,8 @@ import { computed } from 'vue'; import { Button } from '@/components/ui/button'; import { useUserStore } from "@/stores/user.store"; import ThemeSwitch from "../ui/theme-switch"; +import { useWalletStore } from "@/stores/wallet.store"; +import { useRouter } from "vue-router"; const settingsStore = useSettingsStore(); const hasUser = computed(() => settingsStore.settings.account !== undefined); @@ -17,58 +19,77 @@ const logout = () => { window.location.reload(); }; +const router = useRouter(); + +const walletStore = useWalletStore(); + const { toggleSidebar, isMobile } = useSidebar(); const userStore = useUserStore(); -const items = [ - { - title: "Home", - url: "/", - icon: mdiHomeOutline, - }, - { - title: "Memo encryption", - url: "/sign/message", - icon: mdiMessageLockOutline, - }, - { - title: "Transaction signing", - url: "/sign/transaction", - icon: mdiFileSign, - }, - { - title: "Process Account Creation", - url: "/account/create", - icon: mdiAccountPlusOutline, - }, +const groups = [{ + title: "Account management", + items: [ + { + title: "Home", + url: "/", + icon: mdiHomeOutline, + }, + { + title: "Process Account Creation", + url: "/account/create", + icon: mdiAccountPlusOutline, + }, + { + title: "Process Authority Update", + url: "/account/update", + icon: mdiAccountArrowUpOutline, + }, + ] +}, { + title: "Signing", + items: [ { - title: "Process Authority Update", - url: "/account/update", - icon: mdiAccountArrowUpOutline, - } -]; + title: "Memo encryption", + url: "/sign/message", + icon: mdiMessageLockOutline, + }, + { + title: "Transaction signing", + url: "/sign/transaction", + icon: mdiFileSign, + }, + ] +}]; </script> <template> <Sidebar> <SidebarHeader class="pb-0"> + <div class="flex items-center p-2"> + <img src="/icon.svg" class="h-8 w-8" /> + <span class="text-foreground/80 font-bold text-xl ml-2">Hive Bridge</span> + </div> <div class="flex items-center rounded-lg p-2 mt-1 mx-1 bg-background/40 border"> <Avatar class="w-8 h-8 mr-2"> - <AvatarImage v-if="userStore.profileImage" :src="userStore.profileImage" /> + <AvatarImage :src="userStore.profileImage ? userStore.profileImage : '/icon.svg'" /> <AvatarFallback v-if="settingsStore.isLoaded && hasUser">{{ settingsStore.settings.account?.slice(0, 2) }}</AvatarFallback> </Avatar> <span class="font-bold max-w-[140px] truncate" v-if="settingsStore.isLoaded && hasUser">@{{ settingsStore.settings.account }}</span> <ThemeSwitch class="ml-auto w-5 h-5 mr-1" /> </div> + <Button class="bg-background/40" variant="outline" @click="settingsStore.isLoaded && hasUser ? logout : walletStore.openWalletSelectModal()"> + <img v-if="hasUser" :src="getWalletIcon(settingsStore.settings.wallet!)" class="h-6 w-6" /> + <span class="font-bold">{{ settingsStore.isLoaded && hasUser ? 'Disconnect' : 'Connect' }}</span> + </Button> </SidebarHeader> <SidebarContent> - <SidebarGroup> - <SidebarGroupLabel class="text-foreground/60">Hive Bridge</SidebarGroupLabel> + <SidebarGroup class="pb-0" v-for="group in groups" :key="group.title"> + <SidebarGroupLabel class="text-foreground/60">{{ group.title }}</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> - <SidebarMenuItem v-for="item in items" :key="item.title"> - <SidebarMenuButton asChild> + <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"> <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> @@ -80,10 +101,6 @@ const items = [ </SidebarGroup> </SidebarContent> <SidebarFooter> - <Button class="bg-background/40" variant="outline" :disabled="!settingsStore.isLoaded || !hasUser" @click="logout"> - <img v-if="hasUser" :src="getWalletIcon(settingsStore.settings.wallet!)" class="h-6 w-6" /> - <span class="font-bold">Disconnect</span> - </Button> </SidebarFooter> </Sidebar> </template> diff --git a/src/components/ui/sidebar/index.ts b/src/components/ui/sidebar/index.ts index 5bd36eef63366ee9e36d9f66abdd11258c895f24..b0a2e64b9e05601c1894f23173c125add5d63127 100644 --- a/src/components/ui/sidebar/index.ts +++ b/src/components/ui/sidebar/index.ts @@ -40,7 +40,7 @@ export const sidebarMenuButtonVariants = cva( { variants: { variant: { - default: 'hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground', + default: 'hover:bg-primary/10 hover:text-sidebar-accent-foreground', outline: 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', }, diff --git a/src/stores/wallet.store.ts b/src/stores/wallet.store.ts index 1da5f95afcfe404c0e28b2bba38423116885c27b..1ca62d96b8b5d0584b2365515dbc9b86c51f7782 100644 --- a/src/stores/wallet.store.ts +++ b/src/stores/wallet.store.ts @@ -5,14 +5,44 @@ import { useMetamaskStore } from "./metamask.store"; import { createKeychainWalletFor } from "@/utils/wallet/keychain"; import { createPeakVaultWalletFor } from "@/utils/wallet/peakvault"; +let intervalId: NodeJS.Timeout | undefined; + export const useWalletStore = defineStore('wallet', { state: () => ({ - wallet: undefined as undefined | Wallet + _walletsStatus: { + metamask: false, + keychain: false, + peakvault: false + }, + wallet: undefined as undefined | Wallet, + isWalletSelectModalOpen: false }), getters: { hasWallet: state => !!state.wallet, + walletsStatus: state => { + if (!intervalId) { + const metamaskStore = useMetamaskStore(); + + const checkForWallets = () => { + metamaskStore.connect().then(() => state._walletsStatus.metamask = true).catch(console.error); + state._walletsStatus.keychain = "hive_keychain" in window; + state._walletsStatus.peakvault = "peakvault" in window; + }; + + intervalId = setInterval(checkForWallets, 1000); + checkForWallets(); + } + + return state._walletsStatus; + } }, actions: { + openWalletSelectModal() { + this.isWalletSelectModalOpen = true; + }, + closeWalletSelectModal() { + this.isWalletSelectModalOpen = false; + }, async createWalletFor(settings: Settings) { switch(settings.wallet) { case UsedWallet.METAMASK: {