1use qrcode::QrCode;
4use qrcode::render::unicode;
5
6use super::error::PairingError;
7use super::token::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)]
109mod tests {
110 use super::*;
111
112 fn make_token() -> PairingToken {
113 PairingToken::generate(
114 chrono::Utc::now(),
115 "did:keri:test123".to_string(),
116 "http://localhost:3000".to_string(),
117 vec!["sign_commit".to_string()],
118 )
119 .unwrap()
120 .token
121 }
122
123 #[test]
124 fn test_render_qr() {
125 let token = make_token();
126 let options = QrOptions::default();
127
128 let qr = render_qr(&token, &options).unwrap();
129 assert!(!qr.is_empty());
130 assert!(qr.contains('\u{2580}') || qr.contains('\u{2584}') || qr.contains('\u{2588}'));
132 }
133
134 #[test]
135 fn test_render_qr_inverted() {
136 let token = make_token();
137 let options = QrOptions {
138 invert: true,
139 ..Default::default()
140 };
141
142 let qr = render_qr(&token, &options).unwrap();
143 assert!(!qr.is_empty());
144 }
145
146 #[test]
147 fn test_render_qr_non_compact() {
148 let token = make_token();
149 let options = QrOptions {
150 compact: false,
151 ..Default::default()
152 };
153
154 let qr = render_qr(&token, &options).unwrap();
155 assert!(!qr.is_empty());
156 }
157}