feat(wallet): paste-send confirmation step + every-time scan/upload chooser
- Pasting an invoice/address now gets the same second-step confirmation the scan flow has: method, destination, live balance -> amount -> balance after (invoice-fixed amounts win over the typed one), for every rail including token minting. Nothing pays until Confirm & Send. - The scan modal opens on a chooser every time: scan with camera or upload/take a photo of the QR — the camera no longer auto-starts, and Back only relights it if camera was the user's choice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c58f84c6e9
commit
6502f13e29
@@ -43,7 +43,22 @@
|
||||
<Transition :name="direction === 'forward' ? 'pane-forward' : 'pane-back'" mode="out-in">
|
||||
<!-- ============ SCAN PANE ============ -->
|
||||
<div v-if="pane === 'scan'" key="scan">
|
||||
<div class="relative w-full aspect-square rounded-xl overflow-hidden bg-black/40 border border-white/10 mb-4">
|
||||
<!-- Chooser interstitial — every open: the user picks live camera
|
||||
or a photo upload; neither starts until chosen. -->
|
||||
<div v-if="scanChoice === 'unset'" class="w-full rounded-xl bg-black/30 border border-white/10 mb-4 p-6 flex flex-col items-center gap-3">
|
||||
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8V6a2 2 0 012-2h2M3 16v2a2 2 0 002 2h2m10-16h2a2 2 0 012 2v2m-4 12h2a2 2 0 002-2v-2M7 12h10" />
|
||||
</svg>
|
||||
<p class="text-sm text-white/60 text-center">How do you want to read the QR?</p>
|
||||
<button v-if="!liveCameraUnavailable" @click="chooseCamera" class="glass-button w-full px-4 py-2.5 rounded-lg text-sm font-medium">
|
||||
Scan with camera
|
||||
</button>
|
||||
<button @click="photoInput?.click()" class="glass-button w-full px-4 py-2.5 rounded-lg text-sm font-medium">
|
||||
Upload / take a photo of the QR
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="relative w-full aspect-square rounded-xl overflow-hidden bg-black/40 border border-white/10 mb-4">
|
||||
<!-- opacity (not v-if/v-show): the scanner needs the element, and a
|
||||
source-less <video> flashes a native play glyph in Android WebViews -->
|
||||
<video ref="videoElement" class="w-full h-full object-cover transition-opacity duration-200" :class="isScanning ? 'opacity-100' : 'opacity-0'" autoplay muted playsinline></video>
|
||||
@@ -73,17 +88,20 @@
|
||||
<button @click="photoInput?.click()" class="glass-button px-4 py-2 rounded-lg text-sm font-medium">
|
||||
Take photo of QR
|
||||
</button>
|
||||
<input
|
||||
ref="photoInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
class="hidden"
|
||||
@change="onPhotoPicked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single always-mounted picker input, shared by the chooser and
|
||||
the in-camera fallback button -->
|
||||
<input
|
||||
ref="photoInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
class="hidden"
|
||||
@change="onPhotoPicked"
|
||||
/>
|
||||
|
||||
<div class="mb-4 p-3 bg-white/5 rounded-lg min-h-[3rem] flex items-center justify-center">
|
||||
<p class="text-sm text-center" :class="scanStatusIsError ? 'text-red-400' : 'text-white/60'">
|
||||
{{ scanStatus || 'Point the camera at a Lightning invoice, Bitcoin address, Cashu or Fedimint code' }}
|
||||
@@ -282,7 +300,9 @@ function goBack() {
|
||||
if (pane.value === 'amount') {
|
||||
error.value = ''
|
||||
goTo('scan', 'back')
|
||||
nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
|
||||
// Only relight the camera when that's what the user chose; otherwise
|
||||
// they land back on the scan/upload chooser.
|
||||
nextTick(() => { if (scanChoice.value === 'camera' && !liveCameraUnavailable.value) startScanning() })
|
||||
} else if (pane.value === 'success') {
|
||||
close()
|
||||
}
|
||||
@@ -295,6 +315,16 @@ const isScanning = ref(false)
|
||||
// open) — the fallback buttons stay hidden until it resolves, so they don't
|
||||
// flash for a second on every open.
|
||||
const autoStarting = ref(false)
|
||||
|
||||
// Camera-vs-photo chooser, shown on EVERY open (user request 2026-07-23):
|
||||
// nothing starts until the user picks, and Back from a later pane returns to
|
||||
// the live camera only if that's what they chose.
|
||||
const scanChoice = ref<'unset' | 'camera'>('unset')
|
||||
|
||||
function chooseCamera() {
|
||||
scanChoice.value = 'camera'
|
||||
void nextTick(() => startScanning())
|
||||
}
|
||||
const scanStatus = ref('')
|
||||
const scanStatusIsError = ref(false)
|
||||
const cameraError = ref(false)
|
||||
@@ -709,7 +739,8 @@ function close() {
|
||||
watch(() => props.show, (open) => {
|
||||
if (open) {
|
||||
resetAll()
|
||||
nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
|
||||
// No auto-start: the chooser interstitial owns the first move every time.
|
||||
scanChoice.value = 'unset'
|
||||
} else {
|
||||
stopScanning()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user