1use qrcode::QrCode;
4use qrcode::render::unicode;
5
6use super::PairingError;
7use auths_pairing_protocol::PairingToken;
8
9#[derive(Debug, Clone)]
11pub struct QrOptions {
12 pub compact: bool,
14 pub quiet_zone: u32,
16 pub invert: bool,
18}
19
20impl Default for QrOptions {
21 fn default() -> Self {
22 Self {
23 compact: true,
24 quiet_zone: 1,
25 invert: false,
26 }
27 }
28}
29
30pub fn render_qr(token: &PairingToken, options: &QrOptions) -> Result<String, PairingError> {
32 let uri = token.to_uri();
33 render_qr_from_data(&uri, options)
34}
35
36pub fn render_qr_from_data(data: &str, options: &QrOptions) -> Result<String, PairingError> {
38 let code =
39 QrCode::new(data.as_bytes()).map_err(|e| PairingError::QrCodeFailed(e.to_string()))?;
40
41 let qr_string = if options.compact {
42 let (dark, light) = if options.invert {
44 (unicode::Dense1x2::Light, unicode::Dense1x2::Dark)
45 } else {
46 (unicode::Dense1x2::Dark, unicode::Dense1x2::Light)
47 };
48
49 code.render::<unicode::Dense1x2>()
50 .dark_color(dark)
51 .light_color(light)
52 .quiet_zone(options.quiet_zone > 0)
53 .build()
54 } else {
55 let (dark, light) = if options.invert {
57 (' ', '\u{2588}')
58 } else {
59 ('\u{2588}', ' ')
60 };
61
62 code.render::<char>()
63 .dark_color(dark)
64 .light_color(light)
65 .quiet_zone(options.quiet_zone > 0)
66 .build()
67 };
68
69 Ok(qr_string)
70}
71
72pub fn format_pairing_qr(token: &PairingToken) -> Result<String, PairingError> {
77 let options = QrOptions::default();
78 let qr = render_qr(token, &options)?;
79
80 let mut output = String::new();
81 output.push('\n');
82 output.push_str("Scan this QR code with your other device:\n");
83 output.push('\n');
84 output.push_str(&qr);
85 output.push('\n');
86 output.push('\n');
87 output.push_str(&format!(
88 "Or enter this code manually: {}\n",
89 token.short_code
90 ));
91 output.push('\n');
92 output.push_str(&format!("Controller: {}\n", token.controller_did));
93 if !token.capabilities.is_empty() {
94 output.push_str(&format!(
95 "Capabilities: {}\n",
96 token
97 .capabilities
98 .iter()
99 .map(|c| c.as_str())
100 .collect::<Vec<_>>()
101 .join(", ")
102 ));
103 }
104 output.push_str(&format!(
105 "Expires: {}\n",
106 token.expires_at.format("%H:%M:%S")
107 ));
108 output.push('\n');
109
110 Ok(output)
111}
112
113#[cfg(test)]
114#[allow(clippy::disallowed_methods)]
115mod tests {
116 use super::*;
117
118 fn make_token() -> PairingToken {
119 PairingToken::generate(
120 chrono::Utc::now(),
121 "did:keri:test123".to_string(),
122 "http://localhost:3000".to_string(),
123 vec![auths_keri::Capability::sign_commit()],
124 )
125 .unwrap()
126 .token
127 }
128
129 #[test]
130 fn test_render_qr() {
131 let token = make_token();
132 let options = QrOptions::default();
133
134 let qr = render_qr(&token, &options).unwrap();
135 assert!(!qr.is_empty());
136 assert!(qr.contains('\u{2580}') || qr.contains('\u{2584}') || qr.contains('\u{2588}'));
138 }
139
140 #[test]
141 fn test_render_qr_inverted() {
142 let token = make_token();
143 let options = QrOptions {
144 invert: true,
145 ..Default::default()
146 };
147
148 let qr = render_qr(&token, &options).unwrap();
149 assert!(!qr.is_empty());
150 }
151
152 #[test]
153 fn test_render_qr_non_compact() {
154 let token = make_token();
155 let options = QrOptions {
156 compact: false,
157 ..Default::default()
158 };
159
160 let qr = render_qr(&token, &options).unwrap();
161 assert!(!qr.is_empty());
162 }
163}