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

[TAS-2855] ➕ Integrate AppKit #6

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
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
90 changes: 66 additions & 24 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
<template>
<div class="fixed inset-0 flex">
<UModal
:model-value="!userStore.address"
:ui="{
padding: 'p-0',
rounded: 'rounded-none lg:rounded-lg',
overlay: { background: 'bg-gray-200 dark:bg-gray-800' },
}"
:transition="false"
prevent-close
>
<AuthPage />
</UModal>
<ClientOnly>
<div
v-if="accountData.status === 'connecting'"
class="absolute inset-0 z-20 flex flex-col items-center justify-center gap-6 text-center bg-white dark:bg-gray-900"
>
<AppLogo class="h-20 mx-auto" />
<div class="text-2xl font-bold" v-text="APP_NAME" />
<div
class="flex flex-col items-center justify-center text-sm text-gray-400"
>
<UIcon
class="w-10 h-10 mx-auto animate-spin"
name="i-heroicons-arrow-path-20-solid"
/>
<div v-text="$t('$loading')" />
</div>
</div>

<UModal
:model-value="accountData.status === 'disconnected'"
:ui="{
padding: 'p-0',
rounded: 'rounded-none lg:rounded-lg',
overlay: { background: 'bg-gray-200 dark:bg-gray-800' },
}"
:transition="false"
prevent-close
>
<AuthPage />
</UModal>
</ClientOnly>

<AppMenu
:class="[
Expand Down Expand Up @@ -41,7 +60,10 @@
</USlideover>

<NuxtPage
:class="['overflow-y-auto', { 'opacity-0': !userStore.address }]"
:class="[
'overflow-y-auto',
{ 'opacity-0': accountData.status !== 'connected' },
]"
/>

<NuxtLoadingIndicator />
Expand All @@ -51,7 +73,12 @@
</template>

<script setup lang="ts">
const userStore = useUserStore();
import { createAppKit, useAppKitAccount } from "@reown/appkit/vue";
import { createAppKitWagmiAdapter, networks } from "./appkit";

const APP_NAME = "book3.app";

const accountData = useAppKitAccount();
const uiStore = useUIStore();

const isMobileMenuOpen = computed({
Expand All @@ -60,6 +87,29 @@ const isMobileMenuOpen = computed({
});

const route = useRoute();
const { appKitProjectId } = useRuntimeConfig().public;

const metadata = {
name: APP_NAME,
description: APP_NAME,
url: "https://book3.app", // origin must match your domain & subdomain
icons: ["https://book3.app/apple-touch-icon.png"],
};

const wagmiAdapter = createAppKitWagmiAdapter(appKitProjectId);

// Initialize AppKit
createAppKit({
adapters: [wagmiAdapter],
networks,
projectId: appKitProjectId,
metadata,
features: {
email: true,
socials: ["google"],
emailShowWallets: false,
},
});

// NOTE: Close mobile menu on route change
watch(
Expand Down Expand Up @@ -107,15 +157,7 @@ useHead({
});

useSeoMeta({
title: "book3.app",
ogTitle: "book3.app",
});

await callOnce(async () => {
try {
await userStore.fetchSettings();
} catch {
// Ignore
}
title: APP_NAME,
ogTitle: APP_NAME,
});
</script>
11 changes: 11 additions & 0 deletions appkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { WagmiAdapter } from "@reown/appkit-adapter-wagmi";
import { optimismSepolia, type AppKitNetwork } from "@reown/appkit/networks";

export const networks: [AppKitNetwork, ...AppKitNetwork[]] = [optimismSepolia];

export function createAppKitWagmiAdapter(projectId: string) {
return new WagmiAdapter({
networks,
projectId,
});
}
222 changes: 19 additions & 203 deletions components/AuthPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,214 +9,30 @@

<p
class="flex justify-center items-center text-2xl text-gray-900 dark:text-white font-bold text-center"
>
{{ $t("auth_page_tagline") }}
</p>

<div class="relative group rounded-full">
<div class="absolute -inset-[2px] rounded-[inherit] overflow-hidden">
<div
class="absolute inset-x-0 aspect-1 top-[50%] -translate-y-[50%] group-hover:rotate-[720deg] transition-transform duration-[2s] ease-[cubic-bezier(0.27,0,0.24,0.99)]"
style="
background: conic-gradient(
from 180deg,
#45e1e5 0deg,
#0052ff 86.4deg,
#b82ea4 165.6deg,
#ff9533 255.6deg,
#7fd057 320.4deg,
#45e1e5 360deg
);
"
/>
</div>
<div class="relative rounded-[inherit] bg-black">
<UButton
:label="$t('auth_page_sign_in_or_sign_up_button_label')"
color="white"
size="xl"
:disabled="!!authenticatingConnectorId"
:loading="authenticatingConnectorId === 'coinbaseWalletSDK'"
block
:ui="{ rounded: 'rounded-full' }"
@click="createWallet"
/>
</div>
</div>

<UAccordion
v-if="otherConnectors.length"
:items="[{ slot: 'connectors' }]"
:ui="{ wrapper: 'flex flex-col w-full' }"
>
<template #default="{ open }">
<UButton
class="flex justify-center items-center"
:label="$t('auth_page_sign_in_with_other_method_button_label')"
:trailing-icon="
open ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'
"
variant="ghost"
rounded
/>
</template>

<template #connectors>
<ul class="space-y-2">
<li v-for="connector in otherConnectors" :key="connector.name">
<UButton
:label="connector.name"
size="xl"
variant="outline"
:disabled="!!authenticatingConnectorId"
:loading="authenticatingConnectorId === connector.id"
block
@click="handleConnect(connector)"
/>
</li>
</ul>
</template>
</UAccordion>
v-text="$t('auth_page_tagline')"
/>

<UButton
:label="$t('auth_page_sign_in_or_sign_up_button_label')"
color="white"
size="xl"
block
:disabled="isAppKitModalOpen"
:loading="isAppKitModalOpen"
:ui="{ rounded: 'rounded-full' }"
@click="loginWithAppKit"
/>
</UCard>
</template>

<script setup lang="ts">
import {
useAccount,
useConnect,
useDisconnect,
useChainId,
useSignMessage,
type Connector,
} from "@wagmi/vue";
import { SiweMessage } from "siwe";

import { useUserStore } from "../stores/user";

const chainId = useChainId();
const { connectors, connectAsync } = useConnect();
const { disconnect, disconnectAsync } = useDisconnect();
const account = useAccount();
const userStore = useUserStore();
const toast = useToast();

const authenticatingConnectorId = ref<string | undefined>(undefined);

function handleAuthError({
error,
title = "An error occurred.",
}: {
error: Error;
title?: string;
}) {
console.error(error);
authenticatingConnectorId.value = undefined;
disconnect();
toast.add({
title,
color: "red",
description: error.message || error?.toString(),
icon: "i-heroicons-exclamation-triangle",
timeout: 0,
});
}

const otherConnectors = computed(() =>
connectors.filter((connector) => connector.id !== "coinbaseWalletSDK"),
);

const { signMessageAsync } = useSignMessage({
mutation: {
onError: (error) => {
handleAuthError({ error, title: "Failed to sign message." });
},
onSuccess: async (signature, { message }) => {
if (!signature) {
handleAuthError({ error: new Error("Failed to sign message.") });
return;
}
import { useAppKit, useAppKitState } from "@reown/appkit/vue";

const address = account.address?.value;
if (!address) {
handleAuthError({ error: new Error("Failed to fetch address.") });
return;
}

try {
await userStore.login({ address, signature, message });
} catch (error) {
handleAuthError({ error: error as Error, title: "Failed to login." });
return;
}

authenticatingConnectorId.value = undefined;
},
},
});

async function handleConnect(connector: Connector) {
if (account.isConnected) {
await disconnectAsync();
}

authenticatingConnectorId.value = connector.id;

await connectAsync(
{
connector,
chainId: chainId.value,
},
{
onError(error) {
handleAuthError({ error, title: "Failed to connect." });
},
onSuccess: async () => {
let nonce;
try {
nonce = await $fetch("/api/users/nonce");
} catch (error) {
handleAuthError({
error: error as Error,
title: "Failed to fetch nonce.",
});
return;
}
if (!nonce) {
console.error("Failed to fetch nonce.");
disconnect();
return;
}

const address = account.address?.value;
if (!address) {
handleAuthError({ error: new Error("Failed to get address.") });
return;
}

const swieMessage = new SiweMessage({
domain: document.location.host,
address,
chainId: account.chainId?.value,
uri: document.location.origin,
version: "1",
statement: "Login to book3.app",
nonce,
});

const message = swieMessage.prepareMessage();

await signMessageAsync({ account: address, message });
},
},
);
}
const { open: openAppKit } = useAppKit();
const { open: isAppKitModalOpen } = useAppKitState();

function createWallet() {
const coinbaseWalletConnector = connectors.find(
(connector) => connector.id === "coinbaseWalletSDK",
);
if (coinbaseWalletConnector) {
handleConnect(coinbaseWalletConnector);
}
function loginWithAppKit() {
if (isAppKitModalOpen) return;
openAppKit();
}
</script>
2 changes: 2 additions & 0 deletions i18n/messages/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$loading": "Loading",
"auth_page_sign_in_or_sign_up_button_label": "Sign in / Sign up",
"auth_page_sign_in_with_other_method_button_label": "Sign in with other methods",
"auth_page_tagline": "Get started",
Expand All @@ -10,6 +11,7 @@
"menu_item_settings": "Settings",
"reader_view_header_title_default": "Reader",
"settings_page_account_label": "Account",
"settings_page_appkit_button_label": "Open AppKit",
"settings_page_header_title": "Settings",
"settings_page_language_label": "Language",
"settings_page_sign_out_button_label": "Sign out",
Expand Down
2 changes: 2 additions & 0 deletions i18n/messages/zh-Hant.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$loading": "載入中",
"auth_page_sign_in_or_sign_up_button_label": "登錄 / 註册",
"auth_page_sign_in_with_other_method_button_label": "用其他方式登錄",
"auth_page_tagline": "開始",
Expand All @@ -10,6 +11,7 @@
"menu_item_settings": "設定",
"reader_view_header_title_default": "閱讀器",
"settings_page_account_label": "帳戶",
"settings_page_appkit_button_label": "打開 AppKit",
"settings_page_header_title": "設定",
"settings_page_language_label": "語言",
"settings_page_sign_out_button_label": "登出",
Expand Down
Loading