fix: connect AI funding scan to the wallet scanner

This commit is contained in:
archipelago
2026-10-08 12:47:40 -04:00
parent 82367dc1d5
commit 0ff95251f5
3 changed files with 26 additions and 3 deletions
+12
View File
@@ -385,3 +385,15 @@ change the allowance, send a payment, start inference, or publish content. API
keys use the existing private node credential store and are not passed to AIUI. keys use the existing private node credential store and are not passed to AIUI.
Focused provider/publishing tests and production qualification are in progress; Focused provider/publishing tests and production qualification are in progress;
these changes are not yet deployed or publicly released. these changes are not yet deployed or publicly released.
Provider-focused validation: 20 dashboard/setup/signer tests, 22 AIUI provider
and generation tests, and 22 trusted bridge/integration tests pass. Initial
publishing tests required an AI-connection component stub for their isolated
mounts; the provider default test now checks initial state before the suite's
explicit Claude selection. The full backend suite and production builds remain
pending. Framework still runs the preceding candidate; its management service is
active and no Ollama container exists after cleanup.
The funding modal's Scan action now opens the existing wallet scanner and returns
to funding on close; six focused connection-modal tests pass after that wiring.
The first dashboard production build passed; it will be rebuilt for this final
scanner wiring before deployment. AIUI and isolated backend builds are ongoing.
@@ -28,7 +28,8 @@
<button v-if="status?.local_ready" class="glass-button rounded-lg px-4 py-2 text-sm text-white mt-4 w-full" @click="useLocal">Use local AI</button> <button v-if="status?.local_ready" class="glass-button rounded-lg px-4 py-2 text-sm text-white mt-4 w-full" @click="useLocal">Use local AI</button>
<p class="text-xs text-white/40 mt-4">Your draft stays in place when you close this window.</p> <p class="text-xs text-white/40 mt-4">Your draft stays in place when you close this window.</p>
</BaseModal> </BaseModal>
<ReceiveBitcoinModal :show="show && funding" initial-method="ecash" @close="finishFunding" @received="finishFunding" /> <ReceiveBitcoinModal :show="show && funding && !scanning" initial-method="ecash" @close="finishFunding" @received="finishFunding" @scan="scanning = true" />
<WalletScanModal :show="show && scanning" @close="finishScan" @sent="finishScan" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -36,6 +37,7 @@ import { computed, ref, watch } from 'vue'
import { rpcClient } from '@/api/rpc-client' import { rpcClient } from '@/api/rpc-client'
import BaseModal from './BaseModal.vue' import BaseModal from './BaseModal.vue'
import ReceiveBitcoinModal from './ReceiveBitcoinModal.vue' import ReceiveBitcoinModal from './ReceiveBitcoinModal.vue'
import WalletScanModal from './WalletScanModal.vue'
import RoutstrBudgetSection from '@/views/settings/RoutstrBudgetSection.vue' import RoutstrBudgetSection from '@/views/settings/RoutstrBudgetSection.vue'
type Choice = 'claude' | 'openai' | 'routstr' type Choice = 'claude' | 'openai' | 'routstr'
@@ -59,6 +61,7 @@ const error = ref('')
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
const funding = ref(false) const funding = ref(false)
const scanning = ref(false)
const configured = computed(() => mode.value === 'claude' ? status.value?.claude_configured : status.value?.openai_configured) const configured = computed(() => mode.value === 'claude' ? status.value?.claude_configured : status.value?.openai_configured)
let refreshPromise: Promise<ProviderStatus | null> | null = null let refreshPromise: Promise<ProviderStatus | null> | null = null
async function refresh(): Promise<ProviderStatus | null> { async function refresh(): Promise<ProviderStatus | null> {
@@ -96,7 +99,7 @@ async function refreshBalance() {
catch { balance.value = null } catch { balance.value = null }
} }
function choose(choice: Choice) { key.value = ''; error.value = ''; mode.value = choice; if (choice === 'routstr') void refreshBalance() } function choose(choice: Choice) { key.value = ''; error.value = ''; mode.value = choice; if (choice === 'routstr') void refreshBalance() }
function close() { key.value = ''; funding.value = false; emit('close') } function close() { key.value = ''; funding.value = false; scanning.value = false; emit('close') }
async function save() { async function save() {
if ((mode.value !== 'claude' && mode.value !== 'openai') || saving.value) return if ((mode.value !== 'claude' && mode.value !== 'openai') || saving.value) return
const provider = mode.value const provider = mode.value
@@ -128,8 +131,9 @@ async function continueRoutstr() {
emit('configured', 'routstr'); close() emit('configured', 'routstr'); close()
} catch { error.value = 'Could not select Routstr. Try again.' } } catch { error.value = 'Could not select Routstr. Try again.' }
} }
async function finishScan() { scanning.value = false; await refreshBalance() }
async function finishFunding() { funding.value = false; await refreshBalance() } async function finishFunding() { funding.value = false; await refreshBalance() }
watch(() => props.show, open => { if (open) { error.value = ''; void refresh(); if (mode.value === 'routstr') void refreshBalance() } else { key.value = ''; funding.value = false } }) watch(() => props.show, open => { if (open) { error.value = ''; void refresh(); if (mode.value === 'routstr') void refreshBalance() } else { key.value = ''; funding.value = false; scanning.value = false } })
async function syncSelection() { const state = await refresh(); if (state) emit('configured', state.settings.provider, state.settings.provider === 'openai' ? state.settings.openai_model : undefined) } async function syncSelection() { const state = await refresh(); if (state) emit('configured', state.settings.provider, state.settings.provider === 'openai' ? state.settings.openai_model : undefined) }
defineExpose({ checkNeeded, syncSelection, showRoutstr: () => choose('routstr') }) defineExpose({ checkNeeded, syncSelection, showRoutstr: () => choose('routstr') })
</script> </script>
@@ -2,8 +2,10 @@ import { mount, flushPromises } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import AIConnectionModal from '../AIConnectionModal.vue' import AIConnectionModal from '../AIConnectionModal.vue'
import ReceiveBitcoinModal from '../ReceiveBitcoinModal.vue' import ReceiveBitcoinModal from '../ReceiveBitcoinModal.vue'
import WalletScanModal from '../WalletScanModal.vue'
import { rpcClient } from '@/api/rpc-client' import { rpcClient } from '@/api/rpc-client'
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } })) vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
vi.mock('../WalletScanModal.vue', () => ({ default: { props: ['show'], template: '<div />' } }))
vi.mock('../ReceiveBitcoinModal.vue', () => ({ default: { props: ['show', 'initialMethod'], template: '<div />' } })) vi.mock('../ReceiveBitcoinModal.vue', () => ({ default: { props: ['show', 'initialMethod'], template: '<div />' } }))
vi.mock('@/views/settings/RoutstrBudgetSection.vue', () => ({ default: { template: '<div />' } })) vi.mock('@/views/settings/RoutstrBudgetSection.vue', () => ({ default: { template: '<div />' } }))
const state = () => ({ schema: 1, settings: { provider: 'auto', openai_model: '' }, claude_configured: false, openai_configured: false, local_ready: false, routstr_remaining_sats: 0 }) const state = () => ({ schema: 1, settings: { provider: 'auto', openai_model: '' }, claude_configured: false, openai_configured: false, local_ready: false, routstr_remaining_sats: 0 })
@@ -18,6 +20,11 @@ describe('AI connection setup', () => {
expect(vi.mocked(rpcClient.call).mock.calls.every(([r]) => !['assistant.budget-set', 'system.settings.set'].includes(r.method))).toBe(true) expect(vi.mocked(rpcClient.call).mock.calls.every(([r]) => !['assistant.budget-set', 'system.settings.set'].includes(r.method))).toBe(true)
await button(w, 'Top up with ecash').trigger('click') await button(w, 'Top up with ecash').trigger('click')
expect(w.findComponent(ReceiveBitcoinModal).props()).toMatchObject({ show: true, initialMethod: 'ecash' }) expect(w.findComponent(ReceiveBitcoinModal).props()).toMatchObject({ show: true, initialMethod: 'ecash' })
w.findComponent(ReceiveBitcoinModal).vm.$emit('scan'); await flushPromises()
expect(w.findComponent(WalletScanModal).props('show')).toBe(true)
expect(w.findComponent(ReceiveBitcoinModal).props('show')).toBe(false)
w.findComponent(WalletScanModal).vm.$emit('close'); await flushPromises()
expect(w.findComponent(ReceiveBitcoinModal).props('show')).toBe(true)
w.unmount() w.unmount()
}) })