@meddleware/access-gate-ui 0.1.1 → 0.1.4

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": "@meddleware/access-gate-ui",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Operator console for creating and managing on-chain Sui access gates — create gates, configure settings, airdrop NFTs, and freeze gates via a wallet-connected SPA.",
5
5
  "author": "Meddleware <dev@meddleware.co.uk>",
6
6
  "license": "0BSD",
@@ -9,6 +9,17 @@
9
9
  "url": "git+https://github.com/meddleware-org/access-gate-ui.git"
10
10
  },
11
11
  "type": "module",
12
+ "sideEffects": [
13
+ "*.css",
14
+ "*.vue"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
12
23
  "files": [
13
24
  "src",
14
25
  "index.html",
@@ -28,9 +39,10 @@
28
39
  "dependencies": {
29
40
  "@meddleware/design-tokens": "^0.1.2",
30
41
  "@meddleware/ui": "^0.1.6",
31
- "@meddleware/nft-gate-client": "^0.0.5",
32
- "@mysten/sui": "~2.17.0",
33
- "@mysten/wallet-standard": "~0.19.9",
42
+ "@meddleware/nft-gate-client": "^0.0.6",
43
+ "@meddleware/wallet-adapter": "^0.0.3",
44
+ "@mysten/sui": "^2.28.0",
45
+ "@mysten/wallet-standard": "^0.20.0",
34
46
  "vue": "^3.5.40"
35
47
  },
36
48
  "devDependencies": {
package/src/App.vue CHANGED
@@ -1,56 +1,22 @@
1
1
  <script setup lang="ts">
2
- import { ref } from 'vue'
3
- import { AppHeader, AppFooter, ColorModeControl, UiButton, UiNotice, useColorMode } from '@meddleware/ui'
4
- import type { OwnedGate } from '@meddleware/nft-gate-client'
5
- import { NETWORK } from './config.js'
6
- import { PACKAGE_ID, listMyGates } from './gates.js'
2
+ // Standalone shell for the Access Gate SPA: app header + footer wrapping the core tool view.
3
+ // The core UI lives in AccessGateView.vue (also exported for inline embedding in the dashboard).
4
+ import { AppHeader, AppFooter, ColorModeControl, UiButton, useColorMode } from '@meddleware/ui'
7
5
  import { useWallet } from './wallet.js'
8
- import CreateGateForm from './components/CreateGateForm.vue'
9
- import GateList from './components/GateList.vue'
6
+ import AccessGateView from './components/AccessGateView.vue'
10
7
 
11
- const isEmbedded = new URLSearchParams(window.location.search).has('embedded')
12
8
  const { mode, set } = useColorMode('dark')
13
-
14
- type Tab = 'create' | 'gates'
15
- const activeTab = ref<Tab>('gates')
16
-
17
9
  const { wallets, account, connect, disconnect } = useWallet()
18
10
 
19
- const gates = ref<OwnedGate[]>([])
20
- const loadingGates = ref(false)
21
- const loadError = ref<string | null>(null)
22
-
23
- const deployed = PACKAGE_ID !== ''
24
-
25
- async function reloadGates(): Promise<void> {
26
- if (!account.value || !deployed) return
27
- loadingGates.value = true
28
- loadError.value = null
29
- try {
30
- gates.value = await listMyGates(account.value.address)
31
- } catch (e) {
32
- loadError.value = e instanceof Error ? e.message : String(e)
33
- } finally {
34
- loadingGates.value = false
35
- }
36
- }
37
-
38
- async function onConnectFirst(): Promise<void> {
11
+ async function onConnect(): Promise<void> {
39
12
  const w = wallets.value[0]
40
- if (!w) return
41
- await connect(w)
42
- await reloadGates()
43
- }
44
-
45
- function onCreated(): void {
46
- activeTab.value = 'gates'
47
- void reloadGates()
13
+ if (w) await connect(w)
48
14
  }
49
15
  </script>
50
16
 
51
17
  <template>
52
- <div class="app" :class="{ 'app--embedded': isEmbedded }">
53
- <AppHeader v-if="!isEmbedded" variant="dark">
18
+ <div class="app">
19
+ <AppHeader variant="dark">
54
20
  <template #brand>
55
21
  <span>Access Gate</span>
56
22
  </template>
@@ -60,7 +26,7 @@ function onCreated(): void {
60
26
  <UiButton variant="ghost" @click="disconnect">Disconnect</UiButton>
61
27
  </template>
62
28
  <template v-else>
63
- <UiButton :disabled="!wallets.length" @click="onConnectFirst">
29
+ <UiButton :disabled="!wallets.length" @click="onConnect">
64
30
  {{ wallets.length ? 'Connect wallet' : 'No wallet detected' }}
65
31
  </UiButton>
66
32
  </template>
@@ -68,39 +34,9 @@ function onCreated(): void {
68
34
  </template>
69
35
  </AppHeader>
70
36
 
71
- <div class="page">
72
- <p class="sub">Create and manage on-chain access gates on Sui ({{ NETWORK }}).</p>
37
+ <AccessGateView />
73
38
 
74
- <UiNotice v-if="!deployed" type="error">
75
- The access_gate contract is not deployed on {{ NETWORK }}. Switch to a supported network.
76
- </UiNotice>
77
-
78
- <template v-else-if="account">
79
- <nav class="tabs" aria-label="Sections">
80
- <button type="button" class="tab" :class="{ active: activeTab === 'gates' }" @click="activeTab = 'gates'; reloadGates()">
81
- My gates
82
- </button>
83
- <button type="button" class="tab" :class="{ active: activeTab === 'create' }" @click="activeTab = 'create'">
84
- Create gate
85
- </button>
86
- </nav>
87
-
88
- <UiNotice v-if="loadError" type="error">{{ loadError }}</UiNotice>
89
-
90
- <section v-if="activeTab === 'gates'">
91
- <GateList :gates="gates" :loading="loadingGates" @changed="reloadGates" />
92
- </section>
93
- <section v-else>
94
- <CreateGateForm :address="account.address" @created="onCreated" />
95
- </section>
96
- </template>
97
-
98
- <UiNotice v-else type="info">
99
- Connect a Sui wallet to create and manage your access gates.
100
- </UiNotice>
101
- </div>
102
-
103
- <AppFooter v-if="!isEmbedded" />
39
+ <AppFooter />
104
40
  </div>
105
41
  </template>
106
42
 
@@ -111,53 +47,8 @@ function onCreated(): void {
111
47
  flex-direction: column;
112
48
  }
113
49
 
114
- .app--embedded {
115
- min-height: 100%;
116
- }
117
-
118
- .page {
119
- max-width: 720px;
120
- margin: 0 auto;
121
- padding: 2rem 1.25rem 4rem;
122
- flex: 1;
123
- }
124
-
125
- .app--embedded .page {
126
- padding-top: 1rem;
127
- padding-bottom: 1rem;
128
- }
129
-
130
- .sub {
131
- color: var(--muted);
132
- margin: 0.25rem 0 1.25rem;
133
- }
134
-
135
50
  .addr {
136
51
  font-family: monospace;
137
52
  font-size: 0.9rem;
138
53
  }
139
-
140
- .tabs {
141
- display: flex;
142
- gap: 0.25rem;
143
- margin: 0 0 1rem;
144
- border-bottom: 2px solid var(--border);
145
- }
146
-
147
- .tab {
148
- background: none;
149
- border: none;
150
- border-bottom: 2px solid transparent;
151
- padding: 0.5rem 1rem;
152
- margin-bottom: -2px;
153
- cursor: pointer;
154
- font-size: 0.95rem;
155
- color: var(--muted);
156
- }
157
-
158
- .tab.active {
159
- border-bottom-color: var(--accent);
160
- color: var(--text);
161
- font-weight: 600;
162
- }
163
54
  </style>
@@ -0,0 +1,126 @@
1
+ <script setup lang="ts">
2
+ // Core Access Gate tool UI (list + create + manage gates), free of any app shell.
3
+ // Rendered standalone by access-gate-ui's App.vue and inline by the dashboard. Wallet state
4
+ // comes from the shared @meddleware/wallet-adapter singleton (via ./wallet.js): connecting here
5
+ // or in any other inline tool view (or the dashboard header) reflects everywhere.
6
+ //
7
+ // Gate loading is driven by watching the connected account, so it works whether the connection
8
+ // is made from this app's own header (standalone) or the dashboard's shared control (embedded).
9
+ import { ref, watch } from 'vue'
10
+ import { UiNotice } from '@meddleware/ui'
11
+ import { WalletGuard } from '@meddleware/wallet-adapter'
12
+ import type { OwnedGate } from '@meddleware/nft-gate-client'
13
+ import { NETWORK } from '../config.js'
14
+ import { PACKAGE_ID, listMyGates } from '../gates.js'
15
+ import { useWallet } from '../wallet.js'
16
+ import CreateGateForm from './CreateGateForm.vue'
17
+ import GateList from './GateList.vue'
18
+
19
+ type Tab = 'create' | 'gates'
20
+ const activeTab = ref<Tab>('gates')
21
+
22
+ const { account } = useWallet()
23
+
24
+ const gates = ref<OwnedGate[]>([])
25
+ const loadingGates = ref(false)
26
+ const loadError = ref<string | null>(null)
27
+
28
+ const deployed = PACKAGE_ID !== ''
29
+
30
+ async function reloadGates(): Promise<void> {
31
+ if (!account.value || !deployed) return
32
+ loadingGates.value = true
33
+ loadError.value = null
34
+ try {
35
+ gates.value = await listMyGates(account.value.address)
36
+ } catch (e) {
37
+ loadError.value = e instanceof Error ? e.message : String(e)
38
+ } finally {
39
+ loadingGates.value = false
40
+ }
41
+ }
42
+
43
+ // Load (and clear) gates as the shared wallet connects/disconnects — independent of where the
44
+ // connect action originates.
45
+ watch(
46
+ () => account.value?.address ?? null,
47
+ (addr) => {
48
+ if (addr) void reloadGates()
49
+ else gates.value = []
50
+ },
51
+ { immediate: true },
52
+ )
53
+
54
+ function onCreated(): void {
55
+ activeTab.value = 'gates'
56
+ void reloadGates()
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div class="page">
62
+ <p class="sub">Create and manage on-chain access gates on Sui ({{ NETWORK }}).</p>
63
+
64
+ <UiNotice v-if="!deployed" type="error">
65
+ The access_gate contract is not deployed on {{ NETWORK }}. Switch to a supported network.
66
+ </UiNotice>
67
+
68
+ <WalletGuard v-else message="Connect a Sui wallet to create and manage your access gates.">
69
+ <nav class="tabs" aria-label="Sections">
70
+ <button type="button" class="tab" :class="{ active: activeTab === 'gates' }" @click="activeTab = 'gates'; reloadGates()">
71
+ My gates
72
+ </button>
73
+ <button type="button" class="tab" :class="{ active: activeTab === 'create' }" @click="activeTab = 'create'">
74
+ Create gate
75
+ </button>
76
+ </nav>
77
+
78
+ <UiNotice v-if="loadError" type="error">{{ loadError }}</UiNotice>
79
+
80
+ <section v-if="activeTab === 'gates'">
81
+ <GateList :gates="gates" :loading="loadingGates" @changed="reloadGates" />
82
+ </section>
83
+ <section v-else>
84
+ <CreateGateForm :address="account!.address" @created="onCreated" />
85
+ </section>
86
+ </WalletGuard>
87
+ </div>
88
+ </template>
89
+
90
+ <style scoped>
91
+ .page {
92
+ max-width: 720px;
93
+ margin: 0 auto;
94
+ padding: 2rem 1.25rem 4rem;
95
+ flex: 1;
96
+ }
97
+
98
+ .sub {
99
+ color: var(--muted);
100
+ margin: 0.25rem 0 1.25rem;
101
+ }
102
+
103
+ .tabs {
104
+ display: flex;
105
+ gap: 0.25rem;
106
+ margin: 0 0 1rem;
107
+ border-bottom: 2px solid var(--border);
108
+ }
109
+
110
+ .tab {
111
+ background: none;
112
+ border: none;
113
+ border-bottom: 2px solid transparent;
114
+ padding: 0.5rem 1rem;
115
+ margin-bottom: -2px;
116
+ cursor: pointer;
117
+ font-size: 0.95rem;
118
+ color: var(--muted);
119
+ }
120
+
121
+ .tab.active {
122
+ border-bottom-color: var(--accent);
123
+ color: var(--text);
124
+ font-weight: 600;
125
+ }
126
+ </style>
package/src/config.ts CHANGED
@@ -11,11 +11,11 @@ const env = (import.meta as unknown as { env?: Record<string, string | undefined
11
11
  export const NETWORK: SuiNetwork = (env.VITE_NETWORK as SuiNetwork) || 'testnet'
12
12
 
13
13
  /**
14
- * JSON-RPC endpoint used to build + execute gate transactions and read gate state.
15
- * NOTE: this is JSON-RPC the public testnet fullnode serves gRPC only, so a JSON-RPC-capable
16
- * endpoint is used for testnet by default.
14
+ * gRPC-web endpoint used to build + execute gate transactions and read gate state. The Sui SDK's
15
+ * JSON-RPC client is deprecated, so this must be a gRPC-web-capable endpoint (the Mysten public
16
+ * fullnodes serve gRPC-web at :443 via the browser Fetch transport).
17
17
  */
18
18
  export const RPC_URLS: Record<SuiNetwork, string> = {
19
- testnet: env.VITE_RPC_TESTNET || 'https://sui-testnet-rpc.publicnode.com',
19
+ testnet: env.VITE_RPC_TESTNET || 'https://fullnode.testnet.sui.io:443',
20
20
  mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
21
21
  }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ // @meddleware/access-gate-ui — library entry.
2
+ //
3
+ // Exports the core Access Gate tool view (no app shell) for inline embedding in the dashboard.
4
+ // The standalone SPA (App.vue + main.ts) is unaffected and still builds/deploys as before.
5
+ //
6
+ // Consumers must import the shared design tokens + UI base once at their entry:
7
+ // import '@meddleware/design-tokens/tokens.css'
8
+ // import '@meddleware/ui/base.css'
9
+
10
+ export { default as AccessGateView } from './components/AccessGateView.vue'
package/src/wallet.ts CHANGED
@@ -1,138 +1,45 @@
1
- // Minimal Sui wallet integration on @mysten/wallet-standard (dapp-kit is React-only).
2
- // Discovers wallets, connects, and adapts the connected wallet into a transaction executor.
3
- // Module singleton all callers share one wallet/account state.
4
- import { markRaw, readonly, ref, shallowRef } from 'vue'
5
- import { getWallets, isWalletWithRequiredFeatureSet } from '@mysten/wallet-standard'
6
- import type { Wallet, WalletAccount } from '@mysten/wallet-standard'
7
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
8
- import type { Transaction } from '@mysten/sui/transactions'
1
+ // Thin access-gate-ui shim over the shared @meddleware/wallet-adapter singleton.
2
+ //
3
+ // The adapter is network-agnostic (RPC URL passed per call); this shim binds access-gate-ui's
4
+ // RPC_URLS so call sites keep the one-arg ergonomics (`getSuiClient(NETWORK)` /
5
+ // `buildExecutor(NETWORK)`). Because the adapter is a module singleton, the wallet connection is
6
+ // shared with any other tool view rendered in the same window (e.g. the dashboard).
7
+ import {
8
+ useWallet as useWalletBase,
9
+ getSuiClient as getSuiClientBase,
10
+ buildExecutor as buildExecutorBase,
11
+ } from '@meddleware/wallet-adapter'
12
+ import type { Executor } from '@meddleware/wallet-adapter'
9
13
  import type { SuiNetwork } from './config.js'
10
14
  import { RPC_URLS } from './config.js'
11
15
 
12
- const REQUIRED_FEATURES = ['standard:connect', 'sui:signTransaction'] as const
16
+ export type { Executor }
13
17
 
14
- const wallets = shallowRef<Wallet[]>([])
15
- const currentWallet = shallowRef<Wallet | null>(null)
16
- const account = shallowRef<WalletAccount | null>(null)
17
- const connecting = ref(false)
18
- const error = ref<string | null>(null)
19
-
20
- const clients = new Map<SuiNetwork, SuiJsonRpcClient>()
21
- /** Return a memoised {@link SuiJsonRpcClient} for the network (one instance per network). */
22
- export function getSuiClient(network: SuiNetwork): SuiJsonRpcClient {
23
- let c = clients.get(network)
24
- if (!c) {
25
- c = new SuiJsonRpcClient({ url: RPC_URLS[network], network })
26
- clients.set(network, c)
27
- }
28
- return c
29
- }
30
-
31
- function refreshWallets(): void {
32
- // markRaw: extension Wallet objects expose name/icon as ES-private-field getters
33
- // that throw when accessed through a Vue reactive Proxy.
34
- wallets.value = getWallets()
35
- .get()
36
- .filter((w) => isWalletWithRequiredFeatureSet(w, [...REQUIRED_FEATURES]))
37
- .map((w) => markRaw(w))
38
- }
39
-
40
- let initialised = false
41
- function init(): void {
42
- if (initialised) return
43
- initialised = true
44
- const api = getWallets()
45
- refreshWallets()
46
- api.on('register', refreshWallets)
47
- api.on('unregister', refreshWallets)
48
- }
49
-
50
- async function connect(wallet: Wallet): Promise<void> {
51
- error.value = null
52
- connecting.value = true
53
- try {
54
- const feature = wallet.features['standard:connect'] as {
55
- connect: () => Promise<{ accounts: readonly WalletAccount[] }>
56
- }
57
- const { accounts } = await feature.connect()
58
- if (!accounts.length) throw new Error('Wallet returned no accounts.')
59
- currentWallet.value = markRaw(wallet)
60
- account.value = markRaw(accounts[0])
61
- } catch (e) {
62
- error.value = e instanceof Error ? e.message : String(e)
63
- throw e
64
- } finally {
65
- connecting.value = false
66
- }
18
+ /** Memoised Sui JSON-RPC client for the network, using access-gate-ui's configured RPC URL. */
19
+ export function getSuiClient(network: SuiNetwork) {
20
+ return getSuiClientBase(network, RPC_URLS[network])
67
21
  }
68
22
 
69
- function disconnect(): void {
70
- const disc = currentWallet.value?.features['standard:disconnect'] as
71
- | { disconnect?: () => Promise<void> }
72
- | undefined
73
- void disc?.disconnect?.()
74
- currentWallet.value = null
75
- account.value = null
76
- }
77
-
78
- /** Transaction executor bound to the connected wallet: sign+execute a PTB and await finality. */
79
- export interface Executor {
80
- address: string
81
- signAndExecute(tx: Transaction): Promise<{ digest: string }>
82
- waitForTransaction(digest: string): Promise<unknown>
83
- }
84
-
85
- /** Build an executor bound to the connected wallet + network. */
86
- export async function buildExecutor(network: SuiNetwork): Promise<Executor> {
87
- const wallet = currentWallet.value
88
- const acct = account.value
89
- if (!wallet || !acct) throw new Error('Connect a wallet first.')
90
- const client = getSuiClient(network)
91
- const chain = `sui:${network}` as const
92
-
93
- const signFeature = wallet.features['sui:signTransaction'] as {
94
- signTransaction: (input: {
95
- transaction: Transaction
96
- account: WalletAccount
97
- chain: `sui:${string}`
98
- }) => Promise<{ bytes: string; signature: string }>
99
- }
100
-
101
- return {
102
- address: acct.address,
103
- async signAndExecute(tx: Transaction): Promise<{ digest: string }> {
104
- const { bytes, signature } = await signFeature.signTransaction({
105
- transaction: tx,
106
- account: acct,
107
- chain,
108
- })
109
- const res = await client.executeTransactionBlock({
110
- transactionBlock: bytes,
111
- signature,
112
- options: { showEffects: true },
113
- })
114
- return { digest: res.digest }
115
- },
116
- async waitForTransaction(digest: string): Promise<unknown> {
117
- return client.waitForTransaction({ digest })
118
- },
119
- }
23
+ /** Build a transaction executor bound to the connected wallet + access-gate-ui's RPC URL. */
24
+ export function buildExecutor(network: SuiNetwork): Promise<Executor> {
25
+ return buildExecutorBase(network, RPC_URLS[network])
120
26
  }
121
27
 
122
28
  /**
123
- * Wallet composable: discovers wallets, exposes reactive connection state, and provides
124
- * `connect` / `disconnect` / `buildExecutor`. Module singleton all callers share one state.
29
+ * Wallet composable bound to access-gate-ui's network config. Delegates to the shared adapter
30
+ * singleton; gate management needs `sui:signTransaction` so it's requested for discovery.
125
31
  */
126
32
  export function useWallet() {
127
- init()
33
+ const base = useWalletBase({ requiredFeatures: ['sui:signTransaction'] })
128
34
  return {
129
- wallets: readonly(wallets),
130
- currentWallet: readonly(currentWallet),
131
- account: readonly(account),
132
- connecting: readonly(connecting),
133
- error: readonly(error),
134
- connect,
135
- disconnect,
35
+ wallets: base.wallets,
36
+ currentWallet: base.currentWallet,
37
+ account: base.account,
38
+ connecting: base.connecting,
39
+ error: base.error,
40
+ connect: base.connect,
41
+ disconnect: base.disconnect,
42
+ getSuiClient,
136
43
  buildExecutor,
137
44
  }
138
45
  }