Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add guardian recovery creation pages #48

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell-config/cspell-packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ wagmi
cbor
levischuck
ofetch
reown
26 changes: 26 additions & 0 deletions packages/auth-server/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,35 @@
</template>

<script lang="ts" setup>
import type { AppKitNetwork } from "@reown/appkit/networks";
import { createAppKit } from "@reown/appkit/vue";
import { WagmiAdapter } from "@reown/appkit-adapter-wagmi";

import { supportedChains } from "./stores/client";

// BigInt polyfill
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};

// AppKit Configuration
const projectId = "9bc5059f6eed355858cc56a3388e9b50";
const metadata = {
name: "ZKsync SSO Auth Server",
description: "ZKsync SSO Auth Server",
url: "https://auth-test.zksync.dev",
icons: ["https://auth-test.zksync.dev/icon-512.png"],
};
const wagmiAdapter = new WagmiAdapter({
networks: supportedChains,
projectId,
});

createAppKit({
adapters: [wagmiAdapter],
networks: supportedChains as unknown as [AppKitNetwork, ...AppKitNetwork[]],
projectId,
metadata,
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<template>
<Dialog
ref="modalRef"
content-class="min-w-[700px] min-h-[500px]"
description-class="flex-1 mb-0 flex"
close-class="h-8 max-h-8"
:title="title"
>
<template #trigger>
<slot name="trigger">
<Button
class="w-full lg:w-auto"
type="primary"
>
Add Recovery Method
</Button>
</slot>
</template>

<template #submit>
<div />
</template>

<template #cancel>
<div />
</template>

<!-- Method Selection Step -->
<div
v-if="currentStep === 'select-method'"
class="space-y-4 text-left flex-1 flex flex-col"
>
<p class="text-gray-600 mb-6">
Choose a recovery method for your account:
</p>
<div class="flex flex-col gap-5 items-center flex-1 justify-center">
<Button
class="w-64"
@click="selectMethod('guardian')"
>
<div class="flex items-center justify-between gap-2">
<UserIcon class="w-5 h-5" />
<span>Guardian Recovery</span>
</div>
</Button>
<div class="flex flex-col gap-2">
<Button
disabled
class="w-64"
>
<div class="flex items-center justify-between gap-2">
<EnvelopeIcon class="w-5 h-5" />
<span>Email Recovery</span>
</div>
</Button>
<span class="text-sm text-gray-500 text-center">
Coming soon...
</span>
</div>
</div>
</div>

<GuardianFlow
v-if="currentStep === 'guardian'"
:close-modal="closeModal"
@back="currentStep = 'select-method'"
/>
</Dialog>
</template>

<script setup lang="ts">
import { EnvelopeIcon, UserIcon } from "@heroicons/vue/24/solid";
import { ref } from "vue";

import GuardianFlow from "~/components/account-recovery/flows/GuardianFlow.vue";
import Button from "~/components/zk/button.vue";
import Dialog from "~/components/zk/dialog.vue";

type Step = "select-method" | "guardian" | "email";
const currentStep = ref<Step>("select-method");
const modalRef = ref<InstanceType<typeof Dialog>>();

function closeModal() {
modalRef.value?.close();
}

const title = computed(() => {
switch (currentStep.value) {
case "select-method":
return "Add Recovery Method";
case "guardian":
return "Guardian Recovery Setup";
case "email":
return "Email Recovery Setup";
default:
throw new Error("Invalid step");
}
});

function selectMethod(method: "guardian" | "email") {
currentStep.value = method;
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<div
v-if="currentStep === 'info'"
class="gap-4 flex-1 flex flex-col justify-center items-center"
>
<p class="text-gray-600 px-4">
Guardian recovery allows you to designate trusted contacts who can help you
recover your account if you lose access.
</p>
<div class="flex space-x-3">
<Button
type="primary"
class="w-fit"
@click="currentStep = 'add-guardian'"
>
Continue
</Button>
<Button
type="secondary"
class="w-fit"
@click="$emit('back')"
>
Back
</Button>
</div>
</div>

<div
v-else-if="currentStep === 'add-guardian'"
class="flex flex-col gap-4 flex-1 text-left justify-center px-6"
>
<p>Insert address</p>
<Input />
<div class="flex gap-3">
<Button @click="currentStep = 'confirm'">
Continue
</Button>
<Button
type="secondary"
@click="currentStep = 'info'"
>
Back
</Button>
</div>
</div>

<div
v-else-if="currentStep === 'confirm'"
class="flex flex-col justify-between flex-1 text-left px-6"
>
<p>Your recovery address was saved. Please use this url to confirm the recovery method:</p>
<Link
href="https://auth-test.zksync.dev/dashboard/0x1234567890"
class="w-fit mx-auto"
target="_blank"
>
https://auth-test.zksync.dev/dashboard/0x1234567890
</Link>
<Button @click="completeSetup">
Close
</Button>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue";

import Button from "~/components/zk/button.vue";
import Input from "~/components/zk/input.vue";
import Link from "~/components/zk/link.vue";

type GuardianStep = "info" | "add-guardian" | "confirm";
const currentStep = ref<GuardianStep>("info");

const props = defineProps<{
closeModal: () => void;
}>();

defineEmits<{
(e: "back"): void;
}>();

function completeSetup() {
props.closeModal();
}
</script>
14 changes: 13 additions & 1 deletion packages/auth-server/components/app/nav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@
</div>
<div
v-show="!showMobileMenu"
class="flex items-center pr-2"
class="flex gap-2 items-center pr-2"
>
<app-color-mode />
<ZkButtonIcon
icon="Logout"
class="mr-1 -ml-1 scale-[-1]"
@click="logoutAndRedirect"
/>
</div>
<div
v-show="showMobileMenu"
Expand Down Expand Up @@ -80,6 +85,13 @@ onBeforeUnmount(() => {
});

watch(windowWidth, checkWidths);

const { logout } = useAccountStore();

const logoutAndRedirect = () => {
logout();
navigateTo("/");
};
</script>

<style lang="scss" scoped>
Expand Down
49 changes: 49 additions & 0 deletions packages/auth-server/components/common/ConnectButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<Button
:type="type"
:loading="stateData.open && !accountData.isConnected"
@click="onClick"
>
<div class="flex items-center gap-2">
<UserCircleIcon
v-if="accountData.isConnected"
class="w-4 h-4"
/>
<WalletIcon
v-else
class="w-4 h-4"
/>
<span>{{ text }}</span>
</div>
</Button>
</template>

<script setup lang="ts">
import { UserCircleIcon, WalletIcon } from "@heroicons/vue/24/solid";
import { useAppKit, useAppKitAccount, useAppKitState } from "@reown/appkit/vue";

import Button, { type ButtonTypes } from "~/components/zk/button.vue";

const { open } = useAppKit();
const accountData = useAppKitAccount();
const stateData = useAppKitState();

const onClick = async () => {
if (accountData.value.isConnected) {
await open({ view: "Account" });
} else {
await open({ view: "Connect" });
}
};

const text = computed(() => {
if (accountData.value.isConnected && accountData.value.address) {
return shortenAddress(accountData.value.address);
}
return "Connect Wallet";
});

defineProps<{
type?: ButtonTypes;
}>();
</script>
69 changes: 69 additions & 0 deletions packages/auth-server/components/common/CopyToClipboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<button
class="relative w-5 h-5 flex items-center justify-center"
@click="copyToClipboard(text)"
>
<Transition name="fade">
<span
v-if="copied"
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs text-white dark:text-gray-900 bg-gray-800 dark:bg-gray-100 rounded-2xl whitespace-nowrap"
>
Copied!
</span>
</Transition>
<Transition
name="scale"
mode="out-in"
>
<CheckIcon
v-if="copied"
class="w-5 h-5 text-green-500 dark:text-green-400"
/>
<DocumentDuplicateIcon
v-else
class="w-4 h-4"
/>
</Transition>
</button>
</template>

<script setup lang="ts">
import { CheckIcon, DocumentDuplicateIcon } from "@heroicons/vue/24/solid";
import { ref } from "vue";

const copied = ref(false);

const copyToClipboard = async (text: string) => {
await navigator.clipboard.writeText(text);
copied.value = true;

setTimeout(() => {
copied.value = false;
}, 2000);
};

defineProps<{ text: string }>();
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

.scale-enter-active,
.scale-leave-active {
transition: all 0.1s ease-out;
}

.scale-enter-from,
.scale-leave-to {
transform: scale(0.95);
opacity: 0;
}
</style>
Loading
Loading