1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
// Author: cirakg & claude ai
// Summary: The menu (README 13.1): 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.
import Toybox.Application;
import Toybox.Lang;
import Toybox.Time;
import Toybox.Time.Gregorian;
import Toybox.Timer;
import Toybox.WatchUi;
module MainMenu {
function build() as WatchUi.Menu2 {
var vault = AppState.theVault();
var menu = new WatchUi.Menu2({:title => "OTP Wallet"});
if (vault.session.isUnlocked()) {
menu.addItem(new WatchUi.MenuItem("Lock now", null, :lock, null));
}
menu.addItem(new WatchUi.MenuItem(vault.hasPin() ? "Change PIN" : "Set PIN", null, :pin, null));
if (vault.hasPin()) {
menu.addItem(new WatchUi.MenuItem("Reset PIN", null, :reset, null)); // the confirmation says what it erases
}
menu.addItem(new WatchUi.MenuItem("Settings results", null, :results, null));
menu.addItem(new WatchUi.MenuItem("Entry IDs", null, :ids, null));
menu.addItem(new WatchUi.MenuItem("Clock status", null, :clock, null));
menu.addItem(new WatchUi.MenuItem("About", null, :about, null));
DebugMenu.addItems(menu);
return menu;
}
function entryIdLines() as Array<String> {
var vault = AppState.theVault();
var ids = vault.ids();
var lines = [] as Array<String>;
for (var i = 0; i < ids.size(); i++) {
var header = vault.header(ids[i]);
var name = header == null ? "damaged" : (header.name.length() > 0 ? header.name : "Locked entry");
lines.add(ids[i] + " " + name);
}
if (lines.size() == 0) {
lines.add("No entries yet.");
}
var pending = vault.pendingIds().size();
if (pending > 0) {
lines.add(pending + " locked entries wait for your PIN.");
}
return lines;
}
function clockLines() as Array<String> {
var lines = [] as Array<String>;
var watch = Clock.nowUnix(0);
var info = Gregorian.utcInfo(new Time.Moment(watch.toNumber()), Time.FORMAT_SHORT);
lines.add("Watch: " + info.hour.format("%02d") + ":" + info.min.format("%02d") + ":" + info.sec.format("%02d") + " UTC");
var trusted = Clock.trustedUnix();
if (trusted == null) {
lines.add("GPS time: not synced yet. Codes use the watch clock.");
} else {
var diff = (watch - trusted).toNumber();
lines.add("GPS time: synced");
lines.add("Difference: " + diff + " s" + (diff.abs() > 15 ? " (codes may be wrong)" : ""));
}
lines.add("Time offset setting: " + AppState.options.timeOffset + " s");
return lines;
}
function aboutLines() as Array<String> {
var vault = AppState.theVault();
var lines = ["OTP Wallet " + AppInfo.VERSION] as Array<String>;
lines.add("Entries: " + vault.ids().size() + (vault.pendingIds().size() > 0 ? " (+" + vault.pendingIds().size() + " pending)" : ""));
lines.add("Storage: " + (storageBytes(vault) + 1023) / 1024 + " KB");
lines.add("Self-test: " + AppState.selfTestSummary);
lines.add("No network access: your data stays on this watch.");
return lines;
}
// Bytes the entry records take (the largest part of the store).
function storageBytes(vault as Vault) as Number {
var total = 0;
var ids = vault.ids();
for (var i = 0; i < ids.size(); i++) {
var record = Application.Storage.getValue("e." + ids[i]);
if (record instanceof Lang.ByteArray) {
total += record.size();
}
}
return total;
}
}
class MainMenuDelegate 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 == :lock) {
AppState.lock();
WatchUi.popView(WatchUi.SLIDE_DOWN);
} else if (id == :pin) {
if (!vault.hasPin()) {
WatchUi.switchToView(new PinView(PinView.MODE_CREATE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
} else if (vault.session.isUnlocked()) {
WatchUi.switchToView(new PinView(PinView.MODE_CHANGE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
} else {
var then = new ChangePinAfterUnlock();
WatchUi.switchToView(new PinView(PinView.MODE_UNLOCK, then.method(:run)), new PinDelegate(), WatchUi.SLIDE_LEFT);
}
} else if (id == :reset) {
WatchUi.pushView(new WatchUi.Confirmation("Erase locked entries and set a new PIN?"), new ResetPinConfirmation(),
WatchUi.SLIDE_LEFT);
} else if (id == :results) {
WatchUi.switchToView(new InfoView("Results", Results.lines(), "results"), new InfoDelegate(),
WatchUi.SLIDE_LEFT);
Results.markSeen();
} else if (id == :ids) {
WatchUi.switchToView(new InfoView("Entry IDs", MainMenu.entryIdLines(), "ids"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
} else if (id == :clock) {
WatchUi.switchToView(new InfoView("Clock status", MainMenu.clockLines(), "clock"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
} else if (id == :about) {
WatchUi.switchToView(new InfoView("About", MainMenu.aboutLines(), "about"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
} else {
DebugMenu.handle(id);
}
}
}
// After unlocking from the menu's "Change PIN": ask for the new PIN.
class ChangePinAfterUnlock {
function initialize() {
}
function run() as Void {
WatchUi.switchToView(new PinView(PinView.MODE_CHANGE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
}
}
// "Reset PIN" after the user confirmed. The confirmation closes itself after
// onResponse, so the PIN screen opens from a short timer, replacing the menu.
class ResetPinConfirmation extends WatchUi.ConfirmationDelegate {
// Kept so the one-shot timer isn't collected before it fires.
var timer as Timer.Timer? = null;
function initialize() {
ConfirmationDelegate.initialize();
}
function onResponse(response as WatchUi.Confirm) as Boolean {
if (response == WatchUi.CONFIRM_YES) {
AppState.theVault().resetPin();
AppState.theCodes().clear();
var t = new Timer.Timer();
t.start(method(:openCreatePin), 100, false);
timer = t;
}
return true;
}
function openCreatePin() as Void {
timer = null;
WatchUi.switchToView(new PinView(PinView.MODE_CREATE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
}
}
|