@1001-digital/components 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -2
- package/src/base/components/Button.vue +2 -2
- package/src/base/components/Calendar.vue +227 -0
- package/src/base/components/Combobox.vue +241 -0
- package/src/base/components/ConfirmDialog.vue +33 -0
- package/src/base/components/Dialog.vue +7 -1
- package/src/base/components/FormDateField.vue +111 -0
- package/src/base/components/FormDatePicker.vue +382 -0
- package/src/base/components/FormSlider.vue +142 -0
- package/src/base/components/FormSwitch.vue +103 -0
- package/src/base/components/Globals.vue +9 -0
- package/src/base/components/Opepicon.vue +45 -0
- package/src/base/components/PinInput.vue +105 -0
- package/src/base/components/Progress.vue +66 -0
- package/src/base/components/Toasts.vue +6 -1
- package/src/base/composables/confirm.ts +29 -0
- package/src/base/composables/toast.ts +1 -0
- package/src/base/icons.ts +3 -1
- package/src/evm/components/EvmAvatar.vue +62 -0
- package/src/evm/components/EvmConnect.vue +83 -32
- package/src/evm/components/EvmConnectorQR.vue +12 -42
- package/src/evm/components/EvmProfile.vue +183 -0
- package/src/evm/components/EvmSwitchNetwork.vue +130 -0
- package/src/evm/components/EvmTransactionFlow.vue +41 -11
- package/src/evm/components/EvmWalletConnectWallets.vue +199 -0
- package/src/evm/composables/chainId.ts +2 -2
- package/src/evm/composables/uri.ts +11 -0
- package/src/evm/composables/walletExplorer.ts +130 -0
- package/src/evm/config.ts +1 -0
- package/src/evm/index.ts +9 -0
- package/src/evm/utils/uri.ts +24 -0
- package/src/index.ts +18 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button
|
|
3
|
+
@click="dialogOpen = true"
|
|
4
|
+
:class="className"
|
|
5
|
+
>
|
|
6
|
+
<slot
|
|
7
|
+
:address="address"
|
|
8
|
+
:display="display"
|
|
9
|
+
:ens-name="ensName"
|
|
10
|
+
:ens-avatar="ensAvatar"
|
|
11
|
+
>
|
|
12
|
+
{{ display }}
|
|
13
|
+
</slot>
|
|
14
|
+
</Button>
|
|
15
|
+
|
|
16
|
+
<Dialog
|
|
17
|
+
v-model:open="dialogOpen"
|
|
18
|
+
class="evm-profile"
|
|
19
|
+
title="Account"
|
|
20
|
+
>
|
|
21
|
+
<div class="profile-header">
|
|
22
|
+
<div
|
|
23
|
+
class="banner"
|
|
24
|
+
:style="
|
|
25
|
+
ensHeader ? { backgroundImage: `url(${ensHeader})` } : undefined
|
|
26
|
+
"
|
|
27
|
+
/>
|
|
28
|
+
<div class="avatar-wrapper">
|
|
29
|
+
<EvmAvatar :address="address" large />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="profile-identity">
|
|
34
|
+
<strong v-if="ensName">{{ ensName }}</strong>
|
|
35
|
+
<Button
|
|
36
|
+
class="link muted small"
|
|
37
|
+
@click="copyAddress"
|
|
38
|
+
>
|
|
39
|
+
<span>{{ shortAddr }}</span>
|
|
40
|
+
<Icon :type="copied ? 'check' : 'copy'" />
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="profile-actions">
|
|
45
|
+
<EvmSwitchNetwork>
|
|
46
|
+
<template #default="{ currentChain }">
|
|
47
|
+
<Icon type="wallet" />
|
|
48
|
+
<span>Switch Network ({{ currentChain?.name || 'Unknown' }})</span>
|
|
49
|
+
</template>
|
|
50
|
+
</EvmSwitchNetwork>
|
|
51
|
+
|
|
52
|
+
<Button
|
|
53
|
+
v-if="ensName"
|
|
54
|
+
:to="`https://app.ens.domains/${ensName}`"
|
|
55
|
+
target="_blank"
|
|
56
|
+
>
|
|
57
|
+
<Icon type="link" />
|
|
58
|
+
<span>Manage ENS</span>
|
|
59
|
+
</Button>
|
|
60
|
+
|
|
61
|
+
<Button
|
|
62
|
+
class="danger"
|
|
63
|
+
@click="disconnect"
|
|
64
|
+
>
|
|
65
|
+
<span>Disconnect</span>
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</Dialog>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import { ref, computed, nextTick } from 'vue'
|
|
73
|
+
import { useConnection, useDisconnect } from '@wagmi/vue'
|
|
74
|
+
import { useClipboard } from '@vueuse/core'
|
|
75
|
+
import { useConfirm } from '../../base/composables/confirm'
|
|
76
|
+
import { useEnsProfile } from '../composables/ens'
|
|
77
|
+
import { useResolveUri } from '../composables/uri'
|
|
78
|
+
import { shortAddress } from '../utils/addresses'
|
|
79
|
+
import Button from '../../base/components/Button.vue'
|
|
80
|
+
import Dialog from '../../base/components/Dialog.vue'
|
|
81
|
+
import Icon from '../../base/components/Icon.vue'
|
|
82
|
+
import EvmAvatar from './EvmAvatar.vue'
|
|
83
|
+
import EvmSwitchNetwork from './EvmSwitchNetwork.vue'
|
|
84
|
+
|
|
85
|
+
defineProps<{
|
|
86
|
+
className?: string
|
|
87
|
+
}>()
|
|
88
|
+
|
|
89
|
+
const emit = defineEmits<{
|
|
90
|
+
disconnected: []
|
|
91
|
+
}>()
|
|
92
|
+
|
|
93
|
+
const { address } = useConnection()
|
|
94
|
+
const { mutate: disconnectWallet } = useDisconnect()
|
|
95
|
+
const { confirm } = useConfirm()
|
|
96
|
+
const { data: ensData } = useEnsProfile(address)
|
|
97
|
+
|
|
98
|
+
const { copy, copied } = useClipboard()
|
|
99
|
+
const resolve = useResolveUri()
|
|
100
|
+
|
|
101
|
+
const ensName = computed(() => ensData.value?.ens || null)
|
|
102
|
+
const ensAvatar = computed(() => resolve(ensData.value?.data?.avatar))
|
|
103
|
+
const ensHeader = computed(() => resolve(ensData.value?.data?.header))
|
|
104
|
+
|
|
105
|
+
const shortAddr = computed(() =>
|
|
106
|
+
address.value ? shortAddress(address.value) : '',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const display = computed(() => ensName.value || shortAddr.value)
|
|
110
|
+
|
|
111
|
+
const dialogOpen = ref(false)
|
|
112
|
+
|
|
113
|
+
const copyAddress = () => {
|
|
114
|
+
if (address.value) copy(address.value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const disconnect = async () => {
|
|
118
|
+
const confirmed = await confirm({
|
|
119
|
+
title: 'Disconnect Wallet',
|
|
120
|
+
description: 'Are you sure you want to disconnect your wallet?',
|
|
121
|
+
okText: 'Disconnect',
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (!confirmed) return
|
|
125
|
+
|
|
126
|
+
dialogOpen.value = false
|
|
127
|
+
await nextTick()
|
|
128
|
+
disconnectWallet()
|
|
129
|
+
emit('disconnected')
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<style scoped>
|
|
134
|
+
.profile-header {
|
|
135
|
+
position: relative;
|
|
136
|
+
margin: calc(var(--spacer) * -3) calc(var(--spacer) * -1) 0;
|
|
137
|
+
margin-bottom: 0;
|
|
138
|
+
z-index: -1;
|
|
139
|
+
|
|
140
|
+
.banner {
|
|
141
|
+
aspect-ratio: 3 / 1;
|
|
142
|
+
width: 100%;
|
|
143
|
+
height: auto;
|
|
144
|
+
background-color: var(--gray-z-1);
|
|
145
|
+
background-size: cover;
|
|
146
|
+
background-position: center;
|
|
147
|
+
border-bottom: var(--border);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.avatar-wrapper {
|
|
151
|
+
display: flex;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
margin-top: -10%;
|
|
154
|
+
position: relative;
|
|
155
|
+
z-index: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
:deep(.evm-avatar) {
|
|
159
|
+
border: 3px solid var(--background);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.profile-identity {
|
|
164
|
+
display: grid;
|
|
165
|
+
justify-items: center;
|
|
166
|
+
gap: var(--spacer-sm);
|
|
167
|
+
|
|
168
|
+
> strong {
|
|
169
|
+
font-size: var(--font-lg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.profile-actions {
|
|
174
|
+
display: grid;
|
|
175
|
+
gap: var(--spacer);
|
|
176
|
+
padding-block-start: var(--spacer);
|
|
177
|
+
|
|
178
|
+
& :deep(.button),
|
|
179
|
+
& :deep(button) {
|
|
180
|
+
width: 100%;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button
|
|
3
|
+
@click="dialogOpen = true"
|
|
4
|
+
:class="className"
|
|
5
|
+
>
|
|
6
|
+
<slot :current-chain="currentChain">
|
|
7
|
+
{{ currentChain?.name || 'Unknown Network' }}
|
|
8
|
+
</slot>
|
|
9
|
+
</Button>
|
|
10
|
+
|
|
11
|
+
<Dialog
|
|
12
|
+
title="Switch Network"
|
|
13
|
+
v-model:open="dialogOpen"
|
|
14
|
+
@closed="onClosed"
|
|
15
|
+
>
|
|
16
|
+
<Alert
|
|
17
|
+
v-if="errorMessage"
|
|
18
|
+
type="error"
|
|
19
|
+
>
|
|
20
|
+
{{ errorMessage }}
|
|
21
|
+
</Alert>
|
|
22
|
+
|
|
23
|
+
<Loading
|
|
24
|
+
v-if="switching"
|
|
25
|
+
spinner
|
|
26
|
+
stacked
|
|
27
|
+
:txt="`Switching to ${switchingTo}...`"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
v-if="!switching"
|
|
32
|
+
class="chain-list"
|
|
33
|
+
>
|
|
34
|
+
<Button
|
|
35
|
+
v-for="chain in chains"
|
|
36
|
+
:key="chain.id"
|
|
37
|
+
:disabled="chain.id === currentChainId || undefined"
|
|
38
|
+
:class="['chain-item', { active: chain.id === currentChainId }]"
|
|
39
|
+
@click="() => switchTo(chain)"
|
|
40
|
+
>
|
|
41
|
+
<span>{{ chain.name }}</span>
|
|
42
|
+
<Icon
|
|
43
|
+
v-if="chain.id === currentChainId"
|
|
44
|
+
type="check"
|
|
45
|
+
/>
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</Dialog>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup lang="ts">
|
|
52
|
+
import { ref, computed } from 'vue'
|
|
53
|
+
import type { Chain } from 'viem'
|
|
54
|
+
import { useConfig, useConnection, useSwitchChain } from '@wagmi/vue'
|
|
55
|
+
import Button from '../../base/components/Button.vue'
|
|
56
|
+
import Dialog from '../../base/components/Dialog.vue'
|
|
57
|
+
import Icon from '../../base/components/Icon.vue'
|
|
58
|
+
import Alert from '../../base/components/Alert.vue'
|
|
59
|
+
import Loading from '../../base/components/Loading.vue'
|
|
60
|
+
|
|
61
|
+
defineProps<{
|
|
62
|
+
className?: string
|
|
63
|
+
}>()
|
|
64
|
+
|
|
65
|
+
const emit = defineEmits<{
|
|
66
|
+
switched: [{ chainId: number; name: string }]
|
|
67
|
+
error: [{ message: string }]
|
|
68
|
+
}>()
|
|
69
|
+
|
|
70
|
+
const config = useConfig()
|
|
71
|
+
const { chainId: currentChainId } = useConnection()
|
|
72
|
+
const { mutateAsync: switchChainAsync } = useSwitchChain()
|
|
73
|
+
|
|
74
|
+
const chains = computed<readonly Chain[]>(() => config.chains)
|
|
75
|
+
const currentChain = computed(() =>
|
|
76
|
+
chains.value.find((c) => c.id === currentChainId.value),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const dialogOpen = ref(false)
|
|
80
|
+
const switching = ref(false)
|
|
81
|
+
const switchingTo = ref('')
|
|
82
|
+
const errorMessage = ref('')
|
|
83
|
+
|
|
84
|
+
const switchTo = async (chain: Chain) => {
|
|
85
|
+
if (chain.id === currentChainId.value) return
|
|
86
|
+
|
|
87
|
+
switching.value = true
|
|
88
|
+
switchingTo.value = chain.name
|
|
89
|
+
errorMessage.value = ''
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await switchChainAsync({ chainId: chain.id })
|
|
93
|
+
emit('switched', { chainId: chain.id, name: chain.name })
|
|
94
|
+
dialogOpen.value = false
|
|
95
|
+
} catch (e: unknown) {
|
|
96
|
+
const message = e instanceof Error ? e.message : 'Failed to switch network.'
|
|
97
|
+
errorMessage.value = message.includes('rejected') || message.includes('denied')
|
|
98
|
+
? 'Network switch cancelled.'
|
|
99
|
+
: 'Failed to switch network. Please try again.'
|
|
100
|
+
emit('error', { message: errorMessage.value })
|
|
101
|
+
} finally {
|
|
102
|
+
switching.value = false
|
|
103
|
+
switchingTo.value = ''
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const onClosed = () => {
|
|
108
|
+
switching.value = false
|
|
109
|
+
switchingTo.value = ''
|
|
110
|
+
errorMessage.value = ''
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<style scoped>
|
|
115
|
+
.chain-list {
|
|
116
|
+
display: grid;
|
|
117
|
+
gap: var(--spacer-sm);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.chain-item {
|
|
121
|
+
width: 100%;
|
|
122
|
+
inline-size: auto;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
|
|
125
|
+
&.active {
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
:click-outside="canDismiss"
|
|
13
13
|
:title="text.title[step]"
|
|
14
14
|
class="transaction-flow"
|
|
15
|
+
compat
|
|
15
16
|
>
|
|
16
17
|
<slot name="before" />
|
|
17
18
|
|
|
@@ -19,7 +20,9 @@
|
|
|
19
20
|
v-if="step === 'requesting'"
|
|
20
21
|
spinner
|
|
21
22
|
stacked
|
|
22
|
-
:txt="
|
|
23
|
+
:txt="connector?.name
|
|
24
|
+
? `Requesting signature from ${connector.name}...`
|
|
25
|
+
: text.lead[step] || ''"
|
|
23
26
|
/>
|
|
24
27
|
|
|
25
28
|
<p v-if="step !== 'requesting' && step !== 'error' && text.lead[step]">
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
<script setup lang="ts">
|
|
74
77
|
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
|
75
78
|
import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
|
|
76
|
-
import { useConfig, type Config } from '@wagmi/vue'
|
|
79
|
+
import { useConfig, useConnection, type Config } from '@wagmi/vue'
|
|
77
80
|
import type { TransactionReceipt, Hash } from 'viem'
|
|
78
81
|
import Dialog from '../../base/components/Dialog.vue'
|
|
79
82
|
import Loading from '../../base/components/Loading.vue'
|
|
@@ -120,14 +123,9 @@ const defaultText = {
|
|
|
120
123
|
},
|
|
121
124
|
} satisfies TextConfig
|
|
122
125
|
|
|
123
|
-
const checkChain = useEnsureChainIdCheck()
|
|
124
|
-
|
|
125
|
-
const wagmiConfig = useConfig()
|
|
126
|
-
const blockExplorer = useBlockExplorer()
|
|
127
|
-
const toast = useToast()
|
|
128
|
-
|
|
129
126
|
const props = withDefaults(
|
|
130
127
|
defineProps<{
|
|
128
|
+
chain?: string
|
|
131
129
|
text?: TextConfig
|
|
132
130
|
request?: () => Promise<Hash>
|
|
133
131
|
delayAfter?: number
|
|
@@ -145,6 +143,24 @@ const props = withDefaults(
|
|
|
145
143
|
},
|
|
146
144
|
)
|
|
147
145
|
|
|
146
|
+
function isUserRejection(e: unknown): boolean {
|
|
147
|
+
const re = /reject|denied|cancel/i
|
|
148
|
+
let current = e as Record<string, unknown> | undefined
|
|
149
|
+
while (current) {
|
|
150
|
+
if ((current as { code?: number }).code === 4001) return true
|
|
151
|
+
if (re.test((current as { details?: string }).details || '')) return true
|
|
152
|
+
current = current.cause as Record<string, unknown> | undefined
|
|
153
|
+
}
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const checkChain = useEnsureChainIdCheck(props.chain)
|
|
158
|
+
|
|
159
|
+
const wagmiConfig = useConfig()
|
|
160
|
+
const { connector } = useConnection()
|
|
161
|
+
const blockExplorer = useBlockExplorer(props.chain)
|
|
162
|
+
const toast = useToast()
|
|
163
|
+
|
|
148
164
|
const emit = defineEmits<{
|
|
149
165
|
complete: [receipt: TransactionReceipt]
|
|
150
166
|
cancel: []
|
|
@@ -192,8 +208,11 @@ const receipt = ref<TransactionReceipt | null>(null)
|
|
|
192
208
|
const txLink = computed(() => `${blockExplorer}/tx/${tx.value}`)
|
|
193
209
|
|
|
194
210
|
let mounted = true
|
|
211
|
+
let progressTimer: ReturnType<typeof setInterval> | undefined
|
|
212
|
+
|
|
195
213
|
onBeforeUnmount(() => {
|
|
196
214
|
mounted = false
|
|
215
|
+
clearInterval(progressTimer)
|
|
197
216
|
})
|
|
198
217
|
|
|
199
218
|
const canDismiss = computed(
|
|
@@ -217,10 +236,10 @@ const initializeRequest = async (request = cachedRequest.value) => {
|
|
|
217
236
|
step.value = 'requesting'
|
|
218
237
|
tx.value = await request!()
|
|
219
238
|
} catch (e: unknown) {
|
|
220
|
-
|
|
221
|
-
if (err?.cause?.code === 4001) {
|
|
239
|
+
if (isUserRejection(e)) {
|
|
222
240
|
error.value = 'Transaction rejected by user.'
|
|
223
241
|
} else {
|
|
242
|
+
const err = e as { shortMessage?: string }
|
|
224
243
|
error.value = err.shortMessage || 'Error submitting transaction request.'
|
|
225
244
|
}
|
|
226
245
|
step.value = 'error'
|
|
@@ -238,17 +257,26 @@ const initializeRequest = async (request = cachedRequest.value) => {
|
|
|
238
257
|
description: text.value.lead.waiting,
|
|
239
258
|
duration: Infinity,
|
|
240
259
|
loading: true,
|
|
260
|
+
progress: 0,
|
|
241
261
|
action: {
|
|
242
262
|
label: 'View on Block Explorer',
|
|
243
263
|
onClick: () => window.open(link, '_blank'),
|
|
244
264
|
},
|
|
245
265
|
})
|
|
246
266
|
|
|
267
|
+
const start = Date.now()
|
|
268
|
+
progressTimer = setInterval(() => {
|
|
269
|
+
const elapsed = (Date.now() - start) / 1000
|
|
270
|
+
toast.update(toastId, { progress: Math.round(90 * (1 - Math.exp(-elapsed / 15))) })
|
|
271
|
+
}, 500)
|
|
272
|
+
|
|
247
273
|
try {
|
|
248
274
|
const receiptObject = await waitForTransactionReceipt(
|
|
249
275
|
wagmiConfig as Config,
|
|
250
276
|
{ hash: tx.value },
|
|
251
277
|
)
|
|
278
|
+
clearInterval(progressTimer)
|
|
279
|
+
toast.update(toastId, { progress: 100, loading: false })
|
|
252
280
|
await delay(props.delayAfter)
|
|
253
281
|
receipt.value = receiptObject
|
|
254
282
|
emit('complete', receiptObject)
|
|
@@ -257,10 +285,11 @@ const initializeRequest = async (request = cachedRequest.value) => {
|
|
|
257
285
|
variant: 'success',
|
|
258
286
|
title: text.value.title.complete,
|
|
259
287
|
description: text.value.lead.complete,
|
|
260
|
-
|
|
288
|
+
progress: false,
|
|
261
289
|
...(props.autoCloseSuccess && { duration: props.delayAutoclose }),
|
|
262
290
|
})
|
|
263
291
|
} catch (e: unknown) {
|
|
292
|
+
clearInterval(progressTimer)
|
|
264
293
|
const err = e as { shortMessage?: string }
|
|
265
294
|
if (mounted) {
|
|
266
295
|
toast.dismiss(toastId)
|
|
@@ -272,6 +301,7 @@ const initializeRequest = async (request = cachedRequest.value) => {
|
|
|
272
301
|
title: text.value.title.error,
|
|
273
302
|
description: err.shortMessage || 'Transaction failed.',
|
|
274
303
|
loading: false,
|
|
304
|
+
progress: false,
|
|
275
305
|
})
|
|
276
306
|
}
|
|
277
307
|
console.log(e)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="wc-wallets">
|
|
3
|
+
<EvmConnectorQR :uri="uri">
|
|
4
|
+
<template #instruction> Scan with your wallet app </template>
|
|
5
|
+
</EvmConnectorQR>
|
|
6
|
+
|
|
7
|
+
<div class="separator">
|
|
8
|
+
<span>Or choose a wallet</span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<FormItem>
|
|
12
|
+
<template #prefix>
|
|
13
|
+
<Icon type="lucide:search" />
|
|
14
|
+
</template>
|
|
15
|
+
<input
|
|
16
|
+
v-model="searchQuery"
|
|
17
|
+
type="text"
|
|
18
|
+
placeholder="Search wallets"
|
|
19
|
+
@input="onSearchInput"
|
|
20
|
+
/>
|
|
21
|
+
</FormItem>
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
v-if="displayedWallets.length > 0"
|
|
25
|
+
class="wallet-grid"
|
|
26
|
+
>
|
|
27
|
+
<Button
|
|
28
|
+
v-for="wallet in displayedWallets"
|
|
29
|
+
:key="wallet.id"
|
|
30
|
+
@click="openWallet(wallet)"
|
|
31
|
+
class="wallet-card tertiary"
|
|
32
|
+
>
|
|
33
|
+
<img
|
|
34
|
+
:src="explorer.imageUrl(wallet)"
|
|
35
|
+
:alt="wallet.name"
|
|
36
|
+
loading="lazy"
|
|
37
|
+
/>
|
|
38
|
+
<span>{{ wallet.name }}</span>
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<p
|
|
43
|
+
v-if="
|
|
44
|
+
searchQuery &&
|
|
45
|
+
!explorer.searching.value &&
|
|
46
|
+
displayedWallets.length === 0
|
|
47
|
+
"
|
|
48
|
+
class="empty-state"
|
|
49
|
+
>
|
|
50
|
+
No wallets found
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<Loading
|
|
54
|
+
v-if="explorer.loading.value || explorer.searching.value"
|
|
55
|
+
spinner
|
|
56
|
+
stacked
|
|
57
|
+
txt=""
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<Button
|
|
61
|
+
to="https://ethereum.org/wallets/"
|
|
62
|
+
target="_blank"
|
|
63
|
+
class="link muted small help-link"
|
|
64
|
+
>
|
|
65
|
+
<Icon type="help" />
|
|
66
|
+
<span>New to wallets?</span>
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
73
|
+
import Button from '../../base/components/Button.vue'
|
|
74
|
+
import FormItem from '../../base/components/FormItem.vue'
|
|
75
|
+
import Icon from '../../base/components/Icon.vue'
|
|
76
|
+
import Loading from '../../base/components/Loading.vue'
|
|
77
|
+
import EvmConnectorQR from './EvmConnectorQR.vue'
|
|
78
|
+
import {
|
|
79
|
+
useWalletExplorer,
|
|
80
|
+
type ExplorerWallet,
|
|
81
|
+
} from '../composables/walletExplorer'
|
|
82
|
+
|
|
83
|
+
const props = defineProps<{
|
|
84
|
+
uri: string
|
|
85
|
+
}>()
|
|
86
|
+
|
|
87
|
+
const explorer = useWalletExplorer()
|
|
88
|
+
const searchQuery = ref('')
|
|
89
|
+
let searchTimeout: ReturnType<typeof setTimeout>
|
|
90
|
+
|
|
91
|
+
const TOP_COUNT = 9
|
|
92
|
+
|
|
93
|
+
const displayedWallets = computed(() => {
|
|
94
|
+
if (searchQuery.value) return explorer.searchResults.value
|
|
95
|
+
|
|
96
|
+
const recents = explorer.recentWallets.value
|
|
97
|
+
const recentIds = new Set(recents.map((w) => w.id))
|
|
98
|
+
const rest = explorer.wallets.value
|
|
99
|
+
.filter((w) => !recentIds.has(w.id))
|
|
100
|
+
.slice(0, TOP_COUNT - recents.length)
|
|
101
|
+
|
|
102
|
+
return [...recents, ...rest]
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
function openWallet(wallet: ExplorerWallet) {
|
|
106
|
+
const href = explorer.walletHref(wallet, props.uri)
|
|
107
|
+
if (href) {
|
|
108
|
+
window.open(href, '_blank', 'noreferrer')
|
|
109
|
+
}
|
|
110
|
+
explorer.addRecent(wallet.id)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onSearchInput() {
|
|
114
|
+
clearTimeout(searchTimeout)
|
|
115
|
+
searchTimeout = setTimeout(() => {
|
|
116
|
+
if (searchQuery.value) {
|
|
117
|
+
explorer.search(searchQuery.value)
|
|
118
|
+
}
|
|
119
|
+
}, 300)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onBeforeUnmount(() => {
|
|
123
|
+
clearTimeout(searchTimeout)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
onMounted(() => {
|
|
127
|
+
explorer.fetchNextPage()
|
|
128
|
+
})
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.wc-wallets {
|
|
133
|
+
display: grid;
|
|
134
|
+
gap: var(--spacer);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Separator */
|
|
138
|
+
.separator {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: var(--spacer-sm);
|
|
142
|
+
@mixin ui-font;
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
font-size: var(--font-sm);
|
|
145
|
+
|
|
146
|
+
&::before,
|
|
147
|
+
&::after {
|
|
148
|
+
content: '';
|
|
149
|
+
flex: 1;
|
|
150
|
+
border-top: var(--border);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Wallet grid */
|
|
155
|
+
.wallet-grid {
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
|
|
158
|
+
gap: var(--spacer-sm);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.wallet-card {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: var(--spacer-sm);
|
|
166
|
+
block-size: auto;
|
|
167
|
+
min-inline-size: 0;
|
|
168
|
+
inline-size: 100%;
|
|
169
|
+
|
|
170
|
+
img {
|
|
171
|
+
width: var(--size-6);
|
|
172
|
+
height: var(--size-6);
|
|
173
|
+
border-radius: var(--border-radius);
|
|
174
|
+
margin-top: var(--spacer-xs);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
span {
|
|
178
|
+
font-size: var(--font-xs);
|
|
179
|
+
text-align: center;
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
text-overflow: ellipsis;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
max-width: 100%;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Help link */
|
|
188
|
+
.help-link {
|
|
189
|
+
justify-self: center;
|
|
190
|
+
font-size: var(--font-xs);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Empty state */
|
|
194
|
+
.empty-state {
|
|
195
|
+
text-align: center;
|
|
196
|
+
@mixin ui-font;
|
|
197
|
+
color: var(--muted);
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
@@ -22,8 +22,8 @@ export const useMainChainId = () => useChainConfig().id
|
|
|
22
22
|
export const useBlockExplorer = (key?: string) =>
|
|
23
23
|
useChainConfig(key).blockExplorer
|
|
24
24
|
|
|
25
|
-
export const useEnsureChainIdCheck = () => {
|
|
26
|
-
const chainId =
|
|
25
|
+
export const useEnsureChainIdCheck = (key?: string) => {
|
|
26
|
+
const chainId = useChainConfig(key).id
|
|
27
27
|
const { mutateAsync: switchChain } = useSwitchChain()
|
|
28
28
|
const { chainId: currentChainId } = useConnection()
|
|
29
29
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { resolveUri } from '../utils/uri'
|
|
2
|
+
|
|
3
|
+
export const useResolveUri = () => {
|
|
4
|
+
const appConfig = useAppConfig()
|
|
5
|
+
|
|
6
|
+
return (uri?: string) =>
|
|
7
|
+
resolveUri(uri, {
|
|
8
|
+
ipfsGateway: appConfig.evm?.ipfsGateway,
|
|
9
|
+
arweaveGateway: appConfig.evm?.arweaveGateway,
|
|
10
|
+
})
|
|
11
|
+
}
|