use serde::Serialize;
use smbcloud_ascapi_aso::app_store_version::Platform;
use smbcloud_ascapi_aso::prelude::*;
use smbcloud_ascapi_aso::review_submission::OPEN_STATES;
use smbcloud_ascapi_core::Client;
#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
pub struct SubmissionPlan {
pub app_id: String,
pub app_store_version_id: String,
pub version_string: Option<String>,
pub platform: String,
pub app_version_state: Option<String>,
pub existing_submission_id: Option<String>,
pub existing_submission_state: Option<String>,
pub existing_submission_has_items: bool,
pub blocked: Option<String>,
}
#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
pub struct SubmittedReview {
pub submission_id: String,
pub state: Option<String>,
pub submitted_date: Option<String>,
pub plan: SubmissionPlan,
}
pub async fn plan_submission(
client: &Client,
app_id: &str,
app_store_version_id: &str,
) -> Result<SubmissionPlan, String> {
let version = client
.get_app_store_version(app_store_version_id)
.await
.map_err(|error| error.to_string())?;
let platform = version.attributes.platform;
let open = client
.list_review_submissions(app_id, Some(platform), &OPEN_STATES)
.await
.map_err(|error| error.to_string())?;
let existing = open.into_iter().next();
let existing_submission_state = existing
.as_ref()
.and_then(|submission| submission.attributes.state.clone());
let existing_submission_has_items = match &existing {
Some(submission) => !client
.list_review_submission_items(&submission.id)
.await
.map_err(|error| error.to_string())?
.is_empty(),
None => false,
};
let blocked = version
.attributes
.app_version_state
.as_deref()
.and_then(blocked_reason);
Ok(SubmissionPlan {
blocked,
app_id: app_id.to_string(),
app_store_version_id: app_store_version_id.to_string(),
version_string: version.attributes.version_string,
platform: platform_name(platform).to_string(),
app_version_state: version.attributes.app_version_state,
existing_submission_id: existing.map(|submission| submission.id),
existing_submission_state,
existing_submission_has_items,
})
}
pub async fn submit_for_review(
client: &Client,
plan: SubmissionPlan,
) -> Result<SubmittedReview, String> {
if let Some(reason) = &plan.blocked {
return Err(reason.clone());
}
let platform = platform_from_name(&plan.platform)?;
let submission_id = match &plan.existing_submission_id {
Some(id) => id.clone(),
None => {
client
.create_review_submission(&plan.app_id, platform)
.await
.map_err(|error| error.to_string())?
.id
}
};
if !plan.existing_submission_has_items {
client
.add_app_store_version_to_review_submission(&submission_id, &plan.app_store_version_id)
.await
.map_err(|error| error.to_string())?;
}
let submitted = client
.submit_review_submission(&submission_id)
.await
.map_err(|error| error.to_string())?;
Ok(SubmittedReview {
submission_id: submitted.id,
state: submitted.attributes.state,
submitted_date: submitted.attributes.submitted_date,
plan,
})
}
fn blocked_reason(state: &str) -> Option<String> {
const SUBMITTED_OR_LIVE: [&str; 9] = [
"WAITING_FOR_REVIEW",
"IN_REVIEW",
"PENDING_APPLE_RELEASE",
"PENDING_DEVELOPER_RELEASE",
"PROCESSING_FOR_DISTRIBUTION",
"READY_FOR_DISTRIBUTION",
"READY_FOR_SALE",
"ACCEPTED",
"REPLACED_WITH_NEW_VERSION",
];
SUBMITTED_OR_LIVE.contains(&state).then(|| {
format!(
"this version is {state}, so it has already been submitted; \
`review-submissions list` shows its submission, and \
`review-submissions cancel` withdraws one still waiting for review"
)
})
}
fn platform_name(platform: Platform) -> &'static str {
match platform {
Platform::Ios => "IOS",
Platform::MacOs => "MAC_OS",
Platform::TvOs => "TV_OS",
Platform::VisionOs => "VISION_OS",
}
}
fn platform_from_name(name: &str) -> Result<Platform, String> {
match name {
"IOS" => Ok(Platform::Ios),
"MAC_OS" => Ok(Platform::MacOs),
"TV_OS" => Ok(Platform::TvOs),
"VISION_OS" => Ok(Platform::VisionOs),
other => Err(format!("unknown platform {other:?} in submission plan")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_a_version_that_was_already_submitted() {
assert!(blocked_reason("WAITING_FOR_REVIEW").is_some());
assert!(blocked_reason("READY_FOR_DISTRIBUTION").is_some());
assert_eq!(blocked_reason("PREPARE_FOR_SUBMISSION"), None);
assert_eq!(blocked_reason("DEVELOPER_REJECTED"), None);
assert_eq!(blocked_reason("REJECTED"), None);
}
#[test]
fn platform_names_round_trip() {
for platform in [
Platform::Ios,
Platform::MacOs,
Platform::TvOs,
Platform::VisionOs,
] {
assert_eq!(platform_from_name(platform_name(platform)), Ok(platform));
}
assert!(platform_from_name("WATCH_OS").is_err());
}
}