use std::collections::BTreeMap;
use sha1::{Digest, Sha1};
use crate::subject::{auto_foreground, FieldAlignment, RgbColor, TextColorMode, WalletSubject};
use crate::WalletError;
use super::ApplePassBuilder;
pub(crate) fn build_pass_json<S: WalletSubject>(
builder: &ApplePassBuilder,
subject: &S,
) -> Result<Vec<u8>, WalletError> {
let branding = subject.branding();
let org_name = branding
.organization_name
.clone()
.unwrap_or_else(|| builder.app_name.clone());
let foreground = match branding.text_color_mode {
TextColorMode::Auto => auto_foreground(branding.background_color),
TextColorMode::Light => RgbColor {
r: 255,
g: 255,
b: 255,
},
TextColorMode::Dark => RgbColor { r: 0, g: 0, b: 0 },
};
let label_color = foreground;
let mut pass = serde_json::Map::new();
pass.insert("formatVersion".into(), serde_json::json!(1));
pass.insert(
"passTypeIdentifier".into(),
serde_json::json!(builder.pass_type_id),
);
pass.insert("teamIdentifier".into(), serde_json::json!(builder.team_id));
pass.insert("serialNumber".into(), serde_json::json!(subject.serial()));
pass.insert("organizationName".into(), serde_json::json!(org_name));
pass.insert(
"description".into(),
serde_json::json!(builder.app_name.clone()),
);
pass.insert(
"backgroundColor".into(),
serde_json::json!(branding.background_color.css_rgb()),
);
pass.insert(
"foregroundColor".into(),
serde_json::json!(foreground.css_rgb()),
);
pass.insert(
"labelColor".into(),
serde_json::json!(label_color.css_rgb()),
);
if let Some(logo_text) = branding.logo_text.clone() {
pass.insert("logoText".into(), serde_json::json!(logo_text));
}
pass.insert(
"barcodes".into(),
serde_json::json!([{
"format": "PKBarcodeFormatQR",
"message": subject.barcode_token(),
"messageEncoding": "iso-8859-1"
}]),
);
if let Some(t) = subject.relevant_at() {
pass.insert("relevantDate".into(), serde_json::json!(t.to_rfc3339()));
}
if let Some(t) = subject.expires_at() {
pass.insert("expirationDate".into(), serde_json::json!(t.to_rfc3339()));
}
let locs: Vec<serde_json::Value> = subject
.locations()
.iter()
.map(|g| {
let mut obj = serde_json::Map::new();
obj.insert("latitude".into(), serde_json::json!(g.latitude));
obj.insert("longitude".into(), serde_json::json!(g.longitude));
if let Some(t) = &g.relevant_text {
obj.insert("relevantText".into(), serde_json::json!(t));
}
serde_json::Value::Object(obj)
})
.collect();
if !locs.is_empty() {
pass.insert("locations".into(), serde_json::Value::Array(locs));
}
let kind = subject.pass_kind();
let kind_key = match &kind {
crate::subject::PassKind::EventTicket => "eventTicket",
crate::subject::PassKind::BoardingPass(_) => "boardingPass",
crate::subject::PassKind::Generic => "generic",
crate::subject::PassKind::Coupon => "coupon",
};
let serialise_field = |f: &crate::subject::Field| -> serde_json::Value {
let alignment = match f.alignment {
FieldAlignment::Left => "PKTextAlignmentLeft",
FieldAlignment::Center => "PKTextAlignmentCenter",
FieldAlignment::Right => "PKTextAlignmentRight",
FieldAlignment::Natural => "PKTextAlignmentNatural",
};
serde_json::json!({
"key": f.key,
"label": f.label,
"value": f.value,
"textAlignment": alignment,
})
};
let mut fields = serde_json::Map::new();
fields.insert(
"primaryFields".into(),
serde_json::Value::Array(subject.primary().iter().map(serialise_field).collect()),
);
fields.insert(
"secondaryFields".into(),
serde_json::Value::Array(subject.secondary().iter().map(serialise_field).collect()),
);
fields.insert(
"auxiliaryFields".into(),
serde_json::Value::Array(subject.auxiliary().iter().map(serialise_field).collect()),
);
fields.insert(
"backFields".into(),
serde_json::Value::Array(subject.back().iter().map(serialise_field).collect()),
);
if let crate::subject::PassKind::BoardingPass(transit) = &kind {
fields.insert(
"transitType".into(),
serde_json::Value::String(transit.as_apple_str().to_string()),
);
}
pass.insert(kind_key.to_string(), serde_json::Value::Object(fields));
serde_json::to_vec(&serde_json::Value::Object(pass))
.map_err(|e| WalletError::ApplePackage(format!("pass.json serialise: {e}")))
}
pub(crate) fn build_manifest(entries: &[(String, Vec<u8>)]) -> Result<Vec<u8>, WalletError> {
let mut map: BTreeMap<String, String> = BTreeMap::new();
for (name, bytes) in entries {
map.insert(name.clone(), sha1_hex_lower(bytes));
}
serde_json::to_vec(&map).map_err(|e| WalletError::ApplePackage(format!("manifest json: {e}")))
}
fn sha1_hex_lower(bytes: &[u8]) -> String {
let mut h = Sha1::new();
h.update(bytes);
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_sha1_lowercase_hex() {
let entries = vec![("hello.txt".to_string(), b"hello".to_vec())];
let manifest_bytes = build_manifest(&entries).unwrap();
let v: serde_json::Value = serde_json::from_slice(&manifest_bytes).unwrap();
assert_eq!(v["hello.txt"], "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
}
#[test]
fn manifest_is_deterministic_across_calls() {
let entries = vec![
("a.txt".to_string(), b"a".to_vec()),
("b.txt".to_string(), b"b".to_vec()),
("c.txt".to_string(), b"c".to_vec()),
];
let m1 = build_manifest(&entries).unwrap();
let m2 = build_manifest(&entries).unwrap();
assert_eq!(m1, m2);
}
#[test]
fn manifest_uses_sorted_keys() {
let entries = vec![
("zebra.png".to_string(), b"z".to_vec()),
("alpha.png".to_string(), b"a".to_vec()),
];
let manifest_bytes = build_manifest(&entries).unwrap();
let s = std::str::from_utf8(&manifest_bytes).unwrap();
let alpha_pos = s.find("alpha.png").unwrap();
let zebra_pos = s.find("zebra.png").unwrap();
assert!(
alpha_pos < zebra_pos,
"alpha.png should sort before zebra.png in manifest"
);
}
}