diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-20 18:36:54 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-20 18:36:54 +0200 |
| commit | 151bfda5d4c567ff909a61dc9c90bf8eece1db37 (patch) | |
| tree | f8487b91cf6a9ce59dba02e95e67250fc84d8e80 /source/ui | |
| parent | 25910f3a7a7fb28c4a58367b6e56dd2468b60933 (diff) | |
feat(settings): add the Add OTP, Add code and Manage entry forms with a PIN queue for locked entries
The full phone settings form (README 10.2) and its processing (10.3) as one
resumable job: Import, Add OTP, Add code, Manage, then the About fields.
Forms are applied only when their switch is on, cleared afterwards (secret
first; only the secret after a problem), deduplicated by keyed hash, and
report into their Result field and the results screen.
- Add OTP validates per README 10.4; Add code runs the real encoder
(CodeCheck, a job for 2D codes) and caches 2D codes on the way in.
- Manage entry: edit, move, require/drop PIN, replace, counter, delete.
Changes to locked entries are sealed under "q" and applied only after
the PIN is entered under "Phone changes" in the menu.
- About: catalog (800 characters max), version, storage use.
- "Encrypt names" now applies to new and existing entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/ui')
| -rw-r--r-- | source/ui/ListView.mc | 9 | ||||
| -rw-r--r-- | source/ui/MainMenu.mc | 8 | ||||
| -rw-r--r-- | source/ui/PinView.mc | 2 | ||||
| -rw-r--r-- | source/ui/QueueMenu.mc | 91 |
4 files changed, 107 insertions, 3 deletions
diff --git a/source/ui/ListView.mc b/source/ui/ListView.mc index cc4e5f3..94b8c99 100644 --- a/source/ui/ListView.mc +++ b/source/ui/ListView.mc @@ -179,14 +179,19 @@ class ListView extends WatchUi.View { } // "Importing… 40%" while an import runs, "Results ready" when settings - // results haven't been looked at, else the app name. + // results haven't been looked at, "Confirm in menu" while phone changes + // wait for the PIN, else the app name. private function title() as String { var p = AppState.processor; var busy = p != null ? p.busyText() : null; if (busy != null) { return busy; } - return Results.unseen() ? "Results ready (menu)" : "OTP Wallet"; + if (Results.unseen()) { + return "Results ready (menu)"; + } + var v = AppState.vault; + return v != null && ChangeQueue.count(v) > 0 ? "Confirm in menu" : "OTP Wallet"; } private function drawEmpty(dc as Graphics.Dc, top as Number, bottom as Number) as Void { diff --git a/source/ui/MainMenu.mc b/source/ui/MainMenu.mc index 3d13180..8863c6d 100644 --- a/source/ui/MainMenu.mc +++ b/source/ui/MainMenu.mc @@ -1,5 +1,5 @@ // Author: cirakg & claude ai -// Summary: The menu (README 13.1): lock now, PIN actions, settings results, entry IDs, clock status, about. +// Summary: The menu (README 13.1): phone changes to confirm, lock now, PIN actions, settings results, entry IDs, clock status, about. // // A native Menu2, so scrolling, touch and buttons behave like every other // Garmin menu. Debug builds add the items from DebugMenu. @@ -16,6 +16,10 @@ module MainMenu { function build() as WatchUi.Menu2 { var vault = AppState.theVault(); var menu = new WatchUi.Menu2({:title => "OTP Wallet"}); + var waiting = ChangeQueue.count(vault); + if (waiting > 0) { + menu.addItem(new WatchUi.MenuItem("Phone changes", waiting + " to confirm", :queue, null)); + } if (vault.session.isUnlocked()) { menu.addItem(new WatchUi.MenuItem("Lock now", null, :lock, null)); } @@ -120,6 +124,8 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate { WatchUi.switchToView(new InfoView("Results", Results.lines(), "results"), new InfoDelegate(), WatchUi.SLIDE_LEFT); Results.markSeen(); + } else if (id == :queue) { + WatchUi.switchToView(QueueMenu.build(), new QueueMenuDelegate(), WatchUi.SLIDE_LEFT); } else if (id == :ids) { WatchUi.switchToView(new InfoView("Entry IDs", MainMenu.entryIdLines(), "ids"), new InfoDelegate(), WatchUi.SLIDE_LEFT); } else if (id == :clock) { diff --git a/source/ui/PinView.mc b/source/ui/PinView.mc index 6eefa55..4502919 100644 --- a/source/ui/PinView.mc +++ b/source/ui/PinView.mc @@ -225,6 +225,8 @@ class PinView extends WatchUi.View { if (vault.finishUnlock(_job as PinJob)) { _job = null; vault.promotePending(); + // Unlocked: "Encrypt names" can now reach locked entries. + AppState.processSettings(); succeed(); return; } diff --git a/source/ui/QueueMenu.mc b/source/ui/QueueMenu.mc new file mode 100644 index 0000000..880ca7c --- /dev/null +++ b/source/ui/QueueMenu.mc @@ -0,0 +1,91 @@ +// Author: cirakg & claude ai +// Summary: "Phone changes": lists the changes to locked entries waiting for the PIN, and confirms or rejects them (README 7.9). +// +// Confirming always asks for the PIN, even while the watch is unlocked: +// the point is that a person at the watch agrees, not that a key is +// available. (Before any PIN exists only "Require PIN" can wait here, and +// confirming creates the PIN.) The changes are then applied by a +// ChangeQueueJob on a timer and the results screen shows what happened. + +import Toybox.Lang; +import Toybox.WatchUi; + +module QueueMenu { + + function build() as WatchUi.Menu2 { + var vault = AppState.theVault(); + var menu = new WatchUi.Menu2({:title => "Phone changes"}); + menu.addItem(new WatchUi.MenuItem("Confirm all", vault.hasPin() ? "Asks for your PIN" : "Sets a PIN first", :confirm, null)); + menu.addItem(new WatchUi.MenuItem("Reject all", null, :reject, null)); + var items = ChangeQueue.descriptions(vault); + for (var i = 0; i < items.size(); i++) { + menu.addItem(new WatchUi.MenuItem(items[i], null, i, null)); + } + return menu; + } +} + +class QueueMenuDelegate extends WatchUi.Menu2InputDelegate { + + function initialize() { + Menu2InputDelegate.initialize(); + } + + function onSelect(item as WatchUi.MenuItem) as Void { + AppState.activity(); + var id = item.getId(); + var vault = AppState.theVault(); + if (id == :confirm) { + var then = new ApplyQueueAfterPin(); + var mode = vault.hasPin() ? PinView.MODE_UNLOCK : PinView.MODE_CREATE; + WatchUi.switchToView(new PinView(mode, then.method(:run)), new PinDelegate(), WatchUi.SLIDE_LEFT); + } else if (id == :reject) { + var lines = ["Rejected these changes from the phone:"] as Array<String>; + lines.addAll(ChangeQueue.descriptions(vault)); + ChangeQueue.clear(vault); + Results.save(lines); + WatchUi.switchToView(new InfoView("Results", lines, "results"), new InfoDelegate(), WatchUi.SLIDE_LEFT); + Results.markSeen(); + AppState.processSettings(); // the About fields mention waiting changes + } else if (id instanceof Number) { + // The full text of one change (menu labels are cut short). + var items = ChangeQueue.descriptions(vault); + if (id < items.size()) { + WatchUi.pushView(new InfoView("Change", [items[id]] as Array<String>, "change"), new InfoDelegate(), + WatchUi.SLIDE_LEFT); + } + } + } +} + +// After the PIN: apply the waiting changes, then show what happened. +class ApplyQueueAfterPin { + + // The job's timer driver (kept so it isn't collected while it runs). + var runner as Jobs.JobRunner? = null; + private var _job as ChangeQueueJob? = null; + + function initialize() { + } + + function run() as Void { + var job = new ChangeQueueJob(AppState.theVault()); + _job = job; + WatchUi.switchToView(new InfoView("Results", ["Applying the changes…"] as Array<String>, "applying"), new InfoDelegate(), + WatchUi.SLIDE_LEFT); + var r = new Jobs.JobRunner(job, 1, method(:onDone)); + runner = r; + r.start(); + } + + function onDone() as Void { + var job = _job as ChangeQueueJob; + _job = null; + runner = null; + AppState.theCodes().clear(); + Results.save(job.lines); + WatchUi.switchToView(new InfoView("Results", job.lines, "results"), new InfoDelegate(), WatchUi.SLIDE_IMMEDIATE); + Results.markSeen(); + AppState.processSettings(); // refresh the catalog on the phone + } +} |
