Skip to main content
KeyOS API Reference

gui_server_api/navigation/
qrscanner.rs

1// SPDX-FileCopyrightText: 2024-2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! QR code scanner navigation request and response formats.
5
6use std::fmt;
7
8use app_manifest::QrPriority;
9use server::WithAppId;
10use xous::AppId;
11
12/// Options for the QR Scanner navigation request.
13///
14/// Example with a left back arrow and a simple message:
15///
16/// ```rust,ignore
17/// # use navigation::api::qrscanner::{ScanQrOptions};
18/// let options = ScanQrOptions::default()
19///     .with_start_location(Location::External)
20///     .with_allowed_locations(AllowedLocations::specific(&[Location::External]))
21///     .with_allowed_extensions(AllowedExtensions::specific(&["bin"]));
22/// ```
23/// A single rule (and the first sub-rule that triggered it) that matched a scanned QR code.
24#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
25pub struct ScanQrMatchedRule {
26    pub rule_id: String,
27    pub priority: QrPriority,
28    /// The ID of the first sub-rule that matched — used for dispatch hints without bloating the
29    /// message with every matching sub-rule.
30    pub sub_rule_id: String,
31}
32
33/// One entry per app that has at least one matching rule for the scanned QR code.
34/// All matched rules for that app are collected here so the same app never appears
35/// more than once in the disambiguation list.
36#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
37pub struct ScanQrMatchingApp {
38    #[rkyv(with = WithAppId)]
39    pub id: AppId,
40    pub matched_rules: Vec<ScanQrMatchedRule>,
41}
42
43#[derive(Debug, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
44pub struct ScanQrOptions {
45    pub header_title: String,
46    pub header_left_icon: String,
47    pub header_left_text: String,
48    pub header_right_icon: String,
49    pub header_right_text: String,
50    pub message: String,
51    pub button_icon: String,
52    pub button_text: String,
53    pub request_matching_apps: bool,
54}
55
56impl Default for ScanQrOptions {
57    fn default() -> Self {
58        Self {
59            header_title: String::new(),
60            header_left_icon: String::from("chevron-left"),
61            header_left_text: String::new(),
62            header_right_icon: String::new(),
63            header_right_text: String::new(),
64            message: String::new(),
65            button_icon: String::new(),
66            button_text: String::new(),
67            request_matching_apps: false,
68        }
69    }
70}
71
72impl ScanQrOptions {
73    pub fn new() -> Self {
74        Self {
75            header_title: String::new(),
76            header_left_icon: String::new(),
77            header_left_text: String::new(),
78            header_right_icon: String::new(),
79            header_right_text: String::new(),
80            message: String::new(),
81            button_icon: String::new(),
82            button_text: String::new(),
83            request_matching_apps: false,
84        }
85    }
86
87    pub fn from_slice(data: &[u8]) -> Option<Self> {
88        let Ok(archived) = rkyv::access::<ArchivedScanQrOptions, rkyv::rancor::Error>(data) else {
89            return None;
90        };
91        rkyv::deserialize::<Self, rkyv::rancor::Error>(archived).ok()
92    }
93
94    pub fn serialize(&self) -> Vec<u8> { rkyv::to_bytes::<rkyv::rancor::Error>(self).unwrap().to_vec() }
95}
96
97#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
98pub struct MatchedQrResult {
99    pub scan_result: ScanQrResult,
100    pub matched_rules: Vec<ScanQrMatchedRule>,
101}
102
103impl MatchedQrResult {
104    pub fn from_slice(data: &[u8]) -> Option<Self> {
105        let Ok(archived) = rkyv::access::<ArchivedMatchedQrResult, rkyv::rancor::Error>(data) else {
106            return None;
107        };
108        rkyv::deserialize::<Self, rkyv::rancor::Error>(archived).ok()
109    }
110
111    pub fn serialize(&self) -> Vec<u8> { rkyv::to_bytes::<rkyv::rancor::Error>(self).unwrap().to_vec() }
112}
113
114#[derive(Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
115pub enum ScanQrResult {
116    Qr { data: Vec<u8>, matching_apps: Option<Vec<ScanQrMatchingApp>> },
117    Ur2 { ur_type: String, data: Vec<u8>, matching_apps: Option<Vec<ScanQrMatchingApp>> },
118    LeftClicked,
119    RightClicked,
120    ButtonClicked,
121}
122
123// A scan can be a seed phrase or a private key, so Debug must not render the bytes.
124// The type and the matched apps are enough to tell why a payload went unhandled.
125impl fmt::Debug for ScanQrResult {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            Self::Qr { data, matching_apps } => formatter
129                .debug_struct("Qr")
130                .field("data_len", &data.len())
131                .field("matching_apps", matching_apps)
132                .finish_non_exhaustive(),
133            Self::Ur2 { ur_type, data, matching_apps } => formatter
134                .debug_struct("Ur2")
135                .field("ur_type", ur_type)
136                .field("data_len", &data.len())
137                .field("matching_apps", matching_apps)
138                .finish_non_exhaustive(),
139            Self::LeftClicked => formatter.write_str("LeftClicked"),
140            Self::RightClicked => formatter.write_str("RightClicked"),
141            Self::ButtonClicked => formatter.write_str("ButtonClicked"),
142        }
143    }
144}
145
146impl ScanQrResult {
147    pub fn new_qr(data: &[u8]) -> Self { Self::Qr { data: data.to_vec(), matching_apps: None } }
148
149    pub fn new_ur2(ur_type: String, data: &[u8]) -> Self {
150        Self::Ur2 { ur_type, data: data.to_vec(), matching_apps: None }
151    }
152
153    pub fn with_matching_apps(self, matching_apps: Vec<ScanQrMatchingApp>) -> Self {
154        match self {
155            Self::Qr { data, .. } => Self::Qr { data, matching_apps: Some(matching_apps) },
156            Self::Ur2 { ur_type, data, .. } => {
157                Self::Ur2 { ur_type, data, matching_apps: Some(matching_apps) }
158            }
159            other => other,
160        }
161    }
162
163    pub fn new_cancelled() -> Self { Self::LeftClicked }
164
165    pub fn from_slice(data: &[u8]) -> Option<Self> {
166        let Ok(archived) = rkyv::access::<ArchivedScanQrResult, rkyv::rancor::Error>(data) else {
167            return None;
168        };
169        rkyv::deserialize::<Self, rkyv::rancor::Error>(archived).ok()
170    }
171
172    pub fn serialize(&self) -> Vec<u8> { rkyv::to_bytes::<rkyv::rancor::Error>(self).unwrap().to_vec() }
173}