feat(vue-dapp-auth): Add example dapp on Vue 3 (#76)

Co-authored-by: Ben Kremer <contact@bkrem.dev>
This commit is contained in:
Pavel Yankovski 2022-11-14 21:13:55 +04:00 committed by GitHub
parent 882f9d10c8
commit 7e850d66c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 8767 additions and 0 deletions

View File

@ -0,0 +1,2 @@
WALLETCONNECT_PROJECT_ID=...
WALLETCONNECT_RELAY_URL=wss://relay.walletconnect.com

View File

@ -0,0 +1,67 @@
{
"extends": "@antfu",
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}
],
"@typescript-eslint/array-type": [
"error",
{
"default": "array"
}
],
"camelcase": "off",
"import/named": "off",
"no-useless-constructor": "off",
"no-control-regex": "off",
"no-console": "warn",
"@typescript-eslint/brace-style": "off",
"brace-style": [
"error",
"1tbs"
],
"curly": [
"error",
"all"
],
"@typescript-eslint/space-before-function-paren": "off",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"vue/max-attributes-per-line": ["error", {
"singleline": {
"max": 3
},
"multiline": {
"max": 1
}
}],
"vue/component-tags-order": ["error", {
"order": [ "template", "script", "style" ]
}],
"vue/custom-event-name-casing": ["error", "kebab-case"],
"vue/no-deprecated-v-on-native-modifier": "off",
"vue/no-deprecated-dollar-listeners-api": "off",
"vue/no-deprecated-v-bind-sync": "off",
"vue/no-deprecated-dollar-scopedslots-api": "off",
"vue/no-deprecated-filter": "off",
"vue/require-explicit-emits": "off",
"vue/no-deprecated-destroyed-lifecycle": "off",
"vue/component-name-in-template-casing": ["error", "kebab-case"],
"vue/multiline-html-element-content-newline": ["error", {
"ignores": ["pre", "textarea", "nuxt-link", "a", "abbr", "audio", "b", "bdi", "bdo", "canvas", "cite", "code", "data", "del", "dfn", "em", "i", "iframe", "ins", "kbd", "label", "map", "mark", "noscript", "object", "output", "picture", "q", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "svg", "time", "u", "var", "video"]
}],
"vue/singleline-html-element-content-newline": ["error", {
"ignores": ["pre", "textarea", "nuxt-link", "a", "abbr", "audio", "b", "bdi", "bdo", "canvas", "cite", "code", "data", "del", "dfn", "em", "i", "iframe", "ins", "kbd", "label", "map", "mark", "noscript", "object", "output", "picture", "q", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "svg", "time", "u", "var", "video"]
}]
}
}

96
dapps/vue-dapp-auth/.gitignore vendored Normal file
View File

@ -0,0 +1,96 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.*
!*.example
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
.output
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
.history
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
node_modules

View File

@ -0,0 +1,61 @@
# Vue Auth dApp
### Stack
- 💚 Vue 3
- ⛰️ Nuxt 3
- 🍍 Pinia
- 🟦 TypeScript
- 💨 TailwindCSS
- 🔗 ethers.js
## Overview
This example aims to demonstrate dapp-facing use cases enabled by WalletConnect Auth Client.
...And show that you can easily use WalletConnect with any framework.
## Running locally
Install the app's dependencies:
```bash
yarn
```
Set up your local environment variables by copying the example into your own `.env` file:
```bash
cp .env.example .env
```
Your `.env` now contains the following environment variables:
- `WALLETCONNECT_PROJECT_ID` (placeholder) - You can generate your own ProjectId at https://cloud.walletconnect.com
Also, the default relay server `WALLETCONNECT_RELAY_URL` is set. You can change it to use your own instance.
## Development Server
Start the development server on http://localhost:3000
```bash
yarn dev
```
## Production
Build the application for production:
```bash
yarn build
```
Locally preview production build:
```bash
yarn preview
```
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.

View File

@ -0,0 +1,17 @@
export default defineAppConfig({
nuxtIcon: {
aliases: {
'close': 'ion:close-outline',
'chevron-down': 'ion:chevron-down-outline',
'desktop': 'ion:desktop-outline',
'sun': 'ion:sunny-outline',
'moon': 'ion:moon-outline',
'open': 'ion:open-outline',
'github': 'ion:logo-github',
'link': 'ion:link-outline',
'gem': 'ion:diamond-outline',
'copy': 'ion:copy-outline',
'qr': 'ion:qr-code-outline',
},
},
})

View File

@ -0,0 +1,27 @@
<template>
<main class="tw-py-8 tw-container tw-h-screen tw-flex tw-flex-col">
<div class="tw-flex tw-justify-center">
<nuxt-link :to="{ name: 'index' }" class="tw-pl-2.5 tw-text-lg tw-inline-flex tw-items-center tw-gap-3">
<span class="tw-leading-xs">
Example Vue App
</span>
<wc-label />
</nuxt-link>
</div>
<div class="tw-section tw-flex-1">
<nuxt-page />
</div>
<footer class="tw-flex tw-justify-end">
<switch-theme />
</footer>
</main>
</template>
<script setup lang="ts">
const { authClientVersion } = useRuntimeConfig()
// eslint-disable-next-line no-console
console.log(`AuthClient@${authClientVersion}`)
</script>

View File

@ -0,0 +1,22 @@
@layer base {
html {
font-size: 16px; // 1rem
@apply tw-font-main tw-leading-sm tw-bg-base tw-text-base;
}
::selection {
@apply tw-bg-accent-secondary tw-text-bg;
}
a {
@apply tw-cursor-pointer;
}
hr {
@apply tw-text-divider;
}
input,
button {
outline: none !important;
}
}

View File

@ -0,0 +1,65 @@
@layer components {
.tw-section {
@apply tw-py-8;
@screen md {
@apply tw-py-12;
}
}
// Cards
.tw-card {
@apply tw-shadow-card tw-rounded-lg tw-bg-dim-1 tw-p-8 sm:tw-px-10;
}
// Buttons
.tw-clickable {
@apply tw-cursor-pointer tw-gap-1 tw-rounded tw-font-medium tw-inline-flex tw-items-center tw-justify-center tw-duration-onhover-fast;
@apply active:tw-scale-click;
@apply disabled:tw-pointer-events-none disabled:tw-opacity-muted;
}
.tw-button {
@apply tw-clickable;
@apply tw-text-custom tw-leading-xs tw-bg-custom tw-bg-opacity-custom;
@apply tw-text-sm tw-h-[2.5em] tw-p-[0.75em];
@apply tw-relative before:tw-absolute before:tw-inset-0 before:tw-border before:tw-border-solid before:tw-border-custom before:tw-border-opacity-custom;
@apply before:tw-duration-fast before:tw-rounded;
@apply focus:tw-ring-2 focus:tw-ring-accent-primary focus:tw-ring-opacity-outline;
&::before {
mask-image: linear-gradient(to right, rgba(white, 0.5), rgba(white, 1), rgba(white, 0.5));
}
&:hover {
--bg-opacity: var(--bg-opacity-hover);
&::before {
--border-opacity: var(--border-opacity-hover);
}
}
}
.tw-button-primary {
@apply tw-button;
--text-color: var(--c-button-primary-color);
--bg-color: var(--c-button-primary-bg);
--border-color: var(--c-button-primary-border);
--bg-opacity: var(--o-button-primary-bg);
--border-opacity: var(--o-button-primary-border);
--bg-opacity-hover: var(--o-button-primary-bg-hover);
--border-opacity-hover: var(--o-button-primary-border-hover);
}
.tw-button-secondary {
@apply tw-button;
--text-color: var(--c-button-secondary-color);
--bg-color: var(--c-button-secondary-bg);
--border-color: var(--c-button-secondary-border);
--bg-opacity: var(--o-button-secondary-bg);
--border-opacity: var(--o-button-secondary-border);
--bg-opacity-hover: var(--o-button-secondary-bg-hover);
--border-opacity-hover: var(--o-button-secondary-border-hover);
}
// radio
.tw-radio-option {
@apply tw-size-6 tw-flex tw-items-center tw-justify-center tw-text-dim-3 hover:tw-text-dim-2 tw-duration-onhover-fast;
&.checked {
@apply tw-text-dim-1;
}
}
}

View File

@ -0,0 +1,6 @@
@mixin palette {
--c-black: 0, 0, 0;
--c-white: 255, 255, 255;
--c-accent-primary: 51, 150, 255; // #3396FF
--c-accent-secondary: 121, 48, 217; // #7930D9
}

View File

@ -0,0 +1,22 @@
@use 'themes';
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'palette';
@import 'base';
@import 'components';
@layer base {
:root {
/* COMMON PALETTE */
@include palette;
// Default: light mode
@include themes.light-mode;
}
.dark-mode {
@include themes.dark-mode;
}
}

View File

@ -0,0 +1,38 @@
@mixin dark-mode {
/* text */
--c-text-base: var(--c-white);
--c-text-dim-1: 225, 225, 229;
--c-text-dim-2: 187, 187, 187;
--c-text-dim-3: 124, 124, 124;
/* bg */
--c-bg-base: 30, 30, 30; // #1e1e1e
--c-bg-dim-1: 39, 42, 42; // #272a2a
--c-bg-placeholder: 20, 20, 20;
--c-divider: 64, 64, 64;
--c-divider-muted: 54, 54, 54;
--c-state-error: 255, 71, 117;
--c-state-success: 71, 255, 197;
/* buttons */
// primary
--c-button-primary-color: var(--c-text-base);
--c-button-primary-bg: var(--c-accent-primary);
--c-button-primary-border: var(--c-accent-primary);
--o-button-primary-bg: 1;
--o-button-primary-bg-hover: 0.8;
--o-button-primary-border: 1;
--o-button-primary-border-hover: 1;
// secondary
--c-button-secondary-color: var(--c-accent-primary);
--c-button-secondary-bg: var(--c-accent-primary);
--c-button-secondary-border: var(--c-accent-primary);
--o-button-secondary-bg: 0.1;
--o-button-secondary-bg-hover: 0.2;
--o-button-secondary-border: 0.3;
--o-button-secondary-border-hover: 0.6;
--o-outline: 0.3;
}

View File

@ -0,0 +1,2 @@
@import 'dark';
@import 'light';

View File

@ -0,0 +1,38 @@
@mixin light-mode {
/* text */
--c-text-base: 5, 5, 6;
--c-text-dim-1: 69, 77, 84;
--c-text-dim-2: 133, 144, 153;
--c-text-dim-3: 179, 185, 189;
/* bg */
--c-bg-base: 230, 235, 242;
--c-bg-dim-1: 249, 249, 255;
--c-bg-placeholder: 230, 235, 242;
--c-divider: 209, 215, 219;
--c-divider-muted: 230, 235, 242;
--c-state-error: 255, 71, 117;
--c-state-success: 71, 255, 197;
/* buttons */
// primary
--c-button-primary-color: var(--c-white);
--c-button-primary-bg: var(--c-accent-primary);
--c-button-primary-border: var(--c-accent-primary);
--o-button-primary-bg: 1;
--o-button-primary-bg-hover: 0.9;
--o-button-primary-border: 1;
--o-button-primary-border-hover: 1;
// secondary
--c-button-secondary-color: var(--c-accent-primary);
--c-button-secondary-bg: var(--c-accent-primary);
--c-button-secondary-border: var(--c-accent-primary);
--o-button-secondary-bg: 0.2;
--o-button-secondary-bg-hover: 0.25;
--o-button-secondary-border: 0.5;
--o-button-secondary-border-hover: 1;
--o-outline: 0.1;
}

View File

@ -0,0 +1,39 @@
<template>
<div class="tw-card tw-w-full tw-max-w-xs tw-space-y-6">
<div class="tw-space-y-6">
<div class="tw-flex tw-justify-between tw-items-start tw-gap-2">
<avatar-image class="tw--ml-2" :src="avatar" :loading="isLoading" />
<connected-badge />
</div>
<h3>{{ formattedAddress }}</h3>
</div>
<hr>
<div class="tw-text-dim-1 tw-text-xl tw-flex tw-items-center tw-gap-4">
<p class="tw-flex-1">
Balance
</p>
<eth-balance :value="balance" :loading="isLoading" />
</div>
<button class="tw-button-secondary tw-text-lg tw-w-full" @click="resetConnection()">
Sign Out
</button>
</div>
</template>
<script setup lang="ts">
import truncate from 'smart-truncate'
import { useConnectionStore } from '../stores'
const props = defineProps<{
address: string
}>()
const { address } = toRefs(props)
const formattedAddress = computed(() => truncate(address.value, 12, { position: 7 }))
const { balance, avatar, isLoading } = useAccount(address)
const { reset: resetConnection } = useConnectionStore()
</script>

View File

@ -0,0 +1,18 @@
<template>
<div class="tw-border tw-border-muted tw-bg-placeholder tw-circle-24 tw-flex tw-items-center tw-justify-center">
<loading-spinner v-if="loading" class="tw-text-2xl" />
<img v-else-if="src" :src="src" :alt="alt">
</div>
</template>
<script setup lang="ts">
interface Props {
src?: string | null
loading?: boolean
alt?: string
}
withDefaults(defineProps<Props>(), {
alt: 'Avatar',
})
</script>

View File

@ -0,0 +1,56 @@
<template>
<div class="tw-w-full tw-relative tw-flex tw-flex-col tw-items-center">
<img
src="/img/auth.png"
alt="WalletConnect Auth Client logo"
class="tw-absolute tw--top-14 tw-size-20 tw-blur-md"
>
<img
src="/img/auth.png"
alt="WalletConnect Auth Client logo"
class="tw-absolute tw--top-14 tw-size-20 tw-blur-px"
>
<div class="tw-card tw-text-center tw-w-full tw-max-w-xs">
<h2 class="tw-mt-2.5">
Sign in
</h2>
<button
class="tw-button-primary tw-text-xl tw-justify-between tw-w-full sm:tw-min-w-[11em]"
:disabled="!initialized || isLoading"
@click="requestConnection()"
>
<img src="/img/wc.png" alt="WalletConnect logo" class="tw-h-4 tw-w-auto tw-mx-auto">
<span class="tw-flex-1 tw-text-center tw-hidden sm:tw-inline">
<template v-if="initialized">
WalletConnect
</template>
<template v-else>
Initializing...
</template>
</span>
</button>
<p v-if="error" class="tw-text-sm tw-text-state-error">
{{ error }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useConnectionStore } from '../stores'
const connectionStore = useConnectionStore()
const { error, initialized } = storeToRefs(connectionStore)
const isLoading = ref(false)
const requestConnection = async () => {
isLoading.value = true
await connectionStore.requestConnection()
isLoading.value = false
}
</script>

View File

@ -0,0 +1,6 @@
<template>
<div class="tw-text-dim-2 tw-inline-flex tw-items-center tw-gap-2">
<span class="tw-relative tw-circle-[0.5em] tw-bg-state-success tw-inline-flex before:tw-circle-[0.5em] before:tw-scale-[1.2] before:tw-bg-state-success before:tw-animate-ping before:tw-opacity-soft before:tw-absolute" />
Connected
</div>
</template>

View File

@ -0,0 +1,17 @@
<template>
<loading-spinner v-if="loading" class="tw-h-8" />
<div v-else class="tw-h-8 tw-leading-xs tw-inline-flex tw-items-center tw-gap-2">
<img src="/img/eth.png" alt="ETH" class="tw-size-8">
{{ value }} ETH
</div>
</template>
<script setup lang="ts">
interface Props {
value?: number | null
loading?: boolean
}
withDefaults(defineProps<Props>(), {
value: 0,
})
</script>

View File

@ -0,0 +1,60 @@
<template>
<div class="tw-card tw-text-center tw-space-y-8 tw-w-full tw-max-w-sm">
<qr-code
background="transparent"
:foreground="foreground"
class="qr-code tw-py-4"
:value="uri"
:size="400"
/>
<hr class="tw-mx-12">
<div class="tw-space-y-4 tw-flex tw-flex-col tw-items-center">
<div class="tw-text-center">
<h3 class="tw-text-dim-1">
Scan with your phone
</h3>
<p class="tw-text-dim-2">
Open your camera app or mobile wallet and scan the code to connect
</p>
</div>
<button class="tw-button-secondary tw-text-lg tw-w-full sm:tw-min-w-[7em]" @click="copyLink()">
<icon name="copy" />
{{ message }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import QrCode from 'qrcode.vue'
const props = defineProps<{
uri: string
}>()
const colorMode = useColorMode()
const foreground = computed(
() => colorMode.value === Theme.dark
? '#fff'
: '#000',
)
const message = refAutoReset('Copy to clipboard', 1000)
const copyLink = async () => {
await navigator.clipboard.writeText(props.uri)
message.value = 'Copied!'
}
</script>
<style scoped>
.qr-code {
@apply tw-mx-auto;
width: 100% !important;
height: auto !important;
max-width: theme('maxWidth.xs');
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<lib-radio
v-model="$colorMode.preference"
:options="themeModes"
>
<template #option="{ option, checked }">
<a
v-if="option === 'system'"
role="button"
class="tw-radio-option"
:class="{ checked }"
>
<icon name="desktop" />
</a>
<a
v-else-if="option === 'light'"
role="button"
class="tw-radio-option"
:class="{ checked }"
>
<icon name="sun" class="tw-scale-[1.2]" />
</a>
<a
v-else-if="option === 'dark'"
role="button"
class="tw-radio-option"
:class="{ checked }"
>
<icon name="moon" />
</a>
</template>
</lib-radio>
</template>

View File

@ -0,0 +1,12 @@
<template>
<div class="tw-p-2.5 tw-rounded tw-inline-flex tw-items-center tw-gap-2 tw-bg-dim-1">
<img src="/img/wc-bg.png" alt="WalletConnect logo" class="tw-size-8">
<span class="tw-px-0.5">
v{{ authClientVersion }}
</span>
</div>
</template>
<script setup lang="ts">
const { authClientVersion } = useRuntimeConfig()
</script>

View File

@ -0,0 +1,50 @@
<template>
<radio-group v-model="model" class="tw-inline-flex tw-bg-dim-1 tw-p-3 tw-rounded tw-gap-2">
<radio-group-option
v-for="option, index of options"
:key="getKey(option, index)"
v-slot="{ checked }"
:value="getValue(option)"
>
<slot name="option" v-bind="{ option, checked }">
<div class="tw-radio-option tw-clickable" :class="{ checked }">
<radio-group-label as="div" class="tw-z-1">
<span class="tw-font-medium">
{{ option }}
</span>
</radio-group-label>
</div>
</slot>
</radio-group-option>
</radio-group>
</template>
<script setup lang="ts">
import {
RadioGroup,
RadioGroupLabel,
RadioGroupOption,
} from '@headlessui/vue'
type Value = any
type Option = any
interface Props {
modelValue: Value
options?: Option[]
// How to render list of the options
getKey?: (_option: Option, _index: number) => string | number
// Getter to a value to use as a model
getValue?: (_option: Option) => Value
}
const props = withDefaults(defineProps<Props>(), {
options: () => [],
getKey: (_option: Option, index: number) => index,
getValue: (option: Option) => option,
})
const model = useVModel(props)
</script>

View File

@ -0,0 +1,7 @@
<template>
<div class="tw-flex tw-justify-center tw-items-center">
<div class="tw-spinner-border tw-animate-spin tw-inline-block tw-circle-[1em] tw-border-[3px] tw-border-base tw-border-opacity-muted tw-border-l-transparent" role="status">
<span class="tw-hidden">Loading...</span>
</div>
</div>
</template>

View File

@ -0,0 +1,34 @@
import type { MaybeRef } from '@vueuse/shared'
import { providers } from 'ethers'
export const useAccount = (address: MaybeRef<string>) => {
const balance = ref<number | null>(null)
const avatar = ref<string | null>(null)
const isLoading = ref(false)
const updateAccountInfo = async () => {
const addressValue = unref(address)
if (addressValue) {
isLoading.value = true
const provider = providers.getDefaultProvider()
balance.value = (await provider.getBalance(addressValue)).toNumber()
avatar.value = await provider.getAvatar(addressValue)
isLoading.value = false
} else {
avatar.value = null
balance.value = null
}
}
// Register watcher for a ref
// or just call the effect once for regular value
if (isRef(address)) {
watchEffect(updateAccountInfo)
} else {
updateAccountInfo()
}
return { balance, avatar, isLoading }
}

View File

@ -0,0 +1,9 @@
export enum Theme {
light = 'light',
dark = 'dark',
}
export const themeModes = [
'system',
...Object.values(Theme),
]

View File

@ -0,0 +1,29 @@
import { version as authClientVersion } from '@walletconnect/auth-client/package.json'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
'nuxt-icon',
'@vueuse/nuxt',
'@pinia/nuxt',
],
tailwindcss: {
cssPath: '~/assets/scss/tailwind.scss',
},
runtimeConfig: {
public: {
/**
* Avoiding framework-specific prefixes for env vars
* see https://v3.nuxtjs.org/guide/features/runtime-config/#environment-variables
*/
WALLETCONNECT_PROJECT_ID: process.env.WALLETCONNECT_PROJECT_ID ?? '',
WALLETCONNECT_RELAY_URL: process.env.WALLETCONNECT_RELAY_URL ?? 'wss://relay.walletconnect.com',
authClientVersion,
},
},
})

View File

@ -0,0 +1,34 @@
{
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lintfix": "nr lint --fix"
},
"dependencies": {
"@headlessui/vue": "^1.7.4",
"@nuxtjs/color-mode": "^3.1.5",
"@nuxtjs/tailwindcss": "^6.1.3",
"@pinia/nuxt": "^0.4.3",
"@vueuse/nuxt": "^9.1.1",
"@walletconnect/auth-client": "1.0.1",
"ethers": "^5.7.0",
"nuxt-icon": "^0.1.7",
"pinia": "^2.0.23",
"qrcode.vue": "^3.3.3",
"sass": "^1.43.3",
"smart-truncate": "^1.0.1",
"typescript": "^4.5.2"
},
"devDependencies": {
"@antfu/eslint-config": "^0.29.4",
"@types/smart-truncate": "^1.0.2",
"eslint": "^8.27.0",
"nuxt": "3.0.0-rc.13"
}
}

View File

@ -0,0 +1,29 @@
<template>
<div class="tw-h-full tw-flex tw-items-center tw-justify-center">
<div class="tw-items-baseline tw-divide-x tw-divide-base tw-divide-opacity-muted tw-gap-4">
<div class="status-code tw-px-3">
404
</div>
<h2 class="tw-px-3 tw-font-thin">
Not found
</h2>
</div>
</div>
</template>
<style lang="css">
.status-code {
@apply tw-h1 tw-text-accent-primary tw-bg-no-repeat tw-relative;
background: url('/img/auth.png');
-webkit-background-clip: text;
background-clip: text;
color: transparent;
background-size: 200%;
background-position: center center;
filter: drop-shadow(0 0 6px var(--c-accent-secondary));
&::after {
@apply tw-absolute tw-blur-xs tw-opacity-muted tw-top-0 tw-left-1/2 tw--translate-x-1/2 tw-text-accent-primary;
content: "404";
}
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<div class="tw-h-full tw-flex tw-items-center tw-justify-center">
<client-only>
<account-card v-if="address" :address="address" />
<qr-view v-else-if="connectUri" :uri="connectUri" />
<connect-view v-else />
</client-only>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useConnectionStore } from '../stores'
const connectionStore = useConnectionStore()
const { connectUri, address } = storeToRefs(connectionStore)
// Init auth client
onMounted(connectionStore.init)
</script>

View File

@ -0,0 +1,5 @@
// Adjustment for AuthClient
// relying on the `global` field that webpack exposes (but vite doesn't)
window.global ||= window
export default {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

View File

@ -0,0 +1 @@
export * from './useConnectionStore'

View File

@ -0,0 +1,100 @@
import { acceptHMRUpdate, defineStore, skipHydrate } from 'pinia'
import AuthClient, { generateNonce } from '@walletconnect/auth-client'
export const useConnectionStore = defineStore('connection', () => {
const address = useLocalStorage<string | null>('address', null)
const connectUri = ref<string | null>(null)
const client = ref<AuthClient | null>(null)
const initialized = computed(() => !!client.value)
// reactive auth error
const error = ref<string | null>(null)
const setError = (e: unknown) => {
error.value = e?.toString() ?? null
console.error(e)
}
// Init client and listen to auth events
const config = useRuntimeConfig()
const init = () => {
AuthClient.init({
relayUrl: config.WALLETCONNECT_RELAY_URL,
projectId: config.WALLETCONNECT_PROJECT_ID,
metadata: {
name: 'vue-dapp-auth',
description: 'Vue 3 Example Dapp for Auth',
url: window.location.host,
icons: [`${window.location.origin}/img/wc-bg.png`],
},
})
.then((authClient) => {
client.value = authClient
})
.catch(setError)
}
watch(client, () => {
if (!client.value) {
return
}
client.value.on('auth_response', ({ params }) => {
if ('code' in params) {
return setError(params.message)
}
if ('error' in params) {
return setError(params.error)
}
address.value = params.result.p.iss.split(':')[4]
})
})
const reset = () => {
address.value = null
connectUri.value = null
error.value = null
}
// In case if the address is removed from the localStorage side,
// clear all the connection vars
watch(address, (newAddress, oldAddress) => {
if (oldAddress && !newAddress) {
reset()
}
})
const requestConnection = async () => {
if (!client.value) {
return
}
const { uri } = await client.value.request({
aud: window.location.href,
domain: window.location.hostname.split('.').slice(-2).join('.'),
chainId: 'eip155:1',
type: 'eip4361',
nonce: generateNonce(),
statement: 'Sign in with wallet.',
})
connectUri.value = uri
}
return {
// Skip hydration from init state
// as need to init only on the client side from localStorage
address: skipHydrate(address),
connectUri,
initialized,
error,
init,
reset,
requestConnection,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useConnectionStore, import.meta.hot))
}

View File

@ -0,0 +1,190 @@
const plugin = require('tailwindcss/plugin')
const addHeaders = require('./tailwind/headers')
// get formatted color, by color and opacity
function c(color, opacityValue) {
return opacityValue === undefined
? `rgb(var(${color}))`
: `rgba(var(${color}), ${opacityValue})`
}
// get color-maker method receiving an opacity as the input, by color
function co(color) {
return ({ opacityValue }) => c(color, opacityValue)
}
// Read more about tailwindcss configuration: https://tailwindcss.com/docs/configuration
module.exports = {
mode: 'jit',
prefix: 'tw-',
safelist: [
'light-mode',
'dark-mode',
],
theme: {
// structure
container: {
center: true,
padding: {
DEFAULT: '1.5rem',
md: '2rem',
lg: '2.5rem',
},
},
fontFamily: {
main: [
'-apple-system',
'"Segoe UI"',
'Helvetica',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
],
mono: ['monospace'],
},
lineHeight: {
none: 1,
xs: 1.1,
sm: 1.15,
},
// colors
colors: {
black: co('--c-black'),
white: co('--c-white'),
divider: co('--c-divider'),
muted: co('--c-divider-muted'),
accent: {
primary: co('--c-accent-primary'),
secondary: co('--c-accent-secondary'),
},
state: {
error: co('--c-state-error'),
success: co('--c-state-success'),
},
},
textColor: theme => ({
...theme('colors'),
'custom': co('--text-color'),
'base': co('--c-text-base'),
'dim-1': co('--c-text-dim-1'),
'dim-2': co('--c-text-dim-2'),
'dim-3': co('--c-text-dim-3'),
'bg': co('--c-bg-base'),
}),
backgroundColor: theme => ({
...theme('colors'),
'custom': co('--bg-color'),
'base': co('--c-bg-base'),
'dim-1': co('--c-bg-dim-1'),
'placeholder': co('--c-bg-placeholder'),
}),
borderColor: theme => ({
...theme('colors'),
base: co('--c-text-base'),
transparent: 'transparent',
custom: co('--border-color'),
}),
fill: theme => ({
...theme('backgroundColor'),
}),
stroke: theme => ({
...theme('borderColor'),
}),
// opacity
opacity: {
0: '0',
muted: '0.5',
soft: '0.8',
full: '1',
outline: 'var(--o-outline)',
},
textOpacity: theme => ({
...theme('opacity'),
custom: 'var(--text-opacity)',
}),
backgroundOpacity: theme => ({
...theme('opacity'),
custom: 'var(--bg-opacity)',
}),
borderOpacity: theme => ({
...theme('opacity'),
custom: 'var(--border-opacity)',
}),
transitionDuration: {
fast: '250ms',
normal: '500ms',
slow: '750ms',
},
zIndex: {
muted: '-1',
1: '1',
},
borderRadius: {
0: '0',
sm: '0.75rem',
DEFAULT: '1rem',
lg: '1.5rem',
full: '9999px',
},
scale: {
0: '0',
click: '0.975',
normal: '1',
},
extend: {
blur: {
px: '1px',
xs: '2px',
},
fontSize: {
'1/2': '0.5em',
'5/8': '0.625em',
'3/4': '0.75em',
'7/8': '0.875em',
'9/8': '1.125em',
'5/4': '1.25em',
'3/2': '1.5em',
},
boxShadow: {
// shadows for dialogs, popups etc
card: '0 0 2rem -1.75rem rgb(var(--c-accent-secondary))',
},
},
},
plugins: [
plugin(addHeaders),
({ addUtilities, matchUtilities, theme }) => {
const size = value => ({
height: value,
minHeight: value,
width: value,
minWidth: value,
})
matchUtilities(
{
size,
circle: value => ({
...size(value),
borderRadius: theme('borderRadius.full'),
}),
},
{ values: theme('height') },
)
addUtilities({
'.duration-onhover-fast': {
'transitionDuration': theme('transitionDuration.normal'),
'&:hover': {
transitionDuration: theme('transitionDuration.fast'),
},
},
})
},
],
}

View File

@ -0,0 +1,59 @@
module.exports = ({ addBase, theme, addUtilities }) => {
const commonHeaderStyles = {
display: 'inline-block',
}
const headerStyles = {
...commonHeaderStyles,
fontFamily: theme('fontFamily.display'),
fontWeight: theme('fontWeight.bold'),
lineHeight: theme('lineHeight.sm'),
}
const headerStylesStrong = {
...commonHeaderStyles,
fontFamily: theme('fontFamily.display'),
fontWeight: theme('fontWeight.black'),
lineHeight: theme('lineHeight.none'),
textShadow: theme('dropShadow.title'),
}
const headers = {
h1: {
...headerStylesStrong,
fontSize: theme('fontSize.4xl'),
marginBottom: '1em',
},
h2: {
...headerStylesStrong,
fontSize: theme('fontSize.3xl'),
marginBottom: '0.75em',
},
h3: {
...headerStylesStrong,
fontSize: theme('fontSize.2xl'),
marginBottom: '0.75em',
},
h4: {
...headerStyles,
fontSize: theme('fontSize.xl'),
marginBottom: '0.625em',
},
h5: {
...headerStyles,
fontSize: theme('fontSize.lg'),
marginBottom: '0.5em',
},
h6: {
...headerStyles,
fontSize: theme('fontSize.normal'),
marginBottom: '0.375em',
},
}
// bind styles to tags
addBase(headers)
// create .h# utils (e.g. `tw-h6` etc)
addUtilities(Object.entries(headers).reduce((utils, [tag, styles]) => ({
...utils,
[`.${tag}`]: styles,
}), {}))
}

View File

@ -0,0 +1,4 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

File diff suppressed because it is too large Load Diff