cnctd_appstore 0.1.0

App Store Connect API client for TestFlight, builds, and submissions
Documentation
use anyhow::{anyhow, Result};
use serde::Deserialize;
use serde_json::json;

use crate::client::{AppStoreConnectClient, AscResponse, BASE_URL};

/// App Store version data
#[derive(Debug, Deserialize, Clone)]
pub struct AppStoreVersionData {
    pub id: String,
    #[serde(rename = "type")]
    pub data_type: String,
    pub attributes: AppStoreVersionAttributes,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreVersionAttributes {
    pub version_string: Option<String>,
    pub app_store_state: Option<String>,
    pub platform: Option<String>,
    pub release_type: Option<String>,
}

/// App Store version submission data
#[derive(Debug, Deserialize, Clone)]
pub struct AppStoreVersionSubmissionData {
    pub id: String,
    #[serde(rename = "type")]
    pub data_type: String,
}

impl AppStoreConnectClient {
    /// Create a new App Store version for submission
    ///
    /// `platform` should be "IOS", "MAC_OS", "TV_OS", or "VISION_OS"
    pub async fn create_app_store_version(
        &self,
        app_id: &str,
        version_string: &str,
        platform: &str,
    ) -> Result<AppStoreVersionData> {
        let token = self.generate_token()?;

        let body = json!({
            "data": {
                "type": "appStoreVersions",
                "attributes": {
                    "versionString": version_string,
                    "platform": platform,
                    "releaseType": "MANUAL"
                },
                "relationships": {
                    "app": {
                        "data": {
                            "type": "apps",
                            "id": app_id
                        }
                    }
                }
            }
        });

        let response = self.client
            .post(format!("{}/appStoreVersions", BASE_URL))
            .header("Authorization", format!("Bearer {}", token))
            .header("Content-Type", "application/json")
            .json(&body)
            .send()
            .await
            .map_err(|e| anyhow!("Failed to create App Store version: {}", e))?;

        let status = response.status();
        if status.is_success() {
            let result: AscResponse<AppStoreVersionData> = response.json().await
                .map_err(|e| anyhow!("Failed to parse version response: {}", e))?;
            log::info!("Created App Store version {} for app {}", version_string, app_id);
            Ok(result.data)
        } else {
            let error_body = response.text().await.unwrap_or_default();
            Err(anyhow!("{}", Self::parse_error(status, &error_body)))
        }
    }

    /// Submit an App Store version for review
    pub async fn submit_for_review(&self, version_id: &str) -> Result<AppStoreVersionSubmissionData> {
        let token = self.generate_token()?;

        let body = json!({
            "data": {
                "type": "appStoreVersionSubmissions",
                "relationships": {
                    "appStoreVersion": {
                        "data": {
                            "type": "appStoreVersions",
                            "id": version_id
                        }
                    }
                }
            }
        });

        let response = self.client
            .post(format!("{}/appStoreVersionSubmissions", BASE_URL))
            .header("Authorization", format!("Bearer {}", token))
            .header("Content-Type", "application/json")
            .json(&body)
            .send()
            .await
            .map_err(|e| anyhow!("Failed to submit for review: {}", e))?;

        let status = response.status();
        if status.is_success() {
            let result: AscResponse<AppStoreVersionSubmissionData> = response.json().await
                .map_err(|e| anyhow!("Failed to parse submission response: {}", e))?;
            log::info!("Submitted version {} for review", version_id);
            Ok(result.data)
        } else {
            let error_body = response.text().await.unwrap_or_default();
            Err(anyhow!("{}", Self::parse_error(status, &error_body)))
        }
    }
}