// Author: cirakg & claude ai // Summary: The app's shared state: options, the vault, the code cache, start-up errors and the crypto self-test. // // Created in getInitialView (never in glance mode, which has too little // memory for the vault). Views reach the vault and options through here. import Toybox.Application; import Toybox.Lang; import Toybox.Time; import Toybox.WatchUi; module AppState { const SELF_TEST_KEY = "st"; const SECONDS_PER_DAY = 86400; var options as Options = new Options(); var vault as Vault? = null; var codes as CodeCache? = null; var processor as SettingsProcessor? = null; // A message that replaces the whole app (newer data, damaged metadata, // failed self-test), or null. var startError as String? = null; // Codes are shown only after the self-test passed (README 7.10). var cryptoReady as Boolean = false; var selfTestSummary as String = "Not run yet"; // Load options and open the vault. Errors end up in startError. function start() as Void { options = new Options(); options.load(); if (TestMode.ON) { Fixtures.resetStore(); } try { var v = new Vault(new Persist.AppStorageBackend(), new TimeSource()); applyOptions(v); vault = v; codes = new CodeCache(v); processor = new SettingsProcessor(); if (TestMode.ON) { Fixtures.load(v); } } catch (e instanceof VaultError) { startError = e.code == VaultError.NEWER_SCHEMA ? "Data from a newer version of OTP Wallet. Update the app." : "Stored data is damaged. Reinstall the app to start over."; } } // The vault; only call after start() succeeded. function theVault() as Vault { return vault as Vault; } function theCodes() as CodeCache { return codes as CodeCache; } // Run the known-answer self-test on the first start of each version and // then once a day. Returns false (and sets startError) when it failed. function runSelfTestIfDue() as Boolean { var day = (Clock.nowUnix(0) / SECONDS_PER_DAY).toNumber(); var last = Application.Storage.getValue(SELF_TEST_KEY); var due = true; if (last instanceof Array && last.size() == 2 && last[0] instanceof String && last[1] instanceof Number) { due = !(last[0] as String).equals(AppInfo.VERSION) || (last[1] as Number) != day; } if (!due) { selfTestSummary = "Passed (today)"; cryptoReady = true; return true; } var failed = SelfTest.run(); if (failed != null) { selfTestSummary = "FAILED: " + failed; startError = "Crypto self-test failed on this watch (" + failed + "). Codes are not shown."; return false; } Application.Storage.setValue(SELF_TEST_KEY, [AppInfo.VERSION, day] as Array); selfTestSummary = "Passed"; cryptoReady = true; return true; } // The phone changed the settings: reload the options and apply forms. function settingsChanged() as Void { options.load(); var v = vault; if (v != null) { applyOptions(v); processSettings(); } // Theme, code size and groups are read at every draw. WatchUi.requestUpdate(); } // The options the vault itself uses. function applyOptions(v as Vault) as Void { v.eraseAfterFailures = options.eraseAfterFailures; v.encryptNames = options.encryptNames; v.session.autoLockMs = TestMode.ON ? TestMode.AUTO_LOCK_MS : options.autoLockMs; } // Apply any settings form that asks for it (after start-up and on change). function processSettings() as Void { var p = processor; if (p != null && cryptoReady) { p.start(); } } // Note user activity (restarts the auto-lock timeout). function activity() as Void { var v = vault; if (v != null) { v.session.touch(); } } // Lock if the inactivity timeout passed. True when this call locked. function checkAutoLock() as Boolean { var v = vault; if (v != null && v.session.checkExpiry()) { theCodes().clear(); return true; } return false; } // Lock now and forget every computed code. function lock() as Void { var v = vault; if (v != null) { v.lock(); theCodes().clear(); } } function now() as Long { return Clock.nowUnix(options.timeOffset); } }