smbcloud_ascapi_frontend/
review.rs1use serde::Serialize;
9use smbcloud_ascapi_aso::app_store_version::Platform;
10use smbcloud_ascapi_aso::prelude::*;
11use smbcloud_ascapi_aso::review_submission::OPEN_STATES;
12use smbcloud_ascapi_core::Client;
13
14#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
16pub struct SubmissionPlan {
17 pub app_id: String,
18 pub app_store_version_id: String,
19 pub version_string: Option<String>,
20 pub platform: String,
21 pub app_version_state: Option<String>,
22 pub existing_submission_id: Option<String>,
27 pub existing_submission_state: Option<String>,
29 pub existing_submission_has_items: bool,
33 pub blocked: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
40pub struct SubmittedReview {
41 pub submission_id: String,
42 pub state: Option<String>,
43 pub submitted_date: Option<String>,
44 pub plan: SubmissionPlan,
45}
46
47pub async fn plan_submission(
48 client: &Client,
49 app_id: &str,
50 app_store_version_id: &str,
51) -> Result<SubmissionPlan, String> {
52 let version = client
53 .get_app_store_version(app_store_version_id)
54 .await
55 .map_err(|error| error.to_string())?;
56 let platform = version.attributes.platform;
57
58 let open = client
59 .list_review_submissions(app_id, Some(platform), &OPEN_STATES)
60 .await
61 .map_err(|error| error.to_string())?;
62 let existing = open.into_iter().next();
63 let existing_submission_state = existing
64 .as_ref()
65 .and_then(|submission| submission.attributes.state.clone());
66
67 let existing_submission_has_items = match &existing {
68 Some(submission) => !client
69 .list_review_submission_items(&submission.id)
70 .await
71 .map_err(|error| error.to_string())?
72 .is_empty(),
73 None => false,
74 };
75
76 let blocked = version
77 .attributes
78 .app_version_state
79 .as_deref()
80 .and_then(blocked_reason);
81
82 Ok(SubmissionPlan {
83 blocked,
84 app_id: app_id.to_string(),
85 app_store_version_id: app_store_version_id.to_string(),
86 version_string: version.attributes.version_string,
87 platform: platform_name(platform).to_string(),
88 app_version_state: version.attributes.app_version_state,
89 existing_submission_id: existing.map(|submission| submission.id),
90 existing_submission_state,
91 existing_submission_has_items,
92 })
93}
94
95pub async fn submit_for_review(
104 client: &Client,
105 plan: SubmissionPlan,
106) -> Result<SubmittedReview, String> {
107 if let Some(reason) = &plan.blocked {
108 return Err(reason.clone());
109 }
110 let platform = platform_from_name(&plan.platform)?;
111 let submission_id = match &plan.existing_submission_id {
112 Some(id) => id.clone(),
113 None => {
114 client
115 .create_review_submission(&plan.app_id, platform)
116 .await
117 .map_err(|error| error.to_string())?
118 .id
119 }
120 };
121
122 if !plan.existing_submission_has_items {
123 client
124 .add_app_store_version_to_review_submission(&submission_id, &plan.app_store_version_id)
125 .await
126 .map_err(|error| error.to_string())?;
127 }
128
129 let submitted = client
130 .submit_review_submission(&submission_id)
131 .await
132 .map_err(|error| error.to_string())?;
133
134 Ok(SubmittedReview {
135 submission_id: submitted.id,
136 state: submitted.attributes.state,
137 submitted_date: submitted.attributes.submitted_date,
138 plan,
139 })
140}
141
142fn blocked_reason(state: &str) -> Option<String> {
146 const SUBMITTED_OR_LIVE: [&str; 9] = [
147 "WAITING_FOR_REVIEW",
148 "IN_REVIEW",
149 "PENDING_APPLE_RELEASE",
150 "PENDING_DEVELOPER_RELEASE",
151 "PROCESSING_FOR_DISTRIBUTION",
152 "READY_FOR_DISTRIBUTION",
153 "READY_FOR_SALE",
154 "ACCEPTED",
155 "REPLACED_WITH_NEW_VERSION",
156 ];
157 SUBMITTED_OR_LIVE.contains(&state).then(|| {
158 format!(
159 "this version is {state}, so it has already been submitted; \
160 `review-submissions list` shows its submission, and \
161 `review-submissions cancel` withdraws one still waiting for review"
162 )
163 })
164}
165
166fn platform_name(platform: Platform) -> &'static str {
167 match platform {
168 Platform::Ios => "IOS",
169 Platform::MacOs => "MAC_OS",
170 Platform::TvOs => "TV_OS",
171 Platform::VisionOs => "VISION_OS",
172 }
173}
174
175fn platform_from_name(name: &str) -> Result<Platform, String> {
176 match name {
177 "IOS" => Ok(Platform::Ios),
178 "MAC_OS" => Ok(Platform::MacOs),
179 "TV_OS" => Ok(Platform::TvOs),
180 "VISION_OS" => Ok(Platform::VisionOs),
181 other => Err(format!("unknown platform {other:?} in submission plan")),
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn blocks_a_version_that_was_already_submitted() {
191 assert!(blocked_reason("WAITING_FOR_REVIEW").is_some());
192 assert!(blocked_reason("READY_FOR_DISTRIBUTION").is_some());
193 assert_eq!(blocked_reason("PREPARE_FOR_SUBMISSION"), None);
194 assert_eq!(blocked_reason("DEVELOPER_REJECTED"), None);
195 assert_eq!(blocked_reason("REJECTED"), None);
196 }
197
198 #[test]
199 fn platform_names_round_trip() {
200 for platform in [
201 Platform::Ios,
202 Platform::MacOs,
203 Platform::TvOs,
204 Platform::VisionOs,
205 ] {
206 assert_eq!(platform_from_name(platform_name(platform)), Ok(platform));
207 }
208 assert!(platform_from_name("WATCH_OS").is_err());
209 }
210}