Repository navigation
fix(native): restore wallet controls and fast private receive #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
39522ee
fix(native): repair wallet controls and unified startup
Kukks e05b6aa
fix(native): clear failed Lightning handles
Kukks 5a89bfd
fix(native): cancel address activation on wallet close
Kukks 45ed0cd
fix(native): harden wallet controls and shutdown
Kukks 6dab9b1
fix(native): bound blocked wallet reset
Kukks e8896a1
fix(native): repair wallet controls and receive startup
Kukks 4959969
fix(native): finish wallet recovery lifecycle
Kukks 55ad0c9
fix(native): cancel late receive activation timer
Kukks 97baf36
fix(native): finish secure wallet reset
Kukks File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import {useEffect,useRef,useState} from 'react'; | ||
| import './native-about.css'; | ||
|
|
||
| export default function NativeAbout({onClose,onClearWallet}:{onClose:()=>void;onClearWallet?:()=>Promise<void>}){ | ||
| const close=useRef<HTMLButtonElement>(null),[confirmClear,setConfirmClear]=useState(false),[clearing,setClearing]=useState(false),[clearError,setClearError]=useState(''); | ||
| const clear=async()=>{ | ||
| if(!onClearWallet||clearing)return; | ||
| setClearing(true);setClearError(''); | ||
| try{await onClearWallet();} | ||
| catch(error){setClearError(error instanceof Error?error.message:String(error));setClearing(false);} | ||
| }; | ||
| useEffect(()=>{ | ||
| close.current?.focus(); | ||
| const previous=document.body.style.overflow,key=(event:KeyboardEvent)=>{ | ||
| if(event.key==='Escape'&&!clearing)onClose(); | ||
| if(event.key==='Tab'){ | ||
| const buttons=[...document.querySelectorAll<HTMLButtonElement>('.native-about button')]; | ||
| if(!buttons.length)return; | ||
| const first=buttons[0]!,last=buttons.at(-1)!; | ||
| if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();} | ||
| else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();} | ||
| } | ||
| }; | ||
| document.body.style.overflow='hidden';addEventListener('keydown',key); | ||
| return()=>{document.body.style.overflow=previous;removeEventListener('keydown',key);}; | ||
| },[onClose,clearing]); | ||
| return <div className="native-about-layer" onMouseDown={event=>{if(event.target===event.currentTarget&&!clearing)onClose();}}> | ||
| <section className="native-about" role="dialog" aria-modal="true" aria-labelledby="native-about-title"> | ||
| <span>About</span> | ||
| <h2 id="native-about-title">Your keys stay with you.</h2> | ||
| <p>Shielded is a private Bitcoin wallet on Arkade. Payments inside the pool keep their amount, sender and recipient private. Deposits and withdrawals are public.</p> | ||
| <p>This Mutinynet build is for testing with test funds only. Wallet traffic uses Tor by default, and Tor can be turned off. Repeated identifiers and timing can still reveal links.</p> | ||
| {onClearWallet&&<section className="native-about-wallet" aria-labelledby="native-about-wallet-title"> | ||
| <span>Wallet controls</span> | ||
| <h3 id="native-about-wallet-title">Clear this wallet</h3> | ||
| <p>Remove the local wallet, recovery data and vault secret from this device. Keep your recovery phrase first.</p> | ||
| {!confirmClear?<button type="button" className="native-about-clear" onClick={()=>setConfirmClear(true)}>Clear wallet…</button>:<div className="native-about-confirm" role="alert"> | ||
| <p>Pending Lightning swaps may still need this device's saved recovery records. Clear anyway?</p> | ||
| <div><button type="button" onClick={()=>setConfirmClear(false)} disabled={clearing}>Keep wallet</button><button type="button" className="native-about-danger" onClick={()=>void clear()} disabled={clearing}>{clearing?'Clearing wallet…':'Clear permanently'}</button></div> | ||
| </div>} | ||
| {clearError&&<p className="native-about-error" role="alert">{clearError}</p>} | ||
| </section>} | ||
| <button ref={close} type="button" onClick={onClose} disabled={clearing}>Close</button> | ||
| </section> | ||
| </div>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,62 @@ | ||
| import {useEffect,useId,useLayoutEffect,useRef,useState,type CSSProperties} from 'react'; | ||
| import {createPortal} from 'react-dom'; | ||
| import type {NativeTorHealthState} from './native-tor-monitor.ts'; | ||
| import './native-tor-health.css'; | ||
|
|
||
| export const initialTorHealth:NativeTorHealthState={status:{phase:'idle',generation:0},check:'idle'}; | ||
|
|
||
| export function torPanelPosition(rect:{left:number;top:number;bottom:number},viewport:{width:number;height:number},tone:'light'|'dark'):CSSProperties{ | ||
| const edge=16,gap=10,width=Math.min(320,Math.max(0,viewport.width-edge*2)),left=Math.max(edge,Math.min(rect.left,viewport.width-width-edge)),below=Math.max(0,viewport.height-rect.bottom-gap-edge),above=Math.max(0,rect.top-gap-edge); | ||
| const openAbove=tone==='dark'?above>=160||above>=below:above>=below; | ||
| return openAbove?{left,width,bottom:Math.max(edge,viewport.height-rect.top+gap),maxHeight:above}:{left,width,top:rect.bottom+gap,maxHeight:below}; | ||
| } | ||
|
|
||
| export default function NativeTorHealth({health=initialTorHealth,tone='light',onToggleTor,transportBusy=false}:{health?:NativeTorHealthState;tone?:'light'|'dark';onToggleTor?:()=>void;transportBusy?:boolean}){ | ||
| const [open,setOpen]=useState(false),[position,setPosition]=useState<CSSProperties>({}),host=useRef<HTMLDivElement>(null),panel=useRef<HTMLDivElement>(null),trigger=useRef<HTMLButtonElement>(null),panelId=useId(); | ||
| const {status,check,checkedAt,latencyMs}=health; | ||
| const direct=(status.mode??'tor')==='direct'; | ||
| const ready=status.phase==='ready',connected=ready&&check==='ok'; | ||
| const label=direct?status.phase==='error'||check==='failed'?'Direct connection failed · Tor off':status.phase==='paused'?'Direct paused':ready?(connected?'Tor off · Direct connection':check==='checking'?'Checking direct connection':'Direct ready'):'Connecting directly':status.phase==='paused'?'Tor paused':status.phase==='error'?'Tor unavailable':status.phase==='bootstrapping'?'Connecting to Tor':ready?(connected?'Tor connected':check==='failed'?'Tor check failed':check==='checking'?'Checking Tor':'Tor ready'):'Starting Tor'; | ||
| const state=direct&&status.phase==='error'||ready&&check==='failed'?'error':direct?'direct':connected?'connected':status.phase==='error'||ready&&check==='failed'?'error':status.phase==='paused'?'paused':'connecting'; | ||
| const progress=!direct&&status.phase==='bootstrapping'&&status.progress&&/^\d{1,3}%$/.test(status.progress)?status.progress:undefined; | ||
| const detail=direct?(status.phase==='paused'?'The direct connection is paused. Unlock the wallet to reconnect.':status.phase==='error'?(status.error||'The direct connection is unavailable. Reconnect to try again.'):ready&&check==='failed'?'The latest direct connection check failed. Tor remains off.':connected?'A recent direct connection to the wallet service succeeded.':ready?'Checking the direct connection to the wallet service.':'Connecting directly to the wallet services.'):status.phase==='paused'?'The private connection is paused. Unlock the wallet to reconnect.':status.phase==='error'?(status.error||'Tor could not start. Reconnect to try again.'):ready&&check==='failed'?'The latest connection check through Tor failed. Your wallet never falls back to a direct connection.':connected?'A recent connection to the wallet service succeeded through Tor.':ready?'Tor is ready. Checking the connection to the wallet service.':'Tor starts automatically and keeps wallet traffic private while it connects.'; | ||
| return <details className="native-tor-health" data-tone={tone} data-health={state}> | ||
| <summary aria-label={`Wallet connection: ${label}`}><span className="native-tor-dot" aria-hidden="true"/><span role="status" aria-live="polite">{label}{progress&&<span className="native-tor-percent"> {progress}</span>}</span><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="m5 6 3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg></summary> | ||
| <div className="native-tor-panel"><strong>{label}</strong><p>{detail}</p> | ||
| {progress&&<progress max={100} value={Math.min(100,parseInt(progress,10))} aria-label="Tor bootstrap progress"/>} | ||
| {ready&&checkedAt&&<p className="native-tor-checked">Last checked {new Date(checkedAt).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit',second:'2-digit'})}{latencyMs!==undefined?` · ${(latencyMs/1000).toFixed(1)}s`:''}</p>} | ||
| {ready&&check==='failed'&&health.error&&<p className="native-tor-problem">{health.error}</p>} | ||
| {onToggleTor&&<button className="native-tor-switch" type="button" role="switch" aria-checked={!direct} disabled={transportBusy} onClick={()=>{if(!transportBusy)onToggleTor();}}><span>Use Tor</span><span className="native-tor-switch-track" aria-hidden="true"/></button>} | ||
| <p className="native-tor-privacy">{direct?'Your IP address and DNS lookups are exposed. Direct connections have no Tor circuit isolation.':'Separate Tor scopes protect unrelated wallet items. Repeated identifiers and timing can still reveal links. Wallet services may be unavailable even when Tor is connected.'}</p> | ||
| </div> | ||
| </details>; | ||
| const place=()=>{ | ||
| const rect=trigger.current?.getBoundingClientRect(); | ||
| if(!rect)return {}; | ||
| return torPanelPosition(rect,{width:innerWidth,height:innerHeight},tone); | ||
| }; | ||
| const toggle=()=>{ | ||
| if(!open)setPosition(place()); | ||
| setOpen(value=>!value); | ||
| }; | ||
| useLayoutEffect(()=>{ | ||
| if(!open)return; | ||
| const update=()=>setPosition(place()); | ||
| update();addEventListener('resize',update);addEventListener('scroll',update,true); | ||
| return()=>{removeEventListener('resize',update);removeEventListener('scroll',update,true);}; | ||
| },[open,tone]); | ||
| useEffect(()=>{ | ||
| if(!open)return; | ||
| const outside=(event:PointerEvent)=>{const target=event.target as Node;if(!host.current?.contains(target)&&!panel.current?.contains(target))setOpen(false);},key=(event:KeyboardEvent)=>{if(event.key==='Escape'){setOpen(false);trigger.current?.focus();}}; | ||
| addEventListener('pointerdown',outside);addEventListener('keydown',key); | ||
| return()=>{removeEventListener('pointerdown',outside);removeEventListener('keydown',key);}; | ||
| },[open]); | ||
| useEffect(()=>{ | ||
| if(typeof document==='undefined')return; | ||
| const root=document.documentElement,sync=()=>{if(root.classList.contains('native-sheet-open'))setOpen(false);}; | ||
| sync();const observer=new MutationObserver(sync);observer.observe(root,{attributes:true,attributeFilter:['class']}); | ||
| return()=>observer.disconnect(); | ||
| },[]); | ||
| const body=<><strong>{label}</strong><p>{detail}</p> | ||
| {progress&&<progress max={100} value={Math.min(100,parseInt(progress,10))} aria-label="Tor bootstrap progress"/>} | ||
| {ready&&checkedAt&&<p className="native-tor-checked">Last checked {new Date(checkedAt).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit',second:'2-digit'})}{latencyMs!==undefined?` · ${(latencyMs/1000).toFixed(1)}s`:''}</p>} | ||
| {ready&&check==='failed'&&health.error&&<p className="native-tor-problem">{health.error}</p>} | ||
| {onToggleTor&&<button className="native-tor-switch" type="button" role="switch" aria-checked={!direct} disabled={transportBusy} onClick={()=>{if(!transportBusy){setOpen(false);onToggleTor();requestAnimationFrame(()=>trigger.current?.focus());}}}><span>Use Tor</span><span className="native-tor-switch-track" aria-hidden="true"/></button>} | ||
| <p className="native-tor-privacy">{direct?'Your IP address and DNS lookups are exposed. Direct connections have no Tor circuit isolation.':'Separate Tor scopes protect unrelated wallet items. Repeated identifiers and timing can still reveal links. Wallet services may be unavailable even when Tor is connected.'}</p> | ||
| </>; | ||
| const content=<div ref={panel} className="native-tor-panel" id={panelId} role="region" aria-label="Connection details" data-tone={tone} style={position}>{body}</div>; | ||
| return <><div ref={host} className="native-tor-health" data-tone={tone} data-health={state}> | ||
| <button ref={trigger} type="button" className="native-tor-trigger" aria-label={`Wallet connection: ${label}`} aria-expanded={open} aria-controls={panelId} onClick={toggle}><span className="native-tor-dot" aria-hidden="true"/><span role="status" aria-live="polite">{label}{progress&&<span className="native-tor-percent"> {progress}</span>}</span><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="m5 6 3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg></button> | ||
| {typeof document==='undefined'?<div className="native-tor-panel" id={panelId} role="region" aria-label="Connection details" data-tone={tone} hidden>{body}</div>:!open?<div id={panelId} hidden/>:null} | ||
| </div>{open&&typeof document!=='undefined'?createPortal(content,document.body):null}</>; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.