@1001-digital/components 1.2.0 → 1.2.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1001-digital/components",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -69,7 +69,7 @@
69
69
  </Button>
70
70
  <Button
71
71
  v-if="inAppConnector"
72
- @click="showInAppSetup = true"
72
+ @click="() => { emit('connecting'); showInAppSetup = true }"
73
73
  class="block choose-connector"
74
74
  >
75
75
  <img
@@ -127,6 +127,7 @@ const PRIORITY: Record<string, number> = {
127
127
  }
128
128
 
129
129
  const emit = defineEmits<{
130
+ connecting: []
130
131
  connected: []
131
132
  }>()
132
133
 
@@ -179,6 +180,7 @@ const loginWithSafe = () => {
179
180
  }
180
181
 
181
182
  const login = async (connector: Connector) => {
183
+ emit('connecting')
182
184
  errorMessage.value = ''
183
185
  isConnecting.value = true
184
186
  connectingWallet.value = safeDeepLink.value ? 'Safe' : connector.name
@@ -214,10 +216,9 @@ const login = async (connector: Connector) => {
214
216
  try {
215
217
  await connectAsync({ connector, chainId: chainId.value })
216
218
 
217
- setTimeout(() => {
218
- resetConnection()
219
- emit('connected')
220
- }, 100)
219
+ emit('connected')
220
+
221
+ resetConnection()
221
222
  } catch (error: unknown) {
222
223
  isConnecting.value = false
223
224
  metaMaskUri.value = ''
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <Loading
3
+ v-if="step === 'signing'"
4
+ spinner
5
+ stacked
6
+ :txt="connector?.name
7
+ ? `Requesting signature from ${connector.name}...`
8
+ : 'Requesting signature...'"
9
+ />
10
+
11
+ <Loading
12
+ v-else-if="step === 'verifying'"
13
+ spinner
14
+ stacked
15
+ txt="Verifying signature..."
16
+ />
17
+
18
+ <template v-else-if="step === 'complete'">
19
+ <slot name="complete">
20
+ <Alert type="info">
21
+ <p>Successfully signed in.</p>
22
+ </Alert>
23
+ </slot>
24
+ </template>
25
+
26
+ <template v-else-if="step === 'error'">
27
+ <Alert type="error">
28
+ <p>{{ errorMessage }}</p>
29
+ </Alert>
30
+ <Button
31
+ class="secondary"
32
+ @click="signIn"
33
+ >Try Again</Button>
34
+ </template>
35
+
36
+ <template v-else>
37
+ <slot name="idle">
38
+ <p v-if="props.statement">{{ props.statement }}</p>
39
+ </slot>
40
+ <Button @click="signIn">
41
+ Sign In
42
+ </Button>
43
+ </template>
44
+ </template>
45
+
46
+ <script setup lang="ts">
47
+ import { ref } from 'vue'
48
+ import { signMessage } from '@wagmi/core'
49
+ import { useConfig, useConnection } from '@wagmi/vue'
50
+ import type { Config } from '@wagmi/vue'
51
+ import Button from '../../base/components/Button.vue'
52
+ import Alert from '../../base/components/Alert.vue'
53
+ import Loading from '../../base/components/Loading.vue'
54
+ import { createSiweMessage } from '../utils/siwe'
55
+ import { useSiwe } from '../composables/siwe'
56
+
57
+ type Step = 'idle' | 'signing' | 'verifying' | 'complete' | 'error'
58
+
59
+ const props = defineProps<{
60
+ getNonce: () => Promise<string>
61
+ verify: (message: string, signature: string) => Promise<boolean>
62
+ domain?: string
63
+ statement?: string
64
+ uri?: string
65
+ resources?: string[]
66
+ requestId?: string
67
+ expirationTime?: string
68
+ }>()
69
+
70
+ const emit = defineEmits<{
71
+ authenticated: [{ address: `0x${string}`; chainId: number }]
72
+ error: [error: string]
73
+ }>()
74
+
75
+ function isUserRejection(e: unknown): boolean {
76
+ const re = /reject|denied|cancel/i
77
+ let current = e as Record<string, unknown> | undefined
78
+ while (current) {
79
+ if ((current as { code?: number }).code === 4001) return true
80
+ if (re.test((current as { details?: string }).details || '')) return true
81
+ if (re.test((current as { message?: string }).message || '')) return true
82
+ current = current.cause as Record<string, unknown> | undefined
83
+ }
84
+ return false
85
+ }
86
+
87
+ const config = useConfig()
88
+ const { address, chainId, connector } = useConnection()
89
+ const { setSession } = useSiwe()
90
+
91
+ const step = ref<Step>('idle')
92
+ const errorMessage = ref('')
93
+
94
+ const signIn = async () => {
95
+ const currentAddress = address.value
96
+ const currentChainId = chainId.value
97
+
98
+ if (!currentAddress || !currentChainId) {
99
+ errorMessage.value = 'Wallet not connected.'
100
+ step.value = 'error'
101
+ emit('error', errorMessage.value)
102
+ return
103
+ }
104
+
105
+ errorMessage.value = ''
106
+
107
+ // Get nonce
108
+ let nonce: string
109
+ try {
110
+ nonce = await props.getNonce()
111
+ } catch {
112
+ errorMessage.value = 'Failed to get authentication nonce.'
113
+ step.value = 'error'
114
+ emit('error', errorMessage.value)
115
+ return
116
+ }
117
+
118
+ // Sign message
119
+ step.value = 'signing'
120
+ const message = createSiweMessage({
121
+ domain: props.domain || window.location.host,
122
+ address: currentAddress,
123
+ uri: props.uri || window.location.origin,
124
+ chainId: currentChainId,
125
+ nonce,
126
+ statement: props.statement,
127
+ expirationTime: props.expirationTime,
128
+ requestId: props.requestId,
129
+ resources: props.resources,
130
+ })
131
+
132
+ let signature: string
133
+ try {
134
+ signature = await signMessage(config as Config, { message })
135
+ } catch (e: unknown) {
136
+ if (isUserRejection(e)) {
137
+ errorMessage.value = 'Signature rejected by user.'
138
+ } else {
139
+ const err = e as { shortMessage?: string; message?: string }
140
+ errorMessage.value = err.shortMessage || err.message || 'Failed to sign message.'
141
+ }
142
+ step.value = 'error'
143
+ emit('error', errorMessage.value)
144
+ console.error('SIWE signing error:', e)
145
+ return
146
+ }
147
+
148
+ // Verify with backend
149
+ step.value = 'verifying'
150
+ try {
151
+ const verified = await props.verify(message, signature)
152
+
153
+ if (!verified) {
154
+ throw new Error('Signature verification failed')
155
+ }
156
+ } catch (e: unknown) {
157
+ const err = e as { message?: string }
158
+ errorMessage.value = err.message || 'Verification failed.'
159
+ step.value = 'error'
160
+ emit('error', errorMessage.value)
161
+ console.error('SIWE verification error:', e)
162
+ return
163
+ }
164
+
165
+ // Update shared authentication state
166
+ setSession({
167
+ address: currentAddress,
168
+ chainId: currentChainId,
169
+ })
170
+
171
+ step.value = 'complete'
172
+ emit('authenticated', {
173
+ address: currentAddress,
174
+ chainId: currentChainId,
175
+ })
176
+ }
177
+
178
+ const reset = () => {
179
+ step.value = 'idle'
180
+ errorMessage.value = ''
181
+ }
182
+
183
+ defineExpose({ reset })
184
+ </script>
185
+
186
+ <style scoped>
187
+ .secondary {
188
+ margin-top: var(--spacer-sm);
189
+ }
190
+ </style>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <Button
3
+ v-if="!isAuthenticated"
4
+ @click="open = true"
5
+ :class="className"
6
+ >
7
+ <slot>Sign In</slot>
8
+ </Button>
9
+ <slot
10
+ v-else
11
+ name="authenticated"
12
+ :address="session?.address"
13
+ :sign-out="handleSignOut"
14
+ >
15
+ <Button
16
+ @click="handleSignOut"
17
+ :class="className"
18
+ >Sign Out</Button>
19
+ </slot>
20
+
21
+ <Dialog
22
+ v-if="!isAuthenticated"
23
+ title="Sign In with Ethereum"
24
+ v-model:open="open"
25
+ @closed="onClosed"
26
+ >
27
+ <EvmSiwe
28
+ ref="siweRef"
29
+ :get-nonce="getNonce"
30
+ :verify="verify"
31
+ :domain="domain"
32
+ :statement="statement"
33
+ :uri="uri"
34
+ :resources="resources"
35
+ :request-id="requestId"
36
+ :expiration-time="expirationTime"
37
+ @authenticated="onAuthenticated"
38
+ @error="(e) => emit('error', e)"
39
+ />
40
+ </Dialog>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ import { ref, watch } from 'vue'
45
+ import Button from '../../base/components/Button.vue'
46
+ import Dialog from '../../base/components/Dialog.vue'
47
+ import EvmSiwe from './EvmSiwe.vue'
48
+ import { useSiwe } from '../composables/siwe'
49
+
50
+ const props = defineProps<{
51
+ className?: string
52
+ getNonce: () => Promise<string>
53
+ verify: (message: string, signature: string) => Promise<boolean>
54
+ domain?: string
55
+ statement?: string
56
+ uri?: string
57
+ resources?: string[]
58
+ requestId?: string
59
+ expirationTime?: string
60
+ }>()
61
+
62
+ const emit = defineEmits<{
63
+ authenticated: [{ address: `0x${string}`; chainId: number }]
64
+ signedOut: []
65
+ error: [error: string]
66
+ }>()
67
+
68
+ const { isAuthenticated, session, signOut } = useSiwe()
69
+
70
+ const open = ref(false)
71
+ const siweRef = ref<InstanceType<typeof EvmSiwe> | null>(null)
72
+
73
+ const onAuthenticated = (data: { address: `0x${string}`; chainId: number }) => {
74
+ open.value = false
75
+ emit('authenticated', data)
76
+ }
77
+
78
+ const onClosed = () => {
79
+ siweRef.value?.reset()
80
+ }
81
+
82
+ const handleSignOut = () => {
83
+ signOut()
84
+ emit('signedOut')
85
+ }
86
+
87
+ watch(isAuthenticated, (authenticated) => {
88
+ if (!authenticated) {
89
+ open.value = false
90
+ siweRef.value?.reset()
91
+ }
92
+ })
93
+ </script>
@@ -0,0 +1,89 @@
1
+ import { ref, readonly } from 'vue'
2
+ import { signMessage } from '@wagmi/core'
3
+ import { useConfig, useConnection } from '@wagmi/vue'
4
+ import type { Config } from '@wagmi/vue'
5
+ import { createSiweMessage, type SiweMessageParams } from '../utils/siwe'
6
+
7
+ export interface SiweSession {
8
+ address: `0x${string}`
9
+ chainId: number
10
+ }
11
+
12
+ export interface UseSiweOptions {
13
+ getNonce: () => Promise<string>
14
+ verify: (message: string, signature: string) => Promise<boolean>
15
+ domain?: string
16
+ uri?: string
17
+ statement?: string
18
+ expirationTime?: string
19
+ requestId?: string
20
+ resources?: string[]
21
+ }
22
+
23
+ const _isAuthenticated = ref(false)
24
+ const _session = ref<SiweSession | null>(null)
25
+
26
+ export const useSiwe = () => {
27
+ const config = useConfig()
28
+ const { address, chainId } = useConnection()
29
+
30
+ const setSession = (session: SiweSession) => {
31
+ _isAuthenticated.value = true
32
+ _session.value = session
33
+ }
34
+
35
+ const clearSession = () => {
36
+ _isAuthenticated.value = false
37
+ _session.value = null
38
+ }
39
+
40
+ const signIn = async (options: UseSiweOptions) => {
41
+ const currentAddress = address.value
42
+ const currentChainId = chainId.value
43
+
44
+ if (!currentAddress || !currentChainId) {
45
+ throw new Error('Wallet not connected')
46
+ }
47
+
48
+ const nonce = await options.getNonce()
49
+
50
+ const messageParams: SiweMessageParams = {
51
+ domain: options.domain || window.location.host,
52
+ address: currentAddress,
53
+ uri: options.uri || window.location.origin,
54
+ chainId: currentChainId,
55
+ nonce,
56
+ statement: options.statement,
57
+ expirationTime: options.expirationTime,
58
+ requestId: options.requestId,
59
+ resources: options.resources,
60
+ }
61
+
62
+ const message = createSiweMessage(messageParams)
63
+ const signature = await signMessage(config as Config, { message })
64
+
65
+ const verified = await options.verify(message, signature)
66
+
67
+ if (!verified) {
68
+ throw new Error('Signature verification failed')
69
+ }
70
+
71
+ setSession({
72
+ address: currentAddress,
73
+ chainId: currentChainId,
74
+ })
75
+ }
76
+
77
+ const signOut = () => {
78
+ clearSession()
79
+ }
80
+
81
+ return {
82
+ isAuthenticated: readonly(_isAuthenticated),
83
+ session: readonly(_session),
84
+ signIn,
85
+ signOut,
86
+ setSession,
87
+ clearSession,
88
+ }
89
+ }
package/src/evm/index.ts CHANGED
@@ -18,6 +18,8 @@ export type { EnsProfile } from './utils/ens'
18
18
  export { stringifyJSON, parseJSON, formatPrice } from './utils/price'
19
19
  export { resolveUri } from './utils/uri'
20
20
  export type { ResolveUriOptions } from './utils/uri'
21
+ export { createSiweMessage } from './utils/siwe'
22
+ export type { SiweMessageParams } from './utils/siwe'
21
23
 
22
24
  // Composables
23
25
  export { useBaseURL } from './composables/base'
@@ -33,6 +35,8 @@ export { useGasPrice } from './composables/gasPrice'
33
35
  export { usePriceFeed } from './composables/priceFeed'
34
36
  export { useWalletExplorer } from './composables/walletExplorer'
35
37
  export type { ExplorerWallet } from './composables/walletExplorer'
38
+ export { useSiwe } from './composables/siwe'
39
+ export type { SiweSession, UseSiweOptions } from './composables/siwe'
36
40
 
37
41
  // Connectors
38
42
  export { inAppWallet, prepareInAppWallet } from './connectors/inAppWallet'
@@ -52,3 +56,5 @@ export { default as EvmWalletConnectWallets } from './components/EvmWalletConnec
52
56
  export { default as EvmTransactionFlow } from './components/EvmTransactionFlow.vue'
53
57
  export { default as EvmSeedPhraseInput } from './components/EvmSeedPhraseInput.vue'
54
58
  export { default as EvmInAppWalletSetup } from './components/EvmInAppWalletSetup.vue'
59
+ export { default as EvmSiwe } from './components/EvmSiwe.vue'
60
+ export { default as EvmSiweDialog } from './components/EvmSiweDialog.vue'
@@ -0,0 +1,70 @@
1
+ export interface SiweMessageParams {
2
+ domain: string
3
+ address: string
4
+ uri: string
5
+ version?: string
6
+ chainId: number
7
+ nonce: string
8
+ issuedAt?: string
9
+ expirationTime?: string
10
+ notBefore?: string
11
+ requestId?: string
12
+ statement?: string
13
+ resources?: string[]
14
+ }
15
+
16
+ export function createSiweMessage(params: SiweMessageParams): string {
17
+ const {
18
+ domain,
19
+ address,
20
+ uri,
21
+ version = '1',
22
+ chainId,
23
+ nonce,
24
+ issuedAt = new Date().toISOString(),
25
+ expirationTime,
26
+ notBefore,
27
+ requestId,
28
+ statement,
29
+ resources,
30
+ } = params
31
+
32
+ const lines: string[] = [
33
+ `${domain} wants you to sign in with your Ethereum account:`,
34
+ address,
35
+ '',
36
+ ]
37
+
38
+ if (statement) {
39
+ lines.push(statement, '')
40
+ }
41
+
42
+ lines.push(
43
+ `URI: ${uri}`,
44
+ `Version: ${version}`,
45
+ `Chain ID: ${chainId}`,
46
+ `Nonce: ${nonce}`,
47
+ `Issued At: ${issuedAt}`,
48
+ )
49
+
50
+ if (expirationTime) {
51
+ lines.push(`Expiration Time: ${expirationTime}`)
52
+ }
53
+
54
+ if (notBefore) {
55
+ lines.push(`Not Before: ${notBefore}`)
56
+ }
57
+
58
+ if (requestId) {
59
+ lines.push(`Request ID: ${requestId}`)
60
+ }
61
+
62
+ if (resources?.length) {
63
+ lines.push('Resources:')
64
+ for (const resource of resources) {
65
+ lines.push(`- ${resource}`)
66
+ }
67
+ }
68
+
69
+ return lines.join('\n')
70
+ }