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-2391] ➕ Integrate veaury #5

Draft
wants to merge 3 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
56 changes: 44 additions & 12 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
<template>
<div class="fixed inset-0 flex">
<ClientOnly>
<PrivyReactBridge
@ready-change="userStore.setIsPrivyReady"
@authenticated-change="userStore.setIsPrivyAuthenticated"
@user-change="userStore.setPrivyUser"
@login-method-change="userStore.setPrivyLogin"
@logout-method-change="userStore.setPrivyLogout"
/>
</ClientOnly>

<div
v-if="!isPrivyReady"
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="!userStore.address"
:model-value="isPrivyReady && !isPrivyAuthenticated"
:ui="{
padding: 'p-0',
rounded: 'rounded-none lg:rounded-lg',
Expand Down Expand Up @@ -41,7 +68,7 @@
</USlideover>

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

<NuxtLoadingIndicator />
Expand All @@ -51,8 +78,21 @@
</template>

<script setup lang="ts">
import { setVeauryOptions } from "veaury";
import { createRoot } from "react-dom/client";

setVeauryOptions({
react: {
createRoot,
},
});

const APP_NAME = "book3.app";

const userStore = useUserStore();
const { isPrivyAuthenticated, isPrivyReady } = storeToRefs(userStore);
const uiStore = useUIStore();
const { t: $t } = useI18n();

const isMobileMenuOpen = computed({
get: () => uiStore.isMobileMenuOpen,
Expand Down Expand Up @@ -107,15 +147,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>
202 changes: 20 additions & 182 deletions components/AuthPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,98 +9,26 @@

<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"
:disabled="isPrivyAuthenticated"
block
:ui="{ rounded: 'rounded-full' }"
@click="login"
/>
</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);
const { isPrivyAuthenticated, isPrivyReady } = storeToRefs(userStore);

function handleAuthError({
error,
Expand All @@ -110,8 +38,6 @@ function handleAuthError({
title?: string;
}) {
console.error(error);
authenticatingConnectorId.value = undefined;
disconnect();
toast.add({
title,
color: "red",
Expand All @@ -121,102 +47,14 @@ function handleAuthError({
});
}

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;
}

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();
function login() {
if (!isPrivyReady.value || isPrivyAuthenticated.value) {
return;
}

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 });
},
},
);
}

function createWallet() {
const coinbaseWalletConnector = connectors.find(
(connector) => connector.id === "coinbaseWalletSDK",
);
if (coinbaseWalletConnector) {
handleConnect(coinbaseWalletConnector);
try {
userStore.login();
} catch (error) {
handleAuthError({ error: error as Error });
}
}
</script>
28 changes: 28 additions & 0 deletions components/PrivyReactBridge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<PrivyApp
:app-id="privyAppId"
@ready-change="$emit('ready-change', $event)"
@authenticated-change="$emit('authenticated-change', $event)"
@user-change="$emit('user-change', $event)"
@login-method-change="$emit('login-method-change', $event)"
@logout-method-change="$emit('logout-method-change', $event)"
/>
</template>

<script setup>
import { applyPureReactInVue } from "veaury";

import VeauryPrivyApp from "../react_app/PrivyApp.jsx";

const PrivyApp = applyPureReactInVue(VeauryPrivyApp);

const { privyAppId } = useRuntimeConfig().public;

defineEmits([
"ready-change",
"authenticated-change",
"user-change",
"login-method-change",
"logout-method-change",
]);
</script>
1 change: 1 addition & 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 Down
1 change: 1 addition & 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 Down
18 changes: 14 additions & 4 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import veauryVitePlugins from "veaury/vite/index.js";

export default defineNuxtConfig({
app: {
rootAttrs: {
Expand All @@ -7,18 +9,15 @@ export default defineNuxtConfig({
},
compatibilityDate: "2024-04-03",
runtimeConfig: {
sessionSecret:
process.env.SESSION_SECRET || "00000000-0000-0000-0000-000000000000",
public: {
sessionName: "book3_app_session",
privyAppId: "cm38yfagh00iqaegqad5wbbpo",
},
},
devtools: { enabled: true },
modules: [
"@nuxt/ui",
"@nuxt/eslint",
"@pinia/nuxt",
"@wagmi/vue/nuxt",
"@vueuse/nuxt",
"@nuxtjs/i18n",
],
Expand All @@ -29,4 +28,15 @@ export default defineNuxtConfig({
{ code: "zh-Hant", name: "繁體中文" },
],
},
build: {
transpile: ["veaury"],
},
vite: {
plugins: [
veauryVitePlugins({
type: "vue",
isNuxt: true,
}),
],
},
});
Loading