@@ -21,7 +21,7 @@ import { Label } from '../../uielements/label'
2121import { QRCode } from '../../uielements/qrCode/QRCode'
2222
2323type VaultType = 'fast' | 'secure'
24- type Phase = 'password' | 'waiting-qr' | 'qr-ready' | 'device-joined' | 'signing' | 'error'
24+ type Phase = 'checking' | ' password' | 'waiting-qr' | 'qr-ready' | 'device-joined' | 'signing' | 'error'
2525
2626type Props = {
2727 visible : boolean
@@ -89,26 +89,51 @@ export const VultisigConfirmationModal = ({
8989
9090 // Reset state when modal opens
9191 useEffect ( ( ) => {
92- if ( visible ) {
93- setPassword ( '' )
94- setPasswordError ( null )
95- setQrPayload ( null )
96- setDevicesJoined ( 0 )
97- setIsValidating ( false )
98- setIsCancelling ( false )
99- setTxErrorMsg ( null )
100- signingStartedRef . current = false
101- closedRef . current = false
102-
103- if ( ! isEncrypted ) {
104- // No password needed — skip straight to signing
105- // Secure: modal stays open for QR/device flow
106- // Fast: parent closes modal immediately via onSuccess callback
107- setPhase ( vaultType === 'secure' ? 'waiting-qr' : 'signing' )
108- onSuccess ( )
92+ if ( ! visible ) return
93+
94+ setPassword ( '' )
95+ setPasswordError ( null )
96+ setQrPayload ( null )
97+ setDevicesJoined ( 0 )
98+ setIsValidating ( false )
99+ setIsCancelling ( false )
100+ setTxErrorMsg ( null )
101+ signingStartedRef . current = false
102+ closedRef . current = false
103+
104+ // Skip straight to signing without asking for the password.
105+ // Secure: modal stays open for the QR/device flow. Fast: parent closes the
106+ // modal immediately via onSuccess.
107+ const skipToSigning = ( ) => {
108+ setPhase ( vaultType === 'secure' ? 'waiting-qr' : 'signing' )
109+ onSuccess ( )
110+ }
111+
112+ if ( ! isEncrypted ) {
113+ // Un-encrypted vault — no password ever needed.
114+ skipToSigning ( )
115+ return
116+ }
117+
118+ // Encrypted vault: only prompt when the SDK's password cache has actually
119+ // expired (idle > TTL). While it's still cached, re-entering the password is
120+ // redundant; skipping it also avoids the "Password required" cache-miss that
121+ // occurs when signing after the app has sat idle. `phase` alone can't tell us
122+ // this — only the SDK knows the live cache state.
123+ setPhase ( 'checking' )
124+ let cancelled = false
125+ const vaultId = getActiveVaultId ( )
126+ ; ( async ( ) => {
127+ const stillUnlocked = vaultId ? await window . apiMpc . isVaultUnlocked ( vaultId ) . catch ( ( ) => false ) : false
128+ if ( cancelled ) return
129+ if ( stillUnlocked ) {
130+ skipToSigning ( )
109131 } else {
110132 setPhase ( 'password' )
111133 }
134+ } ) ( )
135+ return ( ) => {
136+ cancelled = true
112137 }
113138 } , [ visible ] ) // eslint-disable-line react-hooks/exhaustive-deps
114139
@@ -220,8 +245,8 @@ export const VultisigConfirmationModal = ({
220245 const [ isCancelling , setIsCancelling ] = useState ( false )
221246
222247 const handleCancel = useCallback ( async ( ) => {
223- if ( phase === 'password' || phase === 'error' ) {
224- // Password phase or failed tx - nothing to abort, just close
248+ if ( phase === 'checking' || phase === ' password' || phase === 'error' ) {
249+ // Cache check, password phase, or failed tx - nothing to abort, just close
225250 onCancel ?.( )
226251 onClose ( )
227252 return
@@ -264,6 +289,15 @@ export const VultisigConfirmationModal = ({
264289
265290 // Render content based on phase
266291 const renderContent = ( ) => {
292+ if ( phase === 'checking' ) {
293+ // Brief state while we ask the SDK whether the password is still cached.
294+ return (
295+ < div className = "flex flex-col items-center gap-4" >
296+ < div className = "h-12 w-12 animate-spin rounded-full border-4 border-turquoise border-t-transparent" />
297+ </ div >
298+ )
299+ }
300+
267301 if ( phase === 'password' ) {
268302 return (
269303 < div className = "flex flex-col items-center gap-4" >
@@ -357,7 +391,9 @@ export const VultisigConfirmationModal = ({
357391 // The handleCancel callback controls when closing is allowed
358392 < Dialog static as = "div" className = "relative z-10" open = { visible } onClose = { handleCancel } >
359393 < DialogBackdrop className = "fixed inset-0 bg-bg0/40 dark:bg-bg0d/40" />
360- < div className = "fixed inset-0 flex items-center justify-center p-4" >
394+ { /* `lg:pl-[240px]` offsets the sidebar so the panel centers within the
395+ content area, matching `UnifiedTxModal` (the tx-tracking modal). */ }
396+ < div className = "fixed inset-0 flex items-center justify-center p-4 lg:pl-[240px]" >
361397 < DialogPanel
362398 className = { clsx (
363399 'mx-auto flex flex-col items-center p-6' ,
0 commit comments