Skip to content

Commit

Permalink
➕ Integrate AppKit
Browse files Browse the repository at this point in the history
  • Loading branch information
nwingt committed Dec 18, 2024
1 parent 24d6b21 commit ad0b001
Show file tree
Hide file tree
Showing 17 changed files with 432 additions and 463 deletions.
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

0 comments on commit ad0b001

Please sign in to comment.