feat(vue-dapp-auth): Add example dapp on Vue 3 (#76)
Co-authored-by: Ben Kremer <contact@bkrem.dev>
This commit is contained in:
co-authored by
Ben Kremer
parent
882f9d10c8
commit
7e850d66c4
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user