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.capabilities.join(", ")
97 ));
98 }
99 output.push_str(&format!(
100 "Expires: {}\n",
101 token.expires_at.format("%H:%M:%S")
102 ));
103 output.push('\n');
104
105 Ok(output)
106}
107
108#[cfg(test)]
109#[allow(clippy::disallowed_methods)]
110mod tests {
111 use super::*;
112
113 fn make_token() -> PairingToken {
114 PairingToken::generate(
115 chrono::Utc::now(),
116 "did:keri:test123".to_string(),
117 "http://localhost:3000".to_string(),
118 vec!["sign_commit".to_string()],
119 )
120 .unwrap()
121 .token
122 }
123
124 #[test]
125 fn test_render_qr() {
126 let token = make_token();
127 let options = QrOptions::default();
128
129 let qr = render_qr(&token, &options).unwrap();
130 assert!(!qr.is_empty());
131 assert!(qr.contains('\u{2580}') || qr.contains('\u{2584}') || qr.contains('\u{2588}'));
133 }
134
135 #[test]
136 fn test_render_qr_inverted() {
137 let token = make_token();
138 let options = QrOptions {
139 invert: true,
140 ..Default::default()
141 };
142
143 let qr = render_qr(&token, &options).unwrap();
144 assert!(!qr.is_empty());
145 }
146
147 #[test]
148 fn test_render_qr_non_compact() {
149 let token = make_token();
150 let options = QrOptions {
151 compact: false,
152 ..Default::default()
153 };
154
155 let qr = render_qr(&token, &options).unwrap();
156 assert!(!qr.is_empty());
157 }
158}