diff --git a/.claude/agents/backend.md b/.claude/agents/backend.md new file mode 100644 index 0000000000000000000000000000000000000000..e294350a6331647aca7df37fa56e339938a1a800 --- /dev/null +++ b/.claude/agents/backend.md @@ -0,0 +1,46 @@ +--- +name: backend +description: | + Back-end specialist. Invoke ONLY for: + - server/api/ Nuxt server routes + - src/stores/ Pinia store architecture + - @hiveio/wax blockchain integration + - Google APIs (googleapis, google-auth-library) +tools: Read, Edit, Write, Glob, Grep +model: opus +color: blue +--- + +Senior back-end developer for wallet-dapp. + +## Stack + +- Nuxt server routes (server/api/) +- @hiveio/wax + @hiveio/beekeeper - Hive blockchain +- Wallet signers: MetaMask, Keychain, PeakVault +- googleapis+ google-auth-library + +## Key Packages + +- html5-qrcode / qrcode - QR handling +- jsonwebtoken - JWT tokens +- pinia - State management + +## Store Patterns + +- Define stores with `state`, `getters`, `actions` (not setup functions) +- Shallow refs for wallet instances +- Async actions with proper error handling +- `toastError(message, error)` for errors, never console.* +- Strongly typed interfaces for request/response +- Components never call APIs directly - use stores/services + +## API Structure + +- /server/api/auth/ - Authentication +- /server/api/google-drive/ - Drive sync +- /server/api/google-wallet/ - Wallet passes + +## Coordination + +For complex multi-agent tasks, synchronize through @pm agent. diff --git a/.claude/agents/devops.md b/.claude/agents/devops.md new file mode 100644 index 0000000000000000000000000000000000000000..d573fc6063ff3ff35d4d082d6db552eac7c7313c --- /dev/null +++ b/.claude/agents/devops.md @@ -0,0 +1,47 @@ +--- +name: devops +description: | + DevOps/CI specialist. Invoke ONLY for: + - .gitlab-ci.yml pipeline config + - Dockerfile, docker-compose + - Environment config (.env) + - Pipeline status/triggers +tools: Read, Edit, Write, Glob, Grep, Bash +model: opus +color: yellow +--- + +DevOps engineer for wallet-dapp (gitlab.syncad.com). + +## Pipeline + +Stages: .pre → build → deploy → cleanup + +Jobs: +1. lint - ESLint checks +2. build - Nuxt build (.output/) +3. build_app_image - Docker image +4. deploy_*_environment - Deploy to env + +## Environments + +| Env | URL | Port | +|-----|-----|------| +| Dev | auth.dev.openhive.network | 8133 | +| Prototyping | prototyping.openhive.network | 8155 | +| Production | auth.openhive.network | 9133 | + +## Key Files + +- .gitlab-ci.yml - Pipeline config +- Dockerfile - Node 22-slim multi-stage +- .env.example - Env template + +## Commands + +- /ci-status - Check pipeline status +- /ci-trigger - Trigger new pipeline + +## Coordination + +For complex multi-agent tasks, synchronize through @pm agent. diff --git a/.claude/agents/frontend.md b/.claude/agents/frontend.md new file mode 100644 index 0000000000000000000000000000000000000000..b214edef8a786469253a9f6117ede677976452c0 --- /dev/null +++ b/.claude/agents/frontend.md @@ -0,0 +1,45 @@ +--- +name: frontend +description: | + Vue 3/Nuxt 4 front-end specialist. Invoke ONLY for: + - src/pages/, src/components/, src/layouts/ + - Tailwind styling, Reka UI components + - src/composables/ composition functions + - UI/UX implementation tasks +tools: Read, Edit, Write, Glob, Grep +model: opus +color: red +--- + +Senior Vue 3/Nuxt 4 front-end developer for wallet-dapp. + +## Stack + +Nuxt 4.2 + Vue 3.5 + TypeScript + Tailwind 3 + Reka UI 2.0.2 + Lucide icons + +## Key Directories + +- src/pages/ - Route pages +- src/components/ - Vue components +- src/stores/ - Pinia stores +- src/composables/ - Composition functions +- src/utils/ - Helpers + +## Standards + +- ` +``` + +## Pinia Store Patterns + +Prefer `state`, `getters`, `actions` pattern: + +```typescript +export const useExampleStore = defineStore('example', { + state: () => ({ + isConnected: false, + data: null as DataType | null, + }), + getters: { + status: (state) => state.isConnected ? 'connected' : 'disconnected', + }, + actions: { + async connect() { + try { + // implementation + } catch (error) { + toastError('Connection failed', error); + throw error; + } + }, + }, +}); +``` + +For complex objects (wallet instances), use shallowRef in setup stores. + +## Tailwind CSS + +- Use utility classes only, avoid custom CSS +- Mobile-first: `text-sm md:text-base lg:text-lg` +- Dark mode: `bg-white dark:bg-gray-900` +- Use Reka UI primitives for accessible components + +## Error Handling + +- Never use `console.*` - use `toastError(message, error)` instead +- Always show loading states with Skeleton components +- Handle wallet disconnection gracefully +- Validate blockchain transaction results +- Defensive programming for external APIs/wallets diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..d2de00d9956edbfe5004ea7160bfebefe795e0b5 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "echo '---' && echo 'Branch:' $(git branch --show-current) && echo 'Status:' $(git status --short | wc -l) 'files changed' && echo 'Last commit:' $(git log -1 --oneline)" + } + ] + } + ] + } +} diff --git a/.env.dev b/.env.dev deleted file mode 100644 index 7f26fcf23208c07af1c23962ef9eb0ce61e72b61..0000000000000000000000000000000000000000 --- a/.env.dev +++ /dev/null @@ -1,3 +0,0 @@ -NUXT_PUBLIC_CTOKENS_API_URL=https://15.bc.fqdn.pl:10081 -NUXT_PUBLIC_HIVE_NODE_ENDPOINT=https://15.bc.fqdn.pl:18091 -NUXT_PUBLIC_HIVE_CHAIN_ID=42 diff --git a/.env.example b/.env.example index d3d79b44f640f7c42b9cf2fc3f3316b216b6a8b2..ef5a3857f10bf652697c8808ccc684fe0ec52b39 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,12 @@ NUXT_PUBLIC_SNAP_ORIGIN= ## Uncomment this if you want to show HTM in the menu # NUXT_PUBLIC_SHOW_HTM_IN_MENU=true +## L1 Proxy Configuration +## Set to false to disable L1 proxy functionality (app will work in pure L2 mode) +# NUXT_PUBLIC_ENABLE_L1_PROXY=true +## The Hive account used as proxy for L1 transactions +# NUXT_PUBLIC_HTM_PROXY_ACCOUNT=htm.proxy + # Custom Tokens API Configuration # Set this to your ctokens-api server URL # Example for local development: http://localhost:3000/ctokens-api diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000000000000000000000000000000000..ff3e288b323884fdf22dfbb073b29cd767254099 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,42 @@ +# Copilot Instructions for This Nuxt Repository + +## Code Structure +- Prefer imports from absolute paths using `@`: `@/components/...`, over relative paths. +- Break pages and components into small, reusable units whenever possible. + - **Max page size:** 400 LOC. + - Follow **Single Responsibility Principle** for all components. +- Avoid trailing spaces and maintain consistent formatting (use project ESLint/Prettier rules). + +## Error Handling & Logging +- **Never** use `console.*` for logging, debugging, or error reporting. +- Always use `toastError(message, error)` to surface errors to the user. + +## Loading States +- When components or pages depend on async data, include **Skeleton components** whenever possible. + - Skeletons should be lightweight, reusable, and reflect the expected layout. + +## Architecture & Data Flow +- **HTM/ctokens** is a separate domain and **not** directly tied to the wallet-dapp. Keep abstractions clean. +- Always use the **store** (Pinia/Vuex) and **mediating interfaces/services** for API communication. + - APIs may change - components should never call APIs directly. + - Prefer strongly typed interfaces for request/response modeling. + - Prefer defining Pinia stores as `state`, `getters`, and `actions` (avoid using `defineStore` with only setup functions). + +## UI & Styling +- Maintain consistent component naming and directory structure. Add directories as needed. +- Avoid excessive inline styles; prefer Tailwind CSS utility classes or component-scoped styles. +- If required, install Shadcn UI components via `pnpm dlx shadcn-vue@latest add ` and import them properly. + +## Project Documentation +- **Do not** create markdown files for documentation, architecture, reference, summary or instructions. +- Write self-documenting code; add comments only where intent is not obvious. + +## Code formatting +- Prefer arrow functions and concise syntax where applicable. + +## General Guidelines +- Prefer using `NuxtLink` for internal navigation over `` tags. External links should use `` with `target="_blank"` and `rel="noopener noreferrer"`. +- Do not repeat the code. If you notice that it is the same in a multiple files - extract the code into a separate file/component, implementing common functionality +- When using any component creating a link/navigation, ensure it has a class `keychainify-checked` to avoid hydration issues when keychain is installed. +- Prefer fallback-safe, defensive programming for external interactions (APIs, wallets, etc.). +- Keep dependencies minimal and review them regularly. diff --git a/.gitignore b/.gitignore index 05d1dea4766381c67caa521d2787009afa754ce1..3ccdc3f7837afe437143bba813f26f8f24520794 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ node_modules/ .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Claude Code local state +CLAUDE.md diff --git a/nuxt.config.ts b/nuxt.config.ts index b48a58bc5d49d8953c37349c32f1afd0e7acd891..979667cbd3853974a7a2d871f9c800fff125d966 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -53,6 +53,8 @@ export default defineNuxtConfig({ runtimeConfig: { public: { showHtmInMenu: false, + enableL1Proxy: true, + htmProxyAccount: 'htm.proxy', commitHash: getCommitHash(), ctokensApiUrl: 'https://htm.fqdn.pl:10081', hiveNodeEndpoint: 'https://api.hive.blog', diff --git a/package.json b/package.json index 8e1d3ee9338108bd32ea8a9aecf82307b5dfd041..ea34c6abc7a7a66147f3b5f57964d1fef5b08530 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "scripts": { "start": "nuxt dev", "dev": "nuxt dev", + "test": "echo \"TBA\"", "build": "nuxt build", "preview": "nuxt preview", "lint": "npm run lint-ci -- --fix", @@ -92,6 +93,7 @@ "dependencies": { "google-auth-library": "^10.4.2", "googleapis": "^140.0.0", + "html5-qrcode": "^2.3.8", "jsonwebtoken": "^9.0.2", "qrcode": "^1.5.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05602fc06f8f511e83be3de00496967752fbf6f5..33d3907ce2387cab5d6c3abc369d3fbcde60f3b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: googleapis: specifier: ^140.0.0 version: 140.0.1 + html5-qrcode: + specifier: ^2.3.8 + version: 2.3.8 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -3860,6 +3863,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html5-qrcode@2.3.8: + resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==} + http-assert@1.5.0: resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} engines: {node: '>= 0.8'} @@ -10360,6 +10366,8 @@ snapshots: hookable@5.5.3: {} + html5-qrcode@2.3.8: {} + http-assert@1.5.0: dependencies: deep-equal: 1.0.1 diff --git a/src/assets/css/tailwind.css b/src/assets/css/tailwind.css index 156e7f0f3ca36fe73d39808a0d14ca4afc6ef189..82ec9fb6d6a57061d6263ab8779fe7d171b6e179 100644 --- a/src/assets/css/tailwind.css +++ b/src/assets/css/tailwind.css @@ -78,7 +78,8 @@ * { @apply border-border; } + html, body { - @apply bg-background text-foreground; + @apply bg-background text-foreground overflow-x-hidden; } } diff --git a/src/components/QrScanner.vue b/src/components/QrScanner.vue new file mode 100644 index 0000000000000000000000000000000000000000..394214f44f76740fe8c08f2e5c0daa9b338d94c3 --- /dev/null +++ b/src/components/QrScanner.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/src/components/ReceiveTransferCard.vue b/src/components/ReceiveTransferCard.vue index 934bdf79b10c0d8044015e369596db3e9ab61bb5..464f71b0b5dd36680bb6b78024d3362207d0646e 100644 --- a/src/components/ReceiveTransferCard.vue +++ b/src/components/ReceiveTransferCard.vue @@ -2,23 +2,34 @@ import type { htm_operation } from '@mtyszczak-cargo/htm'; import { computed, ref, watch } from 'vue'; import { useRouter } from 'vue-router'; -import { toast } from 'vue-sonner'; import CollapsibleMemoInput from '@/components/CollapsibleMemoInput.vue'; import { TokenAmountInput } from '@/components/htm/amount'; +import QrScanner from '@/components/QrScanner.vue'; import ReceiverTokenSummary from '@/components/ReceiverTokenSummary.vue'; import TransferCompletedSummary from '@/components/TransferCompletedSummary.vue'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; import { useTokensStore, type CTokenDisplayBase } from '@/stores/tokens.store'; +import { useWalletStore } from '@/stores/wallet.store'; import { toastError } from '@/utils/parse-error'; import { waitForTransactionStatus } from '@/utils/transaction-status'; import CTokensProvider from '@/utils/wallet/ctokens/signer'; +import { TempCTokensSigner } from '@/utils/wallet/ctokens/temp-signer'; + +interface TransferSummary { + amount: string; + tokenLabel: string; + senderPublicKey: string; + receiver: string; + receiverPublicKey: string; + remainingBalance?: string; + timestamp?: string; +} interface Props { - receiverName?: string; receiverKey?: string; - receiverAvatar?: string; tokenData: CTokenDisplayBase | undefined; queryAmount?: string; queryMemo?: string; @@ -27,15 +38,47 @@ interface Props { const props = defineProps(); +const senderPublicKey = ref(''); + +const receiverName = ref(''); +const receiverAvatar = ref(undefined); + +const fetchUserMetadata = async () => { + try { + if (!props.receiverKey) return; + + const { name, profileImage } = await tokensStore.getUser(props.receiverKey); + receiverName.value = name || ''; + receiverAvatar.value = profileImage; + } catch (error) { + toastError('Failed to load receiver metadata', error); + } +}; + const emit = defineEmits<{ - transferCompleted: [summary: { amount: string; tokenLabel: string; receiver: string; remainingBalance?: string; timestamp?: string }]; + transferCompleted: [summary: TransferSummary]; }>(); -const router = useRouter(); const tokensStore = useTokensStore(); +const walletStore = useWalletStore(); const tokenComputed = computed(() => props.tokenData); -const amountComputed = computed(() => props.queryAmount ? props.tokenData ? (props.queryAmount.slice(0, -props.tokenData.precision) + '.' + props.queryAmount.slice(-props.tokenData.precision)) : '' : ''); +const amountComputed = computed(() => { + if (props.tokenData === undefined || props.queryAmount === undefined) + return ''; + + if (props.tokenData.precision === 0) + return props.queryAmount || ''; + + return ((props.queryAmount.slice(0, -props.tokenData.precision) || '0') + '.' + props.queryAmount.slice(-props.tokenData.precision)); +}); + +// QR code signing state +const showQrScanner = ref(false); +const scannedPrivateKey = ref(null); +const tempSigner = shallowRef(undefined); + +const router = useRouter(); // Form state const form = ref({ @@ -44,14 +87,39 @@ const form = ref({ memo: props.queryMemo || '' }); - const isSending = ref(false); const transferCompleted = ref(false); -const sentSummary = ref<{ amount: string; tokenLabel: string; receiver: string; remainingBalance?: string; timestamp?: string } | null>(null); +const sentSummary = ref(null); // Check if user is logged in const isLoggedIn = computed(() => !!tokensStore.wallet); +// Handle QR code scan +const handleQrScan = async (privateKey: string) => { + try { + scannedPrivateKey.value = privateKey; + + // Create a temporary signer from the scanned private key + tempSigner.value = await TempCTokensSigner.for(privateKey); + + showQrScanner.value = false; + } catch (error) { + toastError('Invalid private key from QR code', error); + scannedPrivateKey.value = null; + tempSigner.value = undefined; + } +}; + +// Clear scanned key +const clearScannedKey = () => { + scannedPrivateKey.value = null; + if (tempSigner.value) { + tempSigner.value.destroy(); + tempSigner.value = undefined; + } + senderPublicKey.value = tokensStore.getUserPublicKey() || ''; +}; + // TODO: Fix validation const isFormValid = computed(() => { @@ -66,11 +134,9 @@ const handleSend = async () => { return; } - if (!isLoggedIn.value || !CTokensProvider.getOperationalPublicKey()) { - toast.error('Please log in to your wallet first'); - router.push({ - path: '/tokens/list' - }); + // For non-logged-in users, require QR code + if (!isLoggedIn.value && !tempSigner.value) { + toastError('Please scan your private key QR code to sign the transaction', new Error('No signer available')); return; } @@ -82,7 +148,14 @@ const handleSend = async () => { try { isSending.value = true; - const sender = CTokensProvider.getOperationalPublicKey()!; + // Determine the sender's public key + const sender = tempSigner.value + ? tempSigner.value.publicKey + : tokensStore.getUserPublicKey(); + + if (!sender) + throw new Error('Could not determine sender public key'); + await waitForTransactionStatus( () => ([{ @@ -97,14 +170,21 @@ const handleSend = async () => { memo: form.value.memo } } satisfies htm_operation]), - 'Transfer' + 'Transfer', + true, + tempSigner.value // Use temp signer if available ); transferCompleted.value = true; - const balanceObj = await tokensStore.getBalanceSingleToken(sender, form.value.token.assetNum); + let remainingBalance = `0 ${form.value.token.symbol}`; + + try { + const balanceObj = await tokensStore.getBalanceSingleToken(sender, form.value.token.assetNum); + remainingBalance = balanceObj.displayBalance; + } catch {} - const receiverLabel = props.receiverName || props.receiverKey || 'Recipient'; + const receiverLabel = receiverName.value || props.receiverKey || 'Recipient'; const tokenLabel = form.value.token.symbol || form.value.token.name || String(form.value.token.assetNum) || ''; const ts = new Date().toISOString(); @@ -112,11 +192,16 @@ const handleSend = async () => { sentSummary.value = { amount: form.value.amount, tokenLabel, + senderPublicKey: senderPublicKey.value, receiver: receiverLabel, - remainingBalance: balanceObj.displayBalance, + receiverPublicKey: props.receiverKey!, + remainingBalance, timestamp: ts }; + // Clear scanned key after successful transfer + clearScannedKey(); + emit('transferCompleted', sentSummary.value); } catch (error) { toastError('Transfer failed', error); @@ -125,6 +210,14 @@ const handleSend = async () => { } }; +const conditionalLogin = async () => { + if (await CTokensProvider.hasWallet()) + walletStore.isProvideWalletPasswordModalOpen = true; + else + router.push({ path: '/tokens/register-account' }); + +}; + // Watch query params changes to update form watch(() => props.queryAmount, (newValue) => { if (newValue) @@ -140,6 +233,17 @@ watch(() => props.queryMemo, (newValue) => { if (newValue) form.value.memo = newValue; }); + +watch(isLoggedIn, (newValue) => { + const key = tokensStore.getUserPublicKey(); + senderPublicKey.value = newValue && key ? key : ''; +}); + +onMounted(() => { + fetchUserMetadata(); + + senderPublicKey.value = tokensStore.getUserPublicKey() || ''; +}); diff --git a/src/components/SendTransferCard.vue b/src/components/SendTransferCard.vue index c4747a231317ecdf009cc43cf75d6fb9ee9cb959..578e9d638b71ec0648e6e3f7aef52d8d8b177fbf 100644 --- a/src/components/SendTransferCard.vue +++ b/src/components/SendTransferCard.vue @@ -4,9 +4,7 @@ import { computed, ref, watch } from 'vue'; import CollapsibleMemoInput from '@/components/CollapsibleMemoInput.vue'; import { TokenAmountInput } from '@/components/htm/amount'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import type { CTokenDisplayBase } from '@/stores/tokens.store'; -import CTokensProvider from '@/utils/wallet/ctokens/signer'; - +import { useTokensStore, type CTokenDisplayBase } from '@/stores/tokens.store'; interface Props { token?: CTokenDisplayBase; @@ -22,6 +20,8 @@ const emit = defineEmits<{ updateMemo: [value: string]; }>(); +const tokensStore = useTokensStore(); + // Form state const form = ref({ amount: props.initialAmount || '', @@ -35,7 +35,7 @@ const selectedToken = computed({ } }); -const userOperationalKey = computed(() => CTokensProvider.getOperationalPublicKey()); +const userOperationalKey = computed(() => tokensStore.getUserPublicKey()); // Watch form changes to emit updates watch(() => form.value.amount, (newValue) => { @@ -57,11 +57,11 @@ watch(() => form.value.memo, (newValue) => { -
-
+
+
Receiver
You
-
+
{{ userOperationalKey }}
diff --git a/src/components/TransferCompletedSummary.vue b/src/components/TransferCompletedSummary.vue index c01d31017d00c9ef1380584e2015ca99d9bc6ca3..33f68f52a96eed6edbd5d416bfc91f9c4f85bb69 100644 --- a/src/components/TransferCompletedSummary.vue +++ b/src/components/TransferCompletedSummary.vue @@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import CTokensProvider from '@/utils/wallet/ctokens/signer'; +import { useTokensStore } from '@/stores/tokens.store'; interface Props { amount: string; @@ -23,10 +23,14 @@ const props = defineProps(); const router = useRouter(); +const tokensStore = useTokensStore(); + const generateInvoice = () => { + const pk = tokensStore.getUserPublicKey(); + const params = new URLSearchParams({ - fromPk: props.isReceiveMode ? CTokensProvider.getOperationalPublicKey() || '' : CTokensProvider.getOperationalPublicKey() || '', - toPk: props.isReceiveMode ? props.receiverKey || '' : CTokensProvider.getOperationalPublicKey() || '', + fromPk: pk || '', + toPk: props.isReceiveMode ? props.receiverKey || '' : pk || '', 'asset-num': props.assetNum.toString(), amount: props.amount }); diff --git a/src/components/htm/HTMLoginContent.vue b/src/components/htm/HTMLoginContent.vue index 72fdb0edf7cad121e9a88922c103783b2903ac76..a012044dc840cfe36e9af304a30fa3cd16d0e578 100644 --- a/src/components/htm/HTMLoginContent.vue +++ b/src/components/htm/HTMLoginContent.vue @@ -1,10 +1,12 @@ + + diff --git a/src/components/htm/HTMProvidePassword.vue b/src/components/htm/HTMProvidePassword.vue index 2587f2eb82ad0c33d283edad6ec9962586b7ec44..9bffffd23f90fbbc904c3cd68e618038e6e32975 100644 --- a/src/components/htm/HTMProvidePassword.vue +++ b/src/components/htm/HTMProvidePassword.vue @@ -22,7 +22,7 @@ const close = (ignoreLogIn = false) => {