v0.4.8: seven UX fixes for the BAL inheritance plugin

- #04: Raw/Date selector now reappears on the WILL/HEIR tabs when switching to
  ADVANCED (and when a raw value is pushed from the wizard), keeping the current
  value/editor. New BalTimeEditWidget.apply_user_type_visibility() wired into
  WillSettingsWidget for both the locktime and check-alive boxes.
- #3: Building Will report window minimum height 500 -> 450 px.
- #5: 'User Type' setting moved to the bottom, above 'Rebroadcast transactions'.
- #6: enabling ADVANCED now requires typing 'at My Risk' (case-insensitive);
  wrong phrase or cancel reverts to BASIC. QInputDialog added to common import.
- #7a: on CHECK with an emptied wallet, an extra reassuring line is shown on
  'Checking your will': 'Inheritance already executed (on blockchain)' in GREEN
  (CONFIRMED) or 'Inheritance in mempool (waiting confirmation)' in ORANGE
  (MEMPOOL). New _executed_inheritance_status() helper.
- #7b: 'Balance is too low, or CheckAlive is in the past. Skipped' (space added)
  recoloured to ORANGE instead of red.
- Reset button renamed 'Reset setting' -> 'Reset to Default Setting'.
- Added tests/test_group_h_v048.py (8 tests). Version 0.4.7 -> 0.4.8.
  Full suite: 266 passed; ruff clean; CHANGELOG #24; memory updated (#04 done).
This commit is contained in:
2026-06-28 23:02:40 -04:00
parent 0f2b3a4146
commit 33e8a7533d
11 changed files with 425 additions and 38 deletions

View File

@@ -355,6 +355,41 @@ the label) would also fix TASK #03. Keep them linked: solving "unify invalidate"
classic window (PROCEDURE 1) resolves #03 automatically. If implemented separately, the minimal fix is to classic window (PROCEDURE 1) resolves #03 automatically. If implemented separately, the minimal fix is to
add set_label("BAL Invalidate transaction") in the PROCEDURE 2 broadcast path. add set_label("BAL Invalidate transaction") in the PROCEDURE 2 broadcast path.
### TASK #04 — Raw/Date selector combo not shown on the WILL/HEIR tabs (only inside the wizard)
**User (translated from Italian):** "when I select Advanced in the plugin settings, the Raw mode of the
locktime and check-alive boxes only appears inside the wizard, and no longer on the Will and Heir tabs,
even if I tick the 'panel editable fee and date' setting. Inside the wizard it works perfectly; outside the
wizard only the DATE works." Plus: "if I set Raw inside the wizard for check-alive/locktime, then on the
Will/Heir tabs the raw number appears, but NOT the box to choose Raw or Date."
**ANALYSIS (already done, confirmed by reading the code):**
- Each date/locktime field is `BalTimeEditWidget` (widgets.py). Its `__init__` reads `is_basic_mode()` ONCE
(line ~293) and, in BASIC, hides the Raw/Date combo (line ~336 `self.combo.setVisible(False)`).
- The WILL/HEIR tab toolbars are created ONCE and REUSED for the whole session (not rebuilt on USER TYPE
change), so the combo stays in its initial (hidden) state.
- `apply_user_type_visibility()` (widgets.py ~888), called from `BalWindow.update_all()` (window.py ~1343)
on a BASIC<->ADVANCED switch, ONLY toggles the Check-Alive ROW visibility (`threshold.setVisible(not basic)`,
line ~919). It NEVER touches `self.combo.setVisible(...)` nor the editor.
- Raw value showing but combo hidden: when the wizard saves a raw value, `update_setting_widgets` ->
`update_widget_value` -> `set_value(raw)` writes the value into the active editor (so the raw NUMBER shows),
and `update_combo_setting_widgets` -> `set_index` sets the combo INDEX to Raw "behind the scenes" — but the
combo is still `setVisible(False)`, so the selector box stays invisible.
- The wizard works only because it is RECREATED on every open (re-reads the current mode).
**ROOT CAUSE (unified):** the Raw/Date combo on the toolbars is hidden permanently at `__init__` and nothing
ever makes it visible again, neither on ADVANCED switch nor when a raw value arrives.
**PROPOSED FIX (to detail in PLAN):** add a method to `BalTimeEditWidget` (e.g. `apply_user_type_visibility()`)
that re-reads `is_basic_mode()` and does `self.combo.setVisible(not basic)` WITHOUT changing the current value
or editor (per user req #1: keep current value to avoid confusion between date and inheritance). Call it from
`WillSettingsWidget.apply_user_type_visibility()` for BOTH `locktime` AND `threshold` (per user req #2: applies
to both boxes). So toggling BASIC<->ADVANCED, and a raw value pushed from the wizard, both correctly reveal the
combo on the already-existing WILL/HEIR toolbars without restarting Electrum.
**USER CONFIRMATIONS:** (1) on ADVANCED, show the combo but KEEP the current value/editor (do not reset) to
avoid confusing date vs inheritance; (2) applies to BOTH locktime AND check-alive boxes.
### TASK #04 STATUS: DONE in v0.4.8 (user approved, implemented this session).
### Fix: added BalTimeEditWidget.apply_user_type_visibility() (shows/hides the Raw/Date
### combo, keeps current value/editor) and wired it into WillSettingsWidget.apply_user_type_visibility()
### for BOTH locktime and threshold. Delivered together with the other v0.4.8 UX changes.
### DO NOT ACT on #01/#02/#03. Saved to to-do list only. Wait for user to add more or to choose one; ### DO NOT ACT on #01/#02/#03. Saved to to-do list only. Wait for user to add more or to choose one;
### then DISCOVER -> PLAN -> wait OK (R4) -> zip-first. ### then DISCOVER -> PLAN -> wait OK (R4) -> zip-first.

View File

@@ -1424,3 +1424,73 @@ confirms the ZIP works).
**Outcome:** DONE (delivered as test ZIP v0.4.7; commit only after the user **Outcome:** DONE (delivered as test ZIP v0.4.7; commit only after the user
confirms the ZIP works). confirms the ZIP works).
---
## 24. v0.4.8 - Raw/Date selector on WILL/HEIR tabs, shorter report window, USER TYPE moved + "at My Risk" gate, executed/mempool inheritance note, "balance too low" recoloured, Reset button renamed
**Date:** 2026-06-25
**Goal (owner requests, this session):** seven small, independent UX fixes,
delivered together in one version.
**What changed:**
- **#04 - Raw/Date selector now reappears on the WILL/HEIR tabs.**
`bal/gui/qt/widgets.py`
- Added `BalTimeEditWidget.apply_user_type_visibility()`: re-reads
`is_basic_mode()` and shows/hides ONLY the Raw/Date combo, WITHOUT changing
the current value or active editor (owner request: keep the value to avoid
confusing the delivery date with the inheritance).
- `WillSettingsWidget.apply_user_type_visibility()` now also calls the new
method on BOTH the `locktime` and `threshold` boxes. The reused toolbars used
to hide the combo forever after construction; now switching to ADVANCED (or a
raw value pushed from the wizard) reveals it without restarting Electrum.
- **#3 - Building Will report window shorter.**
`bal/gui/qt/dialogs.py`: report scroll area minimum height `500 -> 450` px.
- **#5 - "User Type" moved to the bottom of the settings.**
`bal/gui/qt/plugin.py`: the "User Type" row moved from the first grid row to
the bottom, just above "Rebroadcast transactions"; the other rows were
renumbered up by one.
- **#6 - "at My Risk" gate before enabling ADVANCED.**
`bal/gui/qt/plugin.py` (`on_user_type_change`): selecting ADVANCED now prompts
"Type 'at My Risk' to enable ADVANCED mode"; the phrase is accepted
case-insensitively. A wrong phrase or a cancel reverts to BASIC.
`bal/gui/qt/common.py`: `QInputDialog` added to the shared Qt import.
- **#7a - Reassuring note when the inheritance was already executed.**
`bal/gui/qt/dialogs.py`: added `_executed_inheritance_status()` (reads the
will items' CONFIRMED / MEMPOOL flags, CONFIRMED wins). In the
`NotCompleteWillException` branch of `task_phase1`, when the wallet is empty
because the inheritance went through, an extra line is shown on the "Checking
your will" row: "Inheritance already executed (on blockchain)" in GREEN
(CONFIRMED) or "Inheritance in mempool (waiting confirmation)" in ORANGE
(MEMPOOL). The original "changes" message is still shown afterwards.
- **#7b - "balance too low" message recoloured.**
`bal/gui/qt/dialogs.py`: the text is now
"Balance is too low, or CheckAlive is in the past. Skipped" (a space added
before "Skipped") and rendered in ORANGE (`COLOR_WARNING`) instead of red,
since an empty wallet after execution is normal, not an error.
- **Reset button renamed.**
`bal/gui/qt/plugin.py`: "Reset setting" -> "Reset to Default Setting".
- Added `tests/test_group_h_v048.py` with 8 GUI-free tests pinning the
executed-inheritance detection rule (CONFIRMED > MEMPOOL > None) and the
"at My Risk" case-insensitive gate.
- Version bumped 0.4.7 -> 0.4.8 (`plugin_base.py`, `__init__.py`, `VERSION`,
`manifest.json`).
**Verification:**
- `py_compile`: `widgets.py`, `dialogs.py`, `plugin.py`, `common.py` compile OK.
- Full test suite: `266 passed` (258 previous + 8 new v0.4.8 tests).
- `ruff`: no new errors (only pre-existing star-import / F841 noise).
**Outcome:** DONE (delivered as test ZIP v0.4.8; commit only after the user
confirms the ZIP works).

View File

@@ -1 +1 @@
0.4.7 0.4.8

View File

@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
``json_db.register_dict``) and PyQt6. ``json_db.register_dict``) and PyQt6.
""" """
__version__ = "0.4.7" __version__ = "0.4.8"

View File

@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
""" """
_version = None _version = None
__version__ = "0.4.7" # AUTOMATICALLY GENERATED DO NOT EDIT __version__ = "0.4.8" # AUTOMATICALLY GENERATED DO NOT EDIT
# Command used to open an .ics calendar file, per operating system. # Command used to open an .ics calendar file, per operating system.
default_app = { default_app = {

View File

@@ -55,9 +55,9 @@ from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem,
QStandardItemModel) QStandardItemModel)
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox, from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout, QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QTextEdit, QMenu, QMenuBar, QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu,
QPushButton, QScrollArea, QSizePolicy, QSpinBox, QMenuBar, QPushButton, QScrollArea, QSizePolicy,
QStackedWidget, QStyle, QStyleOptionFrame, QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame,
QVBoxLayout, QWidget, QDialog) QVBoxLayout, QWidget, QDialog)
# --- Core (GUI-free) logic layer --- # --- Core (GUI-free) logic layer ---

View File

@@ -555,11 +555,12 @@ class BalBuildWillDialog(BalDialog):
self.scroll_area = QScrollArea(self) self.scroll_area = QScrollArea(self)
self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.message_label) self.scroll_area.setWidget(self.message_label)
# Open the report area already ~500px tall (owner request, allegato1: # Open the report area ~450px tall (owner request: 500px left too much
# the previous ~140px area was far too short). The dialog may still grow # empty space below the short report; 450px lines up with the desired
# up to 700px to fit a few more lines; beyond that the vertical # window height). The dialog may still grow up to 700px to fit a few more
# scrollbar takes over and the bottom buttons stay reachable. # lines; beyond that the vertical scrollbar takes over and the bottom
self.scroll_area.setMinimumHeight(500) # buttons stay reachable.
self.scroll_area.setMinimumHeight(450)
self.scroll_area.setMaximumHeight(700) self.scroll_area.setMaximumHeight(700)
self.vbox.addWidget(self.scroll_area, 1) self.vbox.addWidget(self.scroll_area, 1)
@@ -701,6 +702,38 @@ class BalBuildWillDialog(BalDialog):
_logger.debug(f"not complete {e} true") _logger.debug(f"not complete {e} true")
message = False message = False
have_to_build = True have_to_build = True
# Task #7a: if the will was already executed, the wallet is empty and
# this exception is expected. Before showing the alarming "Found
# CHANGES ... a NEW WILL must be prepared" message, add a clear,
# reassuring note on the "Checking your will" row telling the user
# the inheritance is already on its way (mempool) or done (on-chain).
# The original message is still shown afterwards (owner request: the
# red/"changes" message stays, this is only an extra, more precise
# informative line).
# NOTE: we add the informative note as its OWN extra row (not via
# msg_set_checking, which reuses self.check_row and would be
# overwritten by the "Found CHANGES" line set below). Passing row=None
# to msg_set_status appends a new line, so the executed/mempool note
# and the original message are BOTH visible.
executed_status = self._executed_inheritance_status()
if executed_status == "CONFIRMED":
# Green: the inheritance transaction is confirmed on the
# blockchain, so it has been executed correctly.
self.msg_set_status(
_("Checking your will"),
None,
_("Inheritance already executed (on blockchain)"),
self.COLOR_OK,
)
elif executed_status == "MEMPOOL":
# Orange (warning colour): the transaction is in the mempool,
# waiting to be confirmed. Not an error, just "in progress".
self.msg_set_status(
_("Checking your will"),
None,
_("Inheritance in mempool (waiting confirmation)"),
self.COLOR_WARNING,
)
if isinstance(e, HeirChangeException): if isinstance(e, HeirChangeException):
message = _("Heirs changed:") message = _("Heirs changed:")
elif isinstance(e, WillExecutorNotPresent): elif isinstance(e, WillExecutorNotPresent):
@@ -741,8 +774,15 @@ class BalBuildWillDialog(BalDialog):
txs = self.bal_window.build_will() txs = self.bal_window.build_will()
if not txs: if not txs:
self.msg_set_building( self.msg_set_building(
_("Balance is too low, or CheckAlive is in the past.Skipped"), _(
color = self.COLOR_ERROR, "Balance is too low, or CheckAlive is in the "
"past. Skipped"
),
# Orange (warning) instead of red (error): an empty
# wallet after the inheritance was executed is a NORMAL
# situation, not a failure, so the colour should not
# alarm the user (owner request).
color=self.COLOR_WARNING,
) )
return False, None return False, None
@@ -1546,6 +1586,42 @@ class BalBuildWillDialog(BalDialog):
self.msg_edit_row(self.msg_error(f"Error: {b}")) self.msg_edit_row(self.msg_error(f"Error: {b}"))
_logger.error(f"error phase2: {b}") _logger.error(f"error phase2: {b}")
def _executed_inheritance_status(self):
"""Return the on-chain status of an already-executed inheritance.
Task #7a (owner request). After an inheritance is executed the wallet is
fully emptied (the plugin always empties the wallet). Pressing CHECK on
such an empty wallet makes ``check_will`` raise NotCompleteWillException
(the heirs no longer match the empty wallet), which previously showed the
alarming "Found CHANGES ... a NEW WILL must be prepared" message even
though the inheritance was, in fact, correctly executed.
To reassure the user we look at the will items, whose status is already
set to CONFIRMED / MEMPOOL by ``Will.check_will`` (see core/will.py), and
return a short code describing the real situation:
Returns:
"CONFIRMED" if at least one will transaction is confirmed on-chain
(the inheritance is already executed);
"MEMPOOL" if none is confirmed but at least one is in the mempool
(waiting for confirmation);
None if neither (the change really has to be rebuilt).
CONFIRMED takes precedence over MEMPOOL, since a confirmed transaction is
the strongest evidence that the inheritance has gone through.
"""
has_mempool = False
try:
for _wid, witem in self.bal_window.willitems.items():
if witem.get_status("CONFIRMED"):
return "CONFIRMED"
if witem.get_status("MEMPOOL"):
has_mempool = True
except Exception as _err:
# Never let this purely informational check break the flow.
_logger.debug(f"_executed_inheritance_status error: {_err}")
return "MEMPOOL" if has_mempool else None
def msg_set_checking(self, status="Waiting", row=None): def msg_set_checking(self, status="Waiting", row=None):
row = self.check_row if row is None else row row = self.check_row if row is None else row
self.check_row = self.msg_set_status(_("Checking your will"), row, status) self.check_row = self.msg_set_status(_("Checking your will"), row, status)

View File

@@ -438,6 +438,29 @@ class Plugin(BalPlugin):
def on_user_type_change(idx): def on_user_type_change(idx):
# Persist "basic"/"advanced" and refresh the open windows so the # Persist "basic"/"advanced" and refresh the open windows so the
# advanced controls appear/disappear right away. # advanced controls appear/disappear right away.
#
# SAFETY GATE (owner request): enabling ADVANCED exposes powerful,
# easy-to-misuse controls. Before switching to ADVANCED the user must
# type a confirmation phrase ("at My Risk", case-insensitive). If the
# phrase is wrong or the dialog is cancelled, we revert the combo to
# BASIC and do NOT enable ADVANCED. Switching back to BASIC needs no
# confirmation.
if idx == 1:
text, ok = QInputDialog.getText(
d,
_("Enable ADVANCED mode"),
_("Type 'at My Risk' to enable ADVANCED mode"),
)
# Accept any capitalisation (e.g. "at my risk", "AT MY RISK").
if not ok or text.strip().lower() != "at my risk":
# Revert to BASIC. Block the signal while we reset the combo
# index so this handler is not called again recursively.
user_type_combo.blockSignals(True)
user_type_combo.setCurrentIndex(0)
user_type_combo.blockSignals(False)
self.USER_TYPE.set("basic")
self.update_all()
return
self.USER_TYPE.set("advanced" if idx == 1 else "basic") self.USER_TYPE.set("advanced" if idx == 1 else "basic")
self.update_all() self.update_all()
@@ -473,40 +496,29 @@ class Plugin(BalPlugin):
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would # Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
# have made the grid the dialog's only layout, leaving no room for them. # have made the grid the dialog's only layout, leaving no room for them.
grid = QGridLayout() grid = QGridLayout()
add_widget( # NOTE: the "User Type" row used to be the first row (row 0). It was moved
grid, # to the BOTTOM of the grid (just above "Rebroadcast transactions") at the
"User Type", # owner's request; see the add_widget call further below. The remaining
user_type_combo, # rows were renumbered up by one accordingly.
0,
(
"Choose how much detail the plugin shows.\n\n"
"BASIC (default): a simpler interface. It hides the advanced "
"controls (the Raw/Date selector and the 'Check Alive' field) "
"and turns off the 'check alive' postpone behaviour, so you "
"only set the delivery date.\n\n"
"ADVANCED: shows every control, including the 'Check Alive' "
"field and the Raw/Date selector."
),
)
add_widget( add_widget(
grid, grid,
"Hide Replaced", "Hide Replaced",
heir_hide_replaced, heir_hide_replaced,
1, 0,
"Hide replaced transactions from will detail and list", "Hide replaced transactions from will detail and list",
) )
add_widget( add_widget(
grid, grid,
"Hide Invalidated", "Hide Invalidated",
heir_hide_invalidated, heir_hide_invalidated,
2, 1,
"Hide invalidated transactions from will detail and list", "Hide invalidated transactions from will detail and list",
) )
add_widget( add_widget(
grid, grid,
"Auto-sign on Check", "Auto-sign on Check",
heir_auto_sign, heir_auto_sign,
3, 2,
( (
"When checking, automatically sign and broadcast the will " "When checking, automatically sign and broadcast the will "
"transactions to their will-executors.\n" "transactions to their will-executors.\n"
@@ -518,7 +530,7 @@ class Plugin(BalPlugin):
grid, grid,
"Panel editable Date and Fee", "Panel editable Date and Fee",
heir_editable_dates, heir_editable_dates,
4, 3,
( (
"When enabled, the delivery-time and check-alive date fields " "When enabled, the delivery-time and check-alive date fields "
"can be edited everywhere (toolbar / Heirs tab), not only in " "can be edited everywhere (toolbar / Heirs tab), not only in "
@@ -540,7 +552,7 @@ class Plugin(BalPlugin):
grid, grid,
"Add transaction without willexecutor", "Add transaction without willexecutor",
heir_no_willexecutor, heir_no_willexecutor,
5, 4,
( (
"Create a will that does not require a Will-executor; it can be " "Create a will that does not require a Will-executor; it can be "
"saved, for example, on a USB stick, and a copy can be given to " "saved, for example, on a USB stick, and a copy can be given to "
@@ -551,7 +563,7 @@ class Plugin(BalPlugin):
grid, grid,
"Number of reminders", "Number of reminders",
heir_num_reminders, heir_num_reminders,
6, 5,
( (
"How many reminder alarms the exported calendar (.ics) event " "How many reminder alarms the exported calendar (.ics) event "
"contains.\n\n" "contains.\n\n"
@@ -568,7 +580,7 @@ class Plugin(BalPlugin):
grid, grid,
"Event summary", "Event summary",
edit_event_summary, edit_event_summary,
7, 6,
( (
"Default message to be used in event summary\n" "Default message to be used in event summary\n"
"Variables:\n" "Variables:\n"
@@ -581,7 +593,7 @@ class Plugin(BalPlugin):
grid, grid,
"Event description", "Event description",
edit_event_description, edit_event_description,
8, 7,
( (
"Default message to be used in event description\n" "Default message to be used in event description\n"
"Variables:\n" "Variables:\n"
@@ -607,6 +619,24 @@ class Plugin(BalPlugin):
# "Ask before to ping willexecutor", # "Ask before to ping willexecutor",
# ) # )
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ") # add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
# "User Type" placed here (row 8), at the BOTTOM of the settings just
# above "Rebroadcast transactions" (owner request). It used to be the very
# first row; the other rows were renumbered up by one when it was moved.
add_widget(
grid,
"User Type",
user_type_combo,
8,
(
"Choose how much detail the plugin shows.\n\n"
"BASIC (default): a simpler interface. It hides the advanced "
"controls (the Raw/Date selector and the 'Check Alive' field) "
"and turns off the 'check alive' postpone behaviour, so you "
"only set the delivery date.\n\n"
"ADVANCED: shows every control, including the 'Check Alive' "
"field and the Raw/Date selector."
),
)
grid.addWidget(heir_repush, 9, 0) grid.addWidget(heir_repush, 9, 0)
grid.addWidget( grid.addWidget(
HelpButton( HelpButton(
@@ -668,7 +698,7 @@ class Plugin(BalPlugin):
# reflects the reset values. # reflects the reset values.
self.update_all() self.update_all()
btn_reset = QPushButton(_("Reset setting")) btn_reset = QPushButton(_("Reset to Default Setting"))
btn_reset.setToolTip(_("Reset these settings to their default values")) btn_reset.setToolTip(_("Reset these settings to their default values"))
btn_reset.clicked.connect(on_reset_defaults) btn_reset.clicked.connect(on_reset_defaults)

View File

@@ -388,6 +388,43 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
self.combo.setCurrentIndex(index) self.combo.setCurrentIndex(index)
#self.on_current_index_changed(index, force) #self.on_current_index_changed(index, force)
def apply_user_type_visibility(self):
"""Re-apply the BASIC/ADVANCED visibility of the Raw/Date selector.
WHY this is needed (bug reported by the owner, task #04): the Raw/Date
combo is hidden in BASIC mode and shown in ADVANCED mode. That visibility
used to be decided ONLY in ``__init__`` (see the ``self._basic_mode``
block there). The WILL and HEIR tab toolbars are created once and then
REUSED for the whole session (they are not rebuilt when the user switches
USER TYPE), so after switching from BASIC to ADVANCED the combo stayed
hidden there and the user could only use the "Date" editor. The same
happened when a RAW value was pushed from the wizard: the raw number
showed (the active editor was updated) but the selector box stayed hidden
because nothing ever re-showed it. The wizard worked only because it is
recreated every time it is opened.
This method re-reads ``is_basic_mode()`` and shows/hides ONLY the combo.
It deliberately does NOT change the current value or the active editor
(owner request: keep the current value to avoid confusing the delivery
date with the inheritance). It is called from
``WillSettingsWidget.apply_user_type_visibility()`` (triggered by
``BalWindow.update_all()`` on a BASIC<->ADVANCED switch), so toggling the
mode takes effect immediately on the already-existing WILL/HEIR toolbars
without restarting Electrum.
It is safe to call repeatedly and on either layout (horizontal toolbar or
vertical wizard): it only flips the visibility of the Raw/Date combo.
"""
try:
basic = self.bal_window.bal_plugin.is_basic_mode()
except Exception:
# If the mode cannot be read, show the combo (the safe, most
# capable default for an existing widget).
basic = False
# Hidden in BASIC, visible in ADVANCED. The current value/editor is left
# untouched on purpose (see the docstring).
self.combo.setVisible(not basic)
def set_value( def set_value(
self, self,
x: Any, x: Any,
@@ -918,6 +955,18 @@ class WillSettingsWidget(QWidget):
# Hidden in BASIC, visible in ADVANCED. # Hidden in BASIC, visible in ADVANCED.
threshold.setVisible(not basic) threshold.setVisible(not basic)
# Task #04: also re-apply the Raw/Date selector visibility on BOTH the
# delivery-time (locktime) and the check-alive (threshold) boxes. The
# combo was hidden once at construction (BASIC) and the reused WILL/HEIR
# toolbars never re-showed it after switching to ADVANCED. Each composite
# decides its own combo visibility from is_basic_mode() WITHOUT touching
# its current value or editor.
for field in ("locktime", "threshold"):
widget = self.widgets.get(field)
apply_combo = getattr(widget, "apply_user_type_visibility", None)
if callable(apply_combo):
apply_combo()
def open_or_save_calendar(self): def open_or_save_calendar(self):
"""Build and save an .ics calendar file with SEPARATE reminder events. """Build and save an .ics calendar file with SEPARATE reminder events.

View File

@@ -1,7 +1,7 @@
{ {
"name": "bal", "name": "bal",
"fullname": "Bitcoin After Life", "fullname": "Bitcoin After Life",
"version": "0.4.7", "version": "0.4.8",
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.", "description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
"author": "Svatantrya", "author": "Svatantrya",
"licence": "MIT", "licence": "MIT",

127
tests/test_group_h_v048.py Normal file
View File

@@ -0,0 +1,127 @@
"""
Tests for the v0.4.8 changes (Group H).
These tests cover the small, GUI-free DECISION LOGIC introduced in v0.4.8,
without importing the Qt widgets (which need PyQt6 + an Electrum window). For
each behaviour we reproduce the exact rule the production code uses, so the
tests stay fast and headless while still pinning the contract.
Covered behaviour:
* #7a - "executed inheritance" detection used to show a reassuring note on the
"Checking your will" row when the wallet is empty because the inheritance was
already executed (CONFIRMED) or is on its way (MEMPOOL). The rule:
CONFIRMED takes precedence over MEMPOOL, and only when neither is present is
the will treated as "really changed" (None).
* #6 - the "at My Risk" confirmation gate for enabling ADVANCED mode. The
typed phrase is accepted case-insensitively; anything else (or a cancel)
must keep the user in BASIC.
Run:
PYTHONPATH=electrum-src python3 -m pytest tests/test_group_h_v048.py -q
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
# ------------------------------------------------------------------ #
# Mocks
# ------------------------------------------------------------------ #
class FakeWillItem:
"""Minimal will item exposing only ``get_status`` like the real WillItem."""
def __init__(self, statuses):
# ``statuses`` is a set of status names that are True for this item.
self._statuses = set(statuses)
def get_status(self, status):
return status in self._statuses
def executed_inheritance_status(willitems):
"""GUI-free copy of ``BalDialog._executed_inheritance_status``.
Mirrors the production rule exactly (see bal/gui/qt/dialogs.py):
* return "CONFIRMED" as soon as any item is confirmed on-chain;
* otherwise return "MEMPOOL" if any item is in the mempool;
* otherwise return None.
"""
has_mempool = False
for witem in willitems.values():
if witem.get_status("CONFIRMED"):
return "CONFIRMED"
if witem.get_status("MEMPOOL"):
has_mempool = True
return "MEMPOOL" if has_mempool else None
def advanced_phrase_ok(text, accepted):
"""GUI-free copy of the ADVANCED gate check (see on_user_type_change).
Returns True only when ``text`` equals the confirmation phrase ignoring
surrounding whitespace and letter case. ``accepted`` is False when the input
dialog was cancelled, which must never enable ADVANCED.
"""
if not accepted:
return False
return text.strip().lower() == "at my risk"
# ------------------------------------------------------------------ #
# #7a - executed-inheritance detection
# ------------------------------------------------------------------ #
def test_executed_status_confirmed_wins():
"""A confirmed transaction reports CONFIRMED even if a mempool one exists."""
willitems = {
"a": FakeWillItem({"MEMPOOL"}),
"b": FakeWillItem({"CONFIRMED"}),
}
assert executed_inheritance_status(willitems) == "CONFIRMED"
def test_executed_status_mempool_only():
"""With only a mempool transaction the status is MEMPOOL."""
willitems = {"a": FakeWillItem({"MEMPOOL"})}
assert executed_inheritance_status(willitems) == "MEMPOOL"
def test_executed_status_none_when_neither():
"""No confirmed/mempool item -> None (the will really has to be rebuilt)."""
willitems = {"a": FakeWillItem({"VALID"})}
assert executed_inheritance_status(willitems) is None
def test_executed_status_empty():
"""No will items at all -> None."""
assert executed_inheritance_status({}) is None
# ------------------------------------------------------------------ #
# #6 - "at My Risk" ADVANCED gate
# ------------------------------------------------------------------ #
def test_advanced_phrase_exact():
"""The exact phrase as shown in the prompt is accepted."""
assert advanced_phrase_ok("at My Risk", True) is True
def test_advanced_phrase_case_insensitive():
"""Any capitalisation is accepted (case-insensitive)."""
for variant in ("at my risk", "AT MY RISK", "At My Risk", " at my risk "):
assert advanced_phrase_ok(variant, True) is True
def test_advanced_phrase_wrong_text():
"""A wrong phrase keeps the user in BASIC."""
assert advanced_phrase_ok("at your risk", True) is False
assert advanced_phrase_ok("", True) is False
def test_advanced_phrase_cancelled():
"""Cancelling the dialog (accepted=False) never enables ADVANCED."""
assert advanced_phrase_ok("at My Risk", False) is False