Skip to content

Commit 81926d2

Browse files
matias-gonzaonMiniRoman
authored
Merge Guardian recovery (#67)
* feat: add initial guardian recovery views * feat: add confirm guardian view * feat: improve guardian view with edge cases * fix: remove commented code * feat: improve component imports * feat: add logout icon in desktop breakpoint * fix: naming * fix: pnpm lock * fix: wrong nav component import * fix: add missing package to cspell * feat: add recover account views * feat: add unknown account page * feat: improve account init recovery start * feat: reorganize routes with typed routes * feat: add recovery process warning when logged in * feat: add account not ready page * fix: confirm-guardian page * feat: add base guardian recovery module * chore: update contracts submodule * chore: update contracts submodule * chore: update contracts submodule * feat: add sso account validation * Update packages/auth-server/pages/recovery/guardian/index.vue Co-authored-by: Lukasz Romanowski <5160687+MiniRoman@users.noreply.github.com> * feat: add integration with /recovery/guardian/find-account * feat: integrate contracts in guardians settings page * feat: set proper path to confirm-guardian page * feat: add guardian confirmation integration * feat: add ui improvements * feat: address pr comments * feat: update contracts submodule * chore: update contract submodule * feat: add integration to confirm recovery view * feat: add integration with cancel recovery (#53) * feat: add integration with cancel recovery * feat: update contracts submodule and abi * feat: add verify recovery view on the main page * chore: update contracts * feat: add missing nuxt config * feat: improve confirm guardian flow * chore: update contracts package * feat: execute pending recovery on login (#57) * feat: execute pending recovery on login * feat: move recovery client to sdk * feat: add account-not-ready view * chore: fix pnpm lock --------- Co-authored-by: aon <21188659+aon@users.noreply.github.com> --------- Co-authored-by: aon <21188659+aon@users.noreply.github.com> Co-authored-by: Lukasz Romanowski <5160687+MiniRoman@users.noreply.github.com>
1 parent ba0fcdf commit 81926d2

Some content is hidden

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

66 files changed

+7082
-632
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<div class="relative">
3+
<select
4+
:id="id"
5+
v-model="selectedValue"
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-8"
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 ? 'Select an account' : 'No accounts found' }}
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+
modelValue: string;
54+
accounts: Address[];
55+
error?: boolean;
56+
messages?: string[];
57+
disabled?: boolean;
58+
}>();
59+
60+
const emit = defineEmits<{
61+
(e: "update:modelValue", value: string): void;
62+
}>();
63+
64+
const selectedValue = computed({
65+
get: () => props.modelValue,
66+
set: (value) => emit("update:modelValue", value),
67+
});
68+
</script>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
>
9+
<template #trigger>
10+
<slot name="trigger">
11+
<Button
12+
class="w-full lg:w-auto"
13+
type="primary"
14+
>
15+
Add Recovery Method
16+
</Button>
17+
</slot>
18+
</template>
19+
20+
<template #submit>
21+
<div />
22+
</template>
23+
24+
<template #cancel>
25+
<div />
26+
</template>
27+
28+
<!-- Method Selection Step -->
29+
<div
30+
v-if="currentStep === 'select-method'"
31+
class="space-y-6 text-left flex-1 flex flex-col"
32+
>
33+
<div class="flex flex-col gap-6 items-center flex-1 justify-center max-w-md mx-auto w-full">
34+
<div class="text-center">
35+
<p class="text-xl font-medium mb-2">
36+
Choose a Recovery Method
37+
</p>
38+
<p class="text-base text-gray-600 dark:text-gray-400">
39+
Select how you'd like to recover your account if you lose access
40+
</p>
41+
</div>
42+
43+
<div class="flex flex-col gap-5 w-full max-w-xs">
44+
<Button
45+
class="w-full"
46+
@click="selectMethod('guardian')"
47+
>
48+
Recover with Guardian
49+
</Button>
50+
51+
<div class="flex w-full flex-col gap-2">
52+
<Button
53+
disabled
54+
class="w-full"
55+
>
56+
Recover with Email
57+
</Button>
58+
<span class="text-sm text-gray-500 text-center">
59+
Coming soon...
60+
</span>
61+
</div>
62+
</div>
63+
</div>
64+
</div>
65+
66+
<GuardianFlow
67+
v-if="currentStep === 'guardian'"
68+
:close-modal="closeModal"
69+
@back="currentStep = 'select-method'"
70+
/>
71+
</Dialog>
72+
</template>
73+
74+
<script setup lang="ts">
75+
import { ref } from "vue";
76+
77+
import GuardianFlow from "~/components/account-recovery/guardian-flow/Root.vue";
78+
import Button from "~/components/zk/button.vue";
79+
import Dialog from "~/components/zk/dialog.vue";
80+
81+
type Step = "select-method" | "guardian" | "email";
82+
const currentStep = ref<Step>("select-method");
83+
const modalRef = ref<InstanceType<typeof Dialog>>();
84+
85+
const emit = defineEmits<{
86+
(e: "closed"): void;
87+
}>();
88+
89+
function closeModal() {
90+
emit("closed");
91+
modalRef.value?.close();
92+
}
93+
94+
const title = computed(() => {
95+
switch (currentStep.value) {
96+
case "select-method":
97+
return "Add Recovery Method";
98+
case "guardian":
99+
return "Guardian Recovery Setup";
100+
case "email":
101+
return "Email Recovery Setup";
102+
default:
103+
throw new Error("Invalid step");
104+
}
105+
});
106+
107+
function selectMethod(method: "guardian" | "email") {
108+
currentStep.value = method;
109+
}
110+
</script>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<template>
2+
<div class="w-full max-w-md flex flex-col gap-6">
3+
<!-- Generate Passkeys Step -->
4+
<div
5+
v-if="currentStep === generatePasskeysStep"
6+
class="w-full max-w-md flex flex-col gap-6"
7+
>
8+
<p class="text-center text-neutral-700 dark:text-neutral-300">
9+
Generate new passkeys to secure your account
10+
</p>
11+
12+
<ZkButton
13+
class="w-full"
14+
:loading="registerInProgress"
15+
@click="handleGeneratePasskeys"
16+
>
17+
Generate Passkeys
18+
</ZkButton>
19+
20+
<ZkButton
21+
type="secondary"
22+
class="w-full"
23+
@click="$emit('back')"
24+
>
25+
Back
26+
</ZkButton>
27+
</div>
28+
29+
<!-- Confirmation Step -->
30+
<div
31+
v-if="currentStep === confirmationStep"
32+
class="w-full max-w-md flex flex-col gap-6"
33+
>
34+
<div class="flex flex-col gap-4 text-center text-neutral-700 dark:text-neutral-300">
35+
<p>
36+
Your passkeys have been generated successfully.
37+
</p>
38+
<p>
39+
Please share the following url with your guardian to complete the recovery process:
40+
</p>
41+
</div>
42+
43+
<div class="w-full items-center gap-2 p-4 bg-neutral-100 dark:bg-neutral-900 rounded-zk">
44+
<a
45+
:href="recoveryUrl"
46+
target="_blank"
47+
class="text-sm text-neutral-800 dark:text-neutral-100 break-all hover:text-neutral-900 dark:hover:text-neutral-400 leading-relaxed underline underline-offset-4 decoration-neutral-400 hover:decoration-neutral-900 dark:decoration-neutral-600 dark:hover:decoration-neutral-400"
48+
>
49+
{{ recoveryUrl }}
50+
</a>
51+
<common-copy-to-clipboard
52+
:text="recoveryUrl ?? ''"
53+
class="!inline-flex ml-1"
54+
/>
55+
</div>
56+
57+
<p class="text-sm text-center text-neutral-600 dark:text-neutral-400">
58+
You'll be able to access your account once your guardian confirms the recovery.
59+
</p>
60+
61+
<ZkLink
62+
type="primary"
63+
href="/"
64+
class="w-full"
65+
>
66+
Back to Home
67+
</ZkLink>
68+
</div>
69+
</div>
70+
</template>
71+
72+
<script setup lang="ts">
73+
import type { RegisterNewPasskeyReturnType } from "zksync-sso/client/passkey";
74+
75+
const props = defineProps<{
76+
currentStep: number;
77+
generatePasskeysStep: number;
78+
confirmationStep: number;
79+
address: string;
80+
newPasskey: RegisterNewPasskeyReturnType | null;
81+
registerInProgress: boolean;
82+
}>();
83+
84+
const emit = defineEmits<{
85+
(e: "back"): void;
86+
(e: "update:newPasskey", value: RegisterNewPasskeyReturnType): void;
87+
(e: "update:currentStep", value: number): void;
88+
}>();
89+
90+
const runtimeConfig = useRuntimeConfig();
91+
const appUrl = runtimeConfig.public.appUrl;
92+
93+
const { registerPasskey } = usePasskeyRegister();
94+
95+
const recoveryUrl = computedAsync(async () => {
96+
const queryParams = new URLSearchParams();
97+
98+
const credentialId = props.newPasskey?.credentialId ?? "";
99+
const credentialPublicKey = uint8ArrayToHex(props.newPasskey?.credentialPublicKey ?? new Uint8Array()) ?? "";
100+
101+
queryParams.set("credentialId", credentialId);
102+
queryParams.set("credentialPublicKey", credentialPublicKey);
103+
queryParams.set("accountAddress", props.address);
104+
105+
// Create checksum from concatenated credential data
106+
const dataToHash = `${props.address}:${credentialId}:${credentialPublicKey}`;
107+
const fullHash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(dataToHash)));
108+
const shortHash = fullHash.slice(0, 8); // Take first 8 bytes of the hash
109+
const checksum = uint8ArrayToHex(shortHash);
110+
111+
queryParams.set("checksum", checksum);
112+
113+
return `${appUrl}/recovery/guardian/confirm-recovery?${queryParams.toString()}`;
114+
});
115+
116+
const handleGeneratePasskeys = async () => {
117+
const result = await registerPasskey();
118+
if (!result) {
119+
throw new Error("Failed to register passkey");
120+
}
121+
emit("update:newPasskey", result);
122+
emit("update:currentStep", props.confirmationStep);
123+
};
124+
</script>

0 commit comments

Comments
 (0)