//! Google Play Developer API publisher. //! //! Models the Google Play Developer API calls. Stubs the actual HTTP. //! //! API reference: https://developers.google.com/android-publisher use crate::{ config::{GoogleConfig, PublishConfig}, metadata::StoreMetadata, PublishError, PublishOutcome, PublishResult, }; /// Publishes app bundles to Google Play Store. pub struct GooglePublisher { pub google_config: GoogleConfig, pub publish_config: PublishConfig, } impl GooglePublisher { pub fn new(publish_config: PublishConfig) -> PublishResult { let google_config = publish_config.google.clone().ok_or_else(|| { PublishError::Config("no [publish.google] section in el.toml".into()) })?; Ok(Self { google_config, publish_config }) } /// Create a new edit session on Google Play. /// /// In production: POST https://androidpublisher.googleapis.com/v3/applications/{packageName}/edits pub fn create_edit(&self) -> PublishResult { // TODO: OAuth2 authentication via service account JSON let edit_id = format!("edit-{}", self.publish_config.build_number); Ok(edit_id) } /// Upload an AAB (Android App Bundle) to an edit session. /// /// In production: POST .../edits/{editId}/bundles (multipart upload) pub fn upload_bundle( &self, edit_id: &str, aab_path: &str, ) -> PublishResult { let _ = (edit_id, aab_path); // Returns the version code of the uploaded bundle Ok(self.publish_config.build_number) } /// Assign a bundle to a track. /// /// In production: PUT .../edits/{editId}/tracks/{track} pub fn assign_to_track( &self, edit_id: &str, version_code: u32, track: &str, rollout_fraction: f64, ) -> PublishResult<()> { let _ = (edit_id, version_code, track, rollout_fraction); if !matches!(track, "internal" | "alpha" | "beta" | "production") { return Err(PublishError::Config(format!( "unknown Google Play track: '{}'. Use internal/alpha/beta/production", track ))); } Ok(()) } /// Upload store listing (metadata) for a locale. /// /// In production: PATCH .../edits/{editId}/listings/{language} pub fn upload_listing( &self, edit_id: &str, locale: &str, metadata: &StoreMetadata, ) -> PublishResult<()> { let _ = (edit_id, locale, metadata); Ok(()) } /// Commit an edit (makes the changes live). /// /// In production: POST .../edits/{editId}:commit pub fn commit_edit(&self, edit_id: &str) -> PublishResult { // Returns the resulting version code Ok(format!("{}-committed", edit_id)) } /// Update rollout percentage for a track (for staged rollouts). pub fn update_rollout(&self, track: &str, percent: u8) -> PublishResult<()> { if percent > 100 { return Err(PublishError::Config(format!( "rollout percent {} exceeds 100", percent ))); } let _ = (track, percent); // TODO: PATCH .../tracks/{track} with rollout fraction Ok(()) } /// Full publish flow: create edit → upload bundle → assign to track → commit. pub fn publish(&self, aab_path: &str) -> PublishResult { let metadata = StoreMetadata::load_from_dir(&self.publish_config.metadata_dir)?; let edit_id = self.create_edit()?; let version_code = self.upload_bundle(&edit_id, aab_path)?; let track = &self.google_config.track; let rollout_fraction = self .publish_config .rollout .as_ref() .map(|r| r.initial_percent as f64 / 100.0) .unwrap_or(1.0); self.assign_to_track(&edit_id, version_code, track, rollout_fraction)?; self.upload_listing(&edit_id, "en-US", &metadata)?; let commit_id = self.commit_edit(&edit_id)?; Ok(PublishOutcome::new("google", &self.publish_config, track) .with_submission_id(commit_id)) } }