Skip to content

Commit 2dc88f3

Browse files
authored
feat: add guardian recovery (#52)
1 parent 5e255be commit 2dc88f3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+7502
-697
lines changed

cspell-config/cspell-packages.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ wagmi
55
cbor
66
levischuck
77
ofetch
8+
reown

packages/auth-server/app.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@
55
</template>
66

77
<script lang="ts" setup>
8+
import { createAppKit } from "@reown/appkit/vue";
9+
10+
const { defaultChain } = useClientStore();
11+
const { metadata, projectId, wagmiAdapter } = useAppKit();
12+
813
// BigInt polyfill
914
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1015
(BigInt.prototype as any).toJSON = function () {
1116
return this.toString();
1217
};
18+
19+
createAppKit({
20+
adapters: [wagmiAdapter],
21+
networks: [defaultChain],
22+
projectId,
23+
metadata,
24+
});
1325
</script>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<template>
2+
<div class="relative">
3+
<select
4+
:id="id"
5+
v-model="selectedAccount"
6+
class="w-full px-4 py-3 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-zk text-neutral-900 dark:text-neutral-100 appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed truncate pr-10"
7+
:class="{
8+
'border-error-500 dark:border-error-400': error,
9+
}"
10+
:disabled="disabled || !accounts.length"
11+
>
12+
<option
13+
value=""
14+
disabled
15+
>
16+
{{ accounts.length ? placeholder : noAccountsText }}
17+
</option>
18+
<option
19+
v-for="account in accounts"
20+
:key="account"
21+
:value="account"
22+
>
23+
{{ account }}
24+
</option>
25+
</select>
26+
27+
<div class="absolute inset-y-0 right-0 flex items-center px-4 pointer-events-none">
28+
<ZkIcon icon="arrow_drop_down" />
29+
</div>
30+
31+
<!-- Error messages -->
32+
<div
33+
v-if="error && messages?.length"
34+
class="mt-2 space-y-1"
35+
>
36+
<p
37+
v-for="(message, index) in messages"
38+
:key="index"
39+
class="text-sm text-error-500 dark:text-error-400"
40+
>
41+
{{ message }}
42+
</p>
43+
</div>
44+
</div>
45+
</template>
46+
47+
<script setup lang="ts">
48+
import type { Address } from "viem";
49+
import { computed } from "vue";
50+
51+
const props = defineProps<{
52+
id?: string;
53+
accounts: Address[];
54+
error?: boolean;
55+
messages?: string[];
56+
disabled?: boolean;
57+
placeholder?: string;
58+
noAccountsText?: string;
59+
}>();
60+
61+
const selectedAccount = defineModel<Address | null>({ required: true });
62+
63+
const placeholder = computed(() => props.placeholder ?? "Select an account");
64+
const noAccountsText = computed(() => props.noAccountsText ?? "No accounts found");
65+
</script>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<template>
2+
<Dialog
3+
ref="modalRef"
4+
content-class="min-w-[700px] min-h-[500px]"
5+
description-class="flex-1 mb-0 flex text-base"
6+
close-class="h-8 max-h-8"
7+
:title="title"
8+
@close="onModalClosed()"
9+
>
10+
<template #trigger>
11+
<slot name="trigger">
12+
<Button
13+
class="w-full lg:w-auto"
14+
type="primary"
15+
>
16+
Add Recovery Method
17+
</Button>
18+
</slot>
19+
</template>
20+
21+
<template #submit>
22+
<div />
23+
</template>
24+
25+
<template #cancel>
26+
<div />
27+
</template>
28+
29+
<!-- Method Selection Step -->
30+
<div
31+
v-if="currentStep === 'select-method'"
32+
class="space-y-6 text-left flex-1 flex flex-col"
33+
>
34+
<div class="flex flex-col gap-6 items-center flex-1 justify-center max-w-md mx-auto w-full">
35+
<div class="text-center">
36+
<p class="text-xl font-medium mb-2">
37+
Choose a Recovery Method
38+
</p>
39+
<p class="text-base text-gray-600 dark:text-gray-400">
40+
Select how you'd like to recover your account if you lose access
41+
</p>
42+
</div>
43+
44+
<div class="flex flex-col gap-5 w-full max-w-xs">
45+
<Button
46+
class="w-full"
47+
@click="selectMethod('guardian')"
48+
>
49+
Recover with Guardian
50+
</Button>
51+
52+
<div class="flex w-full flex-col gap-2">
53+
<Button
54+
disabled
55+
class="w-full"
56+
>
57+
Recover with Email
58+
</Button>
59+
<span class="text-sm text-gray-500 text-center">
60+
Coming soon...
61+
</span>
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
67+
<GuardianFlow
68+
v-if="currentStep === 'guardian'"
69+
:close-modal="closeModal"
70+
@back="currentStep = 'select-method'"
71+
/>
72+
</Dialog>
73+
</template>
74+
75+
<script setup lang="ts">
76+
import { ref } from "vue";
77+
78+
import GuardianFlow from "~/components/account-recovery/guardian-flow/Root.vue";
79+
import Button from "~/components/zk/button.vue";
80+
import Dialog from "~/components/zk/dialog.vue";
81+
82+
type Step = "select-method" | "guardian" | "email";
83+
const currentStep = ref<Step>("select-method");
84+
const modalRef = ref<InstanceType<typeof Dialog>>();
85+
86+
const emit = defineEmits<{
87+
(e: "closed"): void;
88+
}>();
89+
90+
function onModalClosed() {
91+
emit("closed");
92+
}
93+
94+
function closeModal() {
95+
modalRef.value?.close();
96+
}
97+
98+
const title = computed(() => {
99+
switch (currentStep.value) {
100+
case "select-method":
101+
return "Add Recovery Method";
102+
case "guardian":
103+
return "Guardian Recovery Setup";
104+
case "email":
105+
return "Email Recovery Setup";
106+
default:
107+
throw new Error("Invalid step");
108+
}
109+
});
110+
111+
function selectMethod(method: "guardian" | "email") {
112+
currentStep.value = method;
113+
}
114+
</script>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div
3+
:class="styles.container"
4+
>
5+
<component
6+
:is="icon"
7+
:class="styles.icon"
8+
/>
9+
<div class="flex flex-col flex-1 gap-2">
10+
<span :class="styles.title">
11+
{{ props.title }}
12+
</span>
13+
<p :class="styles.message">
14+
<slot />
15+
</p>
16+
</div>
17+
</div>
18+
</template>
19+
20+
<script setup lang="ts">
21+
import { CheckCircleIcon, ExclamationTriangleIcon } from "@heroicons/vue/24/solid";
22+
23+
const props = defineProps<{
24+
title: string;
25+
type: "success" | "error" | "warning";
26+
}>();
27+
28+
const icon = computed(() => {
29+
if (props.type === "success") {
30+
return CheckCircleIcon;
31+
} else {
32+
return ExclamationTriangleIcon;
33+
}
34+
});
35+
36+
const styles = computed(() => ({
37+
container: ["rounded-2xl flex gap-4", {
38+
"bg-error-50/50 dark:bg-error-900/30 backdrop-blur-sm p-6 border border-error-200 dark:border-error-700/50": props.type === "error",
39+
"bg-warning-50/50 dark:bg-warning-900/30 backdrop-blur-sm p-6 border border-warning-200 dark:border-warning-700/50": props.type === "warning",
40+
"bg-green-50/80 dark:bg-green-900/30 backdrop-blur-sm p-6 border border-green-200 dark:border-green-700/50": props.type === "success",
41+
}],
42+
title: ["text-lg font-medium font-semibold", {
43+
"text-error-600 dark:text-error-400": props.type === "error",
44+
"text-yellow-600 dark:text-yellow-400": props.type === "warning",
45+
"text-green-800 dark:text-green-400": props.type === "success",
46+
}],
47+
message: ["", {
48+
"text-error-600 dark:text-error-400": props.type === "error",
49+
"text-yellow-600 dark:text-yellow-400": props.type === "warning",
50+
"text-green-800 dark:text-green-400": props.type === "success",
51+
}],
52+
icon: ["w-6 h-6", {
53+
"text-error-600 dark:text-error-400": props.type === "error",
54+
"text-yellow-600 dark:text-yellow-400": props.type === "warning",
55+
"text-green-600 dark:text-green-400": props.type === "success",
56+
}],
57+
}));
58+
</script>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<div class="rounded-2xl bg-neutral-100/50 backdrop-blur-sm dark:bg-gray-800/50 p-6">
3+
<label class="block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
4+
{{ props.title }}
5+
</label>
6+
<div class="text-gray-900 dark:text-gray-100">
7+
<slot />
8+
</div>
9+
<p class="mt-2 text-gray-600 dark:text-gray-400 font-mono text-xs">
10+
<slot name="footer" />
11+
</p>
12+
</div>
13+
</template>
14+
15+
<script setup lang="ts">
16+
const props = defineProps<{
17+
title: string;
18+
19+
}>();
20+
</script>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<template>
2+
<div class="flex flex-col gap-8 flex-1">
3+
<CommonStepper
4+
:current-step="currentStep"
5+
:total-steps="5"
6+
:disabled-steps="isConfirmLater ? [5] : undefined"
7+
/>
8+
9+
<div class="flex flex-col items-center gap-4 mt-4">
10+
<h2 class="text-2xl font-medium text-center text-gray-900 dark:text-white">
11+
{{ stepTitle }}
12+
</h2>
13+
14+
<div
15+
class="gap-4 flex-1 flex flex-col justify-center items-center max-w-lg"
16+
>
17+
<Step1
18+
v-if="currentStep === 1"
19+
@next="currentStep++"
20+
@back="$emit('back')"
21+
/>
22+
<Step2
23+
v-if="currentStep === 2"
24+
v-model="guardianAddress"
25+
@next="currentStep++"
26+
@back="currentStep--"
27+
/>
28+
<Step3
29+
v-if="currentStep === 3"
30+
@confirm-now="handleConfirmNow"
31+
@confirm-later="handleConfirmLater"
32+
/>
33+
<Step4ConfirmNow
34+
v-if="currentStep === 4 && !isConfirmLater"
35+
:guardian-address="guardianAddress"
36+
@next="currentStep++"
37+
@back="currentStep--"
38+
/>
39+
<Step4ConfirmLater
40+
v-if="currentStep === 4 && isConfirmLater"
41+
:guardian-address="guardianAddress"
42+
@next="completeSetup"
43+
/>
44+
<Step5
45+
v-if="currentStep === 5"
46+
@next="completeSetup"
47+
/>
48+
</div>
49+
</div>
50+
</div>
51+
</template>
52+
53+
<script setup lang="ts">
54+
import type { Address } from "viem";
55+
import { ref } from "vue";
56+
57+
import Step1 from "./Step1.vue";
58+
import Step2 from "./Step2.vue";
59+
import Step3 from "./Step3.vue";
60+
import Step4ConfirmLater from "./Step4ConfirmLater.vue";
61+
import Step4ConfirmNow from "./Step4ConfirmNow.vue";
62+
import Step5 from "./Step5.vue";
63+
64+
const guardianAddress = ref("" as Address);
65+
66+
const currentStep = ref(1);
67+
const isConfirmLater = ref(false);
68+
69+
const stepTitle = computed(() => {
70+
switch (currentStep.value) {
71+
case 1:
72+
return "Guardian Recovery";
73+
case 2:
74+
return "Insert Guardian Address";
75+
case 3:
76+
return "Confirm Guardian";
77+
case 4:
78+
return isConfirmLater.value ? "Save Recovery URL" : "Connect Guardian Account";
79+
case 5:
80+
return "Guardian Confirmed";
81+
default:
82+
return "";
83+
}
84+
});
85+
86+
const handleConfirmNow = () => {
87+
isConfirmLater.value = false;
88+
currentStep.value++;
89+
};
90+
91+
const handleConfirmLater = () => {
92+
isConfirmLater.value = true;
93+
currentStep.value++;
94+
};
95+
96+
function completeSetup() {
97+
props.closeModal();
98+
}
99+
100+
const props = defineProps<{
101+
closeModal: () => void;
102+
}>();
103+
104+
defineEmits<{
105+
(e: "back"): void;
106+
}>();
107+
</script>

0 commit comments

Comments
 (0)