aboutsummaryrefslogtreecommitdiff
path: root/source/ui
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
commit151bfda5d4c567ff909a61dc9c90bf8eece1db37 (patch)
treef8487b91cf6a9ce59dba02e95e67250fc84d8e80 /source/ui
parent25910f3a7a7fb28c4a58367b6e56dd2468b60933 (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.mc9
-rw-r--r--source/ui/MainMenu.mc8
-rw-r--r--source/ui/PinView.mc2
-rw-r--r--source/ui/QueueMenu.mc91
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
+ }
+}