Skip to main content

egs_api/
lib.rs

1#![deny(missing_docs)]
2#![cfg_attr(test, deny(warnings))]
3
4//! # Epic Games Store API
5//!
6//! Async Rust client for the Epic Games Store API. Provides authentication,
7//! asset management, download manifest parsing (binary + JSON), and
8//! [Fab](https://www.fab.com/) marketplace integration.
9//!
10//! # Quick Start
11//!
12//! ```rust,no_run
13//! use egs_api::EpicGames;
14//!
15//! #[tokio::main]
16//! async fn main() {
17//!     let mut egs = EpicGames::new();
18//!
19//!     // Authenticate with an authorization code
20//!     let code = "your_authorization_code".to_string();
21//!     if egs.auth_code(None, Some(code)).await {
22//!         println!("Logged in as {}", egs.user_details().display_name.unwrap_or_default());
23//!     }
24//!
25//!     // List all owned assets
26//!     let assets = egs.list_assets(None, None).await;
27//!     println!("You own {} assets", assets.len());
28//! }
29//! ```
30//!
31//! # Authentication
32//!
33//! Epic uses OAuth2 with a public launcher client ID. The flow is:
34//!
35//! 1. Open the [authorization URL] in a browser — the user logs in and gets
36//!    redirected to a JSON page with an `authorizationCode`.
37//! 2. Pass that code to [`EpicGames::auth_code`].
38//! 3. Persist the session with [`EpicGames::user_details`] (implements
39//!    `Serialize` / `Deserialize`).
40//! 4. Restore it later with [`EpicGames::set_user_details`] +
41//!    [`EpicGames::login`], which uses the refresh token.
42//!
43//! [authorization URL]: https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect%3FclientId%3D34a02cf8f4414e29b15921876da36f9a%26responseType%3Dcode
44//!
45//! # Features
46//!
47//! - **Assets** — List owned assets, fetch catalog metadata (with DLC trees),
48//!   retrieve asset manifests with CDN download URLs.
49//! - **Download Manifests** — Parse Epic's binary and JSON manifest formats.
50//!   Exposes file lists, chunk hashes, and custom fields for download
51//!   reconstruction.
52//! - **Fab Marketplace** — List Fab library items, fetch signed asset manifests,
53//!   and download manifests from distribution points.
54//! - **Account** — Details, bulk ID lookup, friends list.
55//! - **Entitlements** — Games, DLC, subscriptions.
56//! - **Library** — Paginated listing with optional metadata.
57//! - **Tokens** — Game exchange tokens and per-asset ownership tokens (JWT).
58//!
59//! # Architecture
60//!
61//! [`EpicGames`] is the public facade. It wraps an internal `EpicAPI` struct
62//! that holds the `reqwest::Client` (with cookie store) and session state.
63//! Most public methods return `Option<T>` or `Vec<T>`, swallowing transport
64//! errors for convenience. Fab methods return `Result<T, EpicAPIError>` to
65//! expose timeout/error distinctions.
66//!
67//! # Examples
68//!
69//! The crate ships with examples covering every endpoint. See the
70//! [`examples/`](https://github.com/AchetaGames/egs-api-rs/tree/master/examples)
71//! directory or run:
72//!
73//! ```bash
74//! cargo run --example auth                # Interactive login + token persistence
75//! cargo run --example account             # Account details, ID lookup, friends, external auths, SSO
76//! cargo run --example entitlements        # List all entitlements
77//! cargo run --example library             # Paginated library listing
78//! cargo run --example assets              # Full pipeline: list → info → manifest → download
79//! cargo run --example game_token          # Exchange code + ownership token
80//! cargo run --example fab                 # Fab library → asset manifest → download manifest
81//! cargo run --example catalog             # Catalog items, offers, bulk lookup
82//! cargo run --example commerce            # Currencies, prices, billing, quick purchase
83//! cargo run --example status              # Service status (lightswitch API)
84//! cargo run --example presence            # Update online presence
85//! cargo run --example client_credentials  # App-level auth + library state tokens
86//! ```
87
88use crate::api::types::account::{AccountData, AccountInfo, ExternalAuth, UserData};
89use crate::api::types::epic_asset::EpicAsset;
90use crate::api::types::fab_asset_manifest::DownloadInfo;
91use crate::api::types::friends::Friend;
92use crate::api::{EpicAPI};
93
94use api::types::asset_info::{AssetInfo, GameToken};
95use api::types::asset_manifest::AssetManifest;
96use api::types::artifact_service::ArtifactServiceTicket;
97use api::types::billing_account::BillingAccount;
98use api::types::catalog_item::CatalogItemPage;
99use api::types::catalog_offer::CatalogOfferPage;
100use api::types::cloud_save::CloudSaveResponse;
101use api::types::currency::CurrencyPage;
102use api::types::download_manifest::DownloadManifest;
103use api::types::entitlement::Entitlement;
104use api::types::library::Library;
105use api::types::presence::PresenceUpdate;
106use api::types::price::PriceResponse;
107use api::types::quick_purchase::QuickPurchaseResponse;
108use api::types::service_status::ServiceStatus;
109use api::types::uplay::{
110    UplayClaimResult, UplayCodesResult, UplayGraphQLResponse, UplayRedeemResult,
111};
112use api::types::cosmos;
113use api::types::engine_blob;
114use api::types::fab_search;
115use log::{error, info, warn};
116use crate::api::error::EpicAPIError;
117
118/// Module for authenticated API communication
119pub mod api;
120
121/// Client for the Epic Games Store API.
122///
123/// This is the main entry point for the library. Create an instance with
124/// [`EpicGames::new`], authenticate with [`EpicGames::auth_code`] or
125/// [`EpicGames::login`], then call API methods.
126///
127/// Most methods return `Option<T>` or `Vec<T>`, returning `None` / empty on
128/// errors. Fab methods return `Result<T, EpicAPIError>` for richer error
129/// handling (e.g., distinguishing timeouts from other failures).
130///
131/// Session state is stored in [`UserData`], which implements
132/// `Serialize` / `Deserialize` for persistence across runs.
133#[derive(Debug, Clone)]
134pub struct EpicGames {
135    egs: EpicAPI,
136}
137
138impl Default for EpicGames {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl EpicGames {
145    /// Creates a new [`EpicGames`] client.
146    pub fn new() -> Self {
147        EpicGames {
148            egs: EpicAPI::new(),
149        }
150    }
151
152    /// Check whether the user is logged in.
153    ///
154    /// Returns `true` if the access token exists and has more than 600 seconds
155    /// remaining before expiry.
156    pub fn is_logged_in(&self) -> bool {
157        if let Some(exp) = self.egs.user_data.expires_at {
158            let now = chrono::offset::Utc::now();
159            let td = exp - now;
160            if td.num_seconds() > 600 {
161                return true;
162            }
163        }
164        false
165    }
166
167    /// Returns a clone of the current session state.
168    ///
169    /// The returned [`UserData`] implements `Serialize` / `Deserialize`,
170    /// so you can persist it to disk and restore it later with
171    /// [`set_user_details`](Self::set_user_details).
172    pub fn user_details(&self) -> UserData {
173        self.egs.user_data.clone()
174    }
175
176    /// Restore session state from a previously saved [`UserData`].
177    ///
178    /// Only merges `Some` fields — existing values are preserved for any
179    /// field that is `None` in the input. Call [`login`](Self::login)
180    /// afterward to refresh the access token.
181    pub fn set_user_details(&mut self, user_details: UserData) {
182        self.egs.user_data.update(user_details);
183    }
184
185    /// Like [`auth_code`](Self::auth_code), but returns a `Result` instead of swallowing errors.
186    pub async fn try_auth_code(
187        &mut self,
188        exchange_token: Option<String>,
189        authorization_code: Option<String>,
190    ) -> Result<bool, EpicAPIError> {
191        self.egs
192            .start_session(exchange_token, authorization_code)
193            .await
194    }
195
196    /// Authenticate with an authorization code or exchange token.
197    ///
198    /// Returns `true` on success, `false` on failure. Returns `None` on API errors.
199    pub async fn auth_code(
200        &mut self,
201        exchange_token: Option<String>,
202        authorization_code: Option<String>,
203    ) -> bool {
204        self.try_auth_code(exchange_token, authorization_code)
205            .await
206            .unwrap_or(false)
207    }
208
209    /// Invalidate the current session and log out.
210    pub async fn logout(&mut self) -> bool {
211        self.egs.invalidate_sesion().await
212    }
213
214    /// Like [`login`](Self::login), but returns a `Result` instead of swallowing errors.
215    pub async fn try_login(&mut self) -> Result<bool, EpicAPIError> {
216        if let Some(exp) = self.egs.user_data.expires_at {
217            let now = chrono::offset::Utc::now();
218            let td = exp - now;
219            if td.num_seconds() > 600 {
220                info!("Trying to re-use existing login session... ");
221                let resumed = self.egs.resume_session().await.map_err(|e| {
222                    warn!("{}", e);
223                    e
224                })?;
225                if resumed {
226                    info!("Logged in");
227                    return Ok(true);
228                }
229                return Ok(false);
230            }
231        }
232        info!("Logging in...");
233        if let Some(exp) = self.egs.user_data.refresh_expires_at {
234            let now = chrono::offset::Utc::now();
235            let td = exp - now;
236            if td.num_seconds() > 600 {
237                let started = self.egs.start_session(None, None).await.map_err(|e| {
238                    error!("{}", e);
239                    e
240                })?;
241                if started {
242                    info!("Logged in");
243                    return Ok(true);
244                }
245                return Ok(false);
246            }
247        }
248        Ok(false)
249    }
250
251    /// Resume session using the saved refresh token.
252    ///
253    /// Returns `true` on success, `false` if the refresh token has expired or is invalid.
254    /// Unlike [`try_login`](Self::try_login), this method falls through to
255    /// refresh-token login if session resume fails.
256    pub async fn login(&mut self) -> bool {
257        if let Some(exp) = self.egs.user_data.expires_at {
258            let now = chrono::offset::Utc::now();
259            let td = exp - now;
260            if td.num_seconds() > 600 {
261                info!("Trying to re-use existing login session... ");
262                match self.egs.resume_session().await {
263                    Ok(b) => {
264                        if b {
265                            info!("Logged in");
266                            return true;
267                        }
268                        return false;
269                    }
270                    Err(e) => {
271                        warn!("{}", e)
272                    }
273                };
274            }
275        }
276        info!("Logging in...");
277        if let Some(exp) = self.egs.user_data.refresh_expires_at {
278            let now = chrono::offset::Utc::now();
279            let td = exp - now;
280            if td.num_seconds() > 600 {
281                match self.egs.start_session(None, None).await {
282                    Ok(b) => {
283                        if b {
284                            info!("Logged in");
285                            return true;
286                        }
287                        return false;
288                    }
289                    Err(e) => {
290                        error!("{}", e)
291                    }
292                }
293            }
294        }
295        false
296    }
297
298    /// Like [`list_assets`](Self::list_assets), but returns a `Result` instead of swallowing errors.
299    pub async fn try_list_assets(
300        &mut self,
301        platform: Option<String>,
302        label: Option<String>,
303    ) -> Result<Vec<EpicAsset>, EpicAPIError> {
304        self.egs.assets(platform, label).await
305    }
306
307    /// List all owned assets.
308    ///
309    /// Defaults to platform="Windows" and label="Live" if not specified.
310    /// Returns empty `Vec` on API errors.
311    pub async fn list_assets(
312        &mut self,
313        platform: Option<String>,
314        label: Option<String>,
315    ) -> Vec<EpicAsset> {
316        self.try_list_assets(platform, label)
317            .await
318            .unwrap_or_else(|_| Vec::new())
319    }
320
321    /// Like [`asset_manifest`](Self::asset_manifest), but returns a `Result` instead of swallowing errors.
322    pub async fn try_asset_manifest(
323        &mut self,
324        platform: Option<String>,
325        label: Option<String>,
326        namespace: Option<String>,
327        item_id: Option<String>,
328        app: Option<String>,
329    ) -> Result<AssetManifest, EpicAPIError> {
330        self.egs
331            .asset_manifest(platform, label, namespace, item_id, app)
332            .await
333    }
334
335    /// Fetch asset manifest with CDN download URLs.
336    ///
337    /// Defaults to platform="Windows" and label="Live" if not specified.
338    /// Returns `None` on API errors.
339    pub async fn asset_manifest(
340        &mut self,
341        platform: Option<String>,
342        label: Option<String>,
343        namespace: Option<String>,
344        item_id: Option<String>,
345        app: Option<String>,
346    ) -> Option<AssetManifest> {
347        self.try_asset_manifest(platform, label, namespace, item_id, app)
348            .await
349            .ok()
350    }
351
352    /// Fetch Fab asset manifest with signed distribution points.
353    ///
354    /// Returns `Result` to expose timeout errors (403 → `EpicAPIError::FabTimeout`).
355    pub async fn fab_asset_manifest(
356        &self,
357        artifact_id: &str,
358        namespace: &str,
359        asset_id: &str,
360        platform: Option<&str>,
361    ) -> Result<Vec<DownloadInfo>, EpicAPIError> {
362        match self
363            .egs
364            .fab_asset_manifest(artifact_id, namespace, asset_id, platform)
365            .await
366        {
367            Ok(a) => Ok(a),
368            Err(e) => Err(e),
369        }
370    }
371
372    /// Like [`asset_info`](Self::asset_info), but returns a `Result` instead of swallowing errors.
373    pub async fn try_asset_info(
374        &mut self,
375        asset: &EpicAsset,
376    ) -> Result<Option<AssetInfo>, EpicAPIError> {
377        let mut info = self.egs.asset_info(asset).await?;
378        Ok(info.remove(asset.catalog_item_id.as_str()))
379    }
380
381    /// Fetch catalog metadata for an asset (includes DLC tree).
382    ///
383    /// Returns `None` on API errors.
384    pub async fn asset_info(&mut self, asset: &EpicAsset) -> Option<AssetInfo> {
385        self.try_asset_info(asset).await.ok().flatten()
386    }
387
388    /// Like [`account_details`](Self::account_details), but returns a `Result` instead of swallowing errors.
389    pub async fn try_account_details(&mut self) -> Result<AccountData, EpicAPIError> {
390        self.egs.account_details().await
391    }
392
393    /// Fetch account details (email, display name, country, 2FA status).
394    ///
395    /// Returns `None` on API errors.
396    pub async fn account_details(&mut self) -> Option<AccountData> {
397        self.try_account_details().await.ok()
398    }
399
400    /// Like [`account_ids_details`](Self::account_ids_details), but returns a `Result` instead of swallowing errors.
401    pub async fn try_account_ids_details(
402        &mut self,
403        ids: Vec<String>,
404    ) -> Result<Vec<AccountInfo>, EpicAPIError> {
405        self.egs.account_ids_details(ids).await
406    }
407
408    /// Bulk lookup of account IDs to display names.
409    ///
410    /// Returns `None` on API errors.
411    pub async fn account_ids_details(&mut self, ids: Vec<String>) -> Option<Vec<AccountInfo>> {
412        self.try_account_ids_details(ids).await.ok()
413    }
414
415    /// Like [`account_friends`](Self::account_friends), but returns a `Result` instead of swallowing errors.
416    pub async fn try_account_friends(
417        &mut self,
418        include_pending: bool,
419    ) -> Result<Vec<Friend>, EpicAPIError> {
420        self.egs.account_friends(include_pending).await
421    }
422
423    /// Fetch friends list (including pending requests if `include_pending` is true).
424    ///
425    /// Returns `None` on API errors.
426    pub async fn account_friends(&mut self, include_pending: bool) -> Option<Vec<Friend>> {
427        self.try_account_friends(include_pending).await.ok()
428    }
429
430    /// Like [`game_token`](Self::game_token), but returns a `Result` instead of swallowing errors.
431    pub async fn try_game_token(&mut self) -> Result<GameToken, EpicAPIError> {
432        self.egs.game_token().await
433    }
434
435    /// Fetch a short-lived exchange code for game launches.
436    ///
437    /// Returns `None` on API errors.
438    pub async fn game_token(&mut self) -> Option<GameToken> {
439        self.try_game_token().await.ok()
440    }
441
442    /// Like [`ownership_token`](Self::ownership_token), but returns a `Result` instead of swallowing errors.
443    pub async fn try_ownership_token(&mut self, asset: &EpicAsset) -> Result<String, EpicAPIError> {
444        self.egs.ownership_token(asset).await.map(|a| a.token)
445    }
446
447    /// Fetch a JWT proving ownership of an asset.
448    ///
449    /// Returns `None` on API errors.
450    pub async fn ownership_token(&mut self, asset: &EpicAsset) -> Option<String> {
451        self.try_ownership_token(asset).await.ok()
452    }
453
454    /// Like [`user_entitlements`](Self::user_entitlements), but returns a `Result` instead of swallowing errors.
455    pub async fn try_user_entitlements(&mut self) -> Result<Vec<Entitlement>, EpicAPIError> {
456        self.egs.user_entitlements().await
457    }
458
459    /// Fetch all user entitlements (games, DLC, subscriptions).
460    ///
461    /// Returns empty `Vec` on API errors.
462    pub async fn user_entitlements(&mut self) -> Vec<Entitlement> {
463        self.try_user_entitlements().await.unwrap_or_else(|_| Vec::new())
464    }
465
466    /// Like [`library_items`](Self::library_items), but returns a `Result` instead of swallowing errors.
467    pub async fn try_library_items(&mut self, include_metadata: bool) -> Result<Library, EpicAPIError> {
468        self.egs.library_items(include_metadata).await
469    }
470
471    /// Fetch the user library with optional metadata.
472    ///
473    /// Paginates internally and returns all records at once. Returns `None` on API errors.
474    pub async fn library_items(&mut self, include_metadata: bool) -> Option<Library> {
475        self.try_library_items(include_metadata).await.ok()
476    }
477
478    /// Like [`fab_library_items`](Self::fab_library_items), but returns a `Result` instead of swallowing errors.
479    pub async fn try_fab_library_items(
480        &mut self,
481        account_id: String,
482    ) -> Result<api::types::fab_library::FabLibrary, EpicAPIError> {
483        self.egs.fab_library_items(account_id).await
484    }
485
486    /// Fetch the user Fab library.
487    ///
488    /// Paginates internally and returns all records at once. Returns `None` on API errors.
489    pub async fn fab_library_items(
490        &mut self,
491        account_id: String,
492    ) -> Option<api::types::fab_library::FabLibrary> {
493        self.try_fab_library_items(account_id).await.ok()
494    }
495
496    /// Parse download manifests from all CDN mirrors.
497    ///
498    /// Fetches from all mirrors, parses binary/JSON format, and populates custom fields
499    /// (BaseUrl, CatalogItemId, etc.). Returns empty `Vec` on API errors.
500    pub async fn asset_download_manifests(&self, manifest: AssetManifest) -> Vec<DownloadManifest> {
501        self.egs.asset_download_manifests(manifest).await
502    }
503
504    /// Parse a Fab download manifest from a specific distribution point.
505    ///
506    /// Checks signature expiration before fetching. Returns `Result` to expose timeout errors.
507    pub async fn fab_download_manifest(
508        &self,
509        download_info: DownloadInfo,
510        distribution_point_url: &str,
511    ) -> Result<DownloadManifest, EpicAPIError> {
512        self.egs
513            .fab_download_manifest(download_info, distribution_point_url)
514            .await
515    }
516
517    /// Like [`auth_client_credentials`](Self::auth_client_credentials), but returns a `Result` instead of swallowing errors.
518    pub async fn try_auth_client_credentials(&mut self) -> Result<bool, EpicAPIError> {
519        self.egs.start_client_credentials_session().await
520    }
521
522    /// Authenticate with client credentials (app-level, no user context).
523    ///
524    /// Uses the launcher's public client ID/secret to obtain an access token
525    /// without any user interaction. The resulting session has limited
526    /// permissions — it can query public endpoints (catalog, service status,
527    /// currencies) but cannot access user-specific data (library, entitlements).
528    ///
529    /// Returns `true` on success, `false` on failure.
530    pub async fn auth_client_credentials(&mut self) -> bool {
531        self.try_auth_client_credentials().await.unwrap_or(false)
532    }
533
534    /// Like [`external_auths`](Self::external_auths), but returns a `Result` instead of swallowing errors.
535    pub async fn try_external_auths(&self, account_id: &str) -> Result<Vec<ExternalAuth>, EpicAPIError> {
536        self.egs.external_auths(account_id).await
537    }
538
539    /// Fetch external auth connections linked to an account.
540    ///
541    /// Returns linked platform accounts (Steam, PSN, Xbox, Nintendo, etc.)
542    /// with external display names and account IDs. Requires a valid user session.
543    ///
544    /// Returns `None` on API errors.
545    pub async fn external_auths(&self, account_id: &str) -> Option<Vec<ExternalAuth>> {
546        self.try_external_auths(account_id).await.ok()
547    }
548
549    /// Like [`sso_domains`](Self::sso_domains), but returns a `Result` instead of swallowing errors.
550    pub async fn try_sso_domains(&self) -> Result<Vec<String>, EpicAPIError> {
551        self.egs.sso_domains().await
552    }
553
554    /// Fetch the list of SSO (Single Sign-On) domains.
555    ///
556    /// Returns domain strings that support Epic's SSO flow. Used by the
557    /// launcher to determine which domains can share authentication cookies.
558    ///
559    /// Returns `None` on API errors.
560    pub async fn sso_domains(&self) -> Option<Vec<String>> {
561        self.try_sso_domains().await.ok()
562    }
563
564    /// Like [`catalog_items`](Self::catalog_items), but returns a `Result` instead of swallowing errors.
565    pub async fn try_catalog_items(
566        &self,
567        namespace: &str,
568        start: i64,
569        count: i64,
570    ) -> Result<CatalogItemPage, EpicAPIError> {
571        self.egs.catalog_items(namespace, start, count).await
572    }
573
574    /// Fetch paginated catalog items for a namespace.
575    ///
576    /// Queries the Epic catalog service for items within a given namespace
577    /// (e.g., a game's namespace). Results are paginated — use `start` and
578    /// `count` to page through. Each [`CatalogItemPage`] includes a `paging`
579    /// field with the total count.
580    ///
581    /// Returns `None` on API errors.
582    pub async fn catalog_items(
583        &self,
584        namespace: &str,
585        start: i64,
586        count: i64,
587    ) -> Option<CatalogItemPage> {
588        self.try_catalog_items(namespace, start, count).await.ok()
589    }
590
591    /// Like [`catalog_offers`](Self::catalog_offers), but returns a `Result` instead of swallowing errors.
592    pub async fn try_catalog_offers(
593        &self,
594        namespace: &str,
595        start: i64,
596        count: i64,
597    ) -> Result<CatalogOfferPage, EpicAPIError> {
598        self.egs.catalog_offers(namespace, start, count).await
599    }
600
601    /// Fetch paginated catalog offers for a namespace.
602    ///
603    /// Queries the Epic catalog service for offers (purchasable items) within
604    /// a namespace. Offers include pricing metadata, seller info, and linked
605    /// catalog items. Use `start` and `count` to paginate.
606    ///
607    /// Returns `None` on API errors.
608    pub async fn catalog_offers(
609        &self,
610        namespace: &str,
611        start: i64,
612        count: i64,
613    ) -> Option<CatalogOfferPage> {
614        self.try_catalog_offers(namespace, start, count).await.ok()
615    }
616
617    /// Like [`bulk_catalog_items`](Self::bulk_catalog_items), but returns a `Result` instead of swallowing errors.
618    pub async fn try_bulk_catalog_items(
619        &self,
620        items: &[(&str, &str)],
621    ) -> Result<std::collections::HashMap<String, std::collections::HashMap<String, AssetInfo>>, EpicAPIError> {
622        self.egs.bulk_catalog_items(items).await
623    }
624
625    /// Bulk fetch catalog items across multiple namespaces.
626    ///
627    /// Accepts a slice of `(namespace, item_id)` pairs and returns them grouped
628    /// by namespace → item_id → [`AssetInfo`]. Useful for resolving catalog
629    /// metadata for items from different games in a single request.
630    ///
631    /// Returns `None` on API errors.
632    pub async fn bulk_catalog_items(
633        &self,
634        items: &[(&str, &str)],
635    ) -> Option<std::collections::HashMap<String, std::collections::HashMap<String, AssetInfo>>> {
636        self.try_bulk_catalog_items(items).await.ok()
637    }
638
639    /// Like [`currencies`](Self::currencies), but returns a `Result` instead of swallowing errors.
640    pub async fn try_currencies(&self, start: i64, count: i64) -> Result<CurrencyPage, EpicAPIError> {
641        self.egs.currencies(start, count).await
642    }
643
644    /// Fetch available currencies from the Epic catalog.
645    ///
646    /// Returns paginated currency definitions including code, symbol, and
647    /// decimal precision. Use `start` and `count` to paginate.
648    ///
649    /// Returns `None` on API errors.
650    pub async fn currencies(&self, start: i64, count: i64) -> Option<CurrencyPage> {
651        self.try_currencies(start, count).await.ok()
652    }
653
654    /// Like [`library_state_token_status`](Self::library_state_token_status), but returns a `Result` instead of swallowing errors.
655    pub async fn try_library_state_token_status(
656        &self,
657        token_id: &str,
658    ) -> Result<bool, EpicAPIError> {
659        self.egs.library_state_token_status(token_id).await
660    }
661
662    /// Check the validity of a library state token.
663    ///
664    /// Returns `Some(true)` if the token is still valid, `Some(false)` if
665    /// expired or invalid, or `None` on API errors. Library state tokens are
666    /// used to detect changes to the user's library since the last sync.
667    ///
668    /// Returns `None` on API errors.
669    pub async fn library_state_token_status(&self, token_id: &str) -> Option<bool> {
670        self.try_library_state_token_status(token_id).await.ok()
671    }
672
673    /// Like [`service_status`](Self::service_status), but returns a `Result` instead of swallowing errors.
674    pub async fn try_service_status(
675        &self,
676        service_id: &str,
677    ) -> Result<Vec<ServiceStatus>, EpicAPIError> {
678        self.egs.service_status(service_id).await
679    }
680
681    /// Fetch service status from Epic's lightswitch API.
682    ///
683    /// Returns the operational status of an Epic online service (e.g., a game's
684    /// backend). The response includes whether the service is UP/DOWN, any
685    /// maintenance message, and whether the current user is banned.
686    ///
687    /// Returns `None` on API errors.
688    pub async fn service_status(&self, service_id: &str) -> Option<Vec<ServiceStatus>> {
689        self.try_service_status(service_id).await.ok()
690    }
691
692    /// Like [`offer_prices`](Self::offer_prices), but returns a `Result` instead of swallowing errors.
693    pub async fn try_offer_prices(
694        &self,
695        namespace: &str,
696        offer_ids: &[String],
697        country: &str,
698    ) -> Result<PriceResponse, EpicAPIError> {
699        self.egs.offer_prices(namespace, offer_ids, country).await
700    }
701
702    /// Fetch offer prices from Epic's price engine.
703    ///
704    /// Queries current pricing for one or more offers within a namespace,
705    /// localized to a specific country. The response includes original price,
706    /// discount price, and pre-formatted display strings.
707    ///
708    /// Returns `None` on API errors.
709    pub async fn offer_prices(
710        &self,
711        namespace: &str,
712        offer_ids: &[String],
713        country: &str,
714    ) -> Option<PriceResponse> {
715        self.try_offer_prices(namespace, offer_ids, country).await.ok()
716    }
717
718    /// Like [`quick_purchase`](Self::quick_purchase), but returns a `Result` instead of swallowing errors.
719    pub async fn try_quick_purchase(
720        &self,
721        namespace: &str,
722        offer_id: &str,
723    ) -> Result<QuickPurchaseResponse, EpicAPIError> {
724        self.egs.quick_purchase(namespace, offer_id).await
725    }
726
727    /// Execute a quick purchase (typically for free game claims).
728    ///
729    /// Initiates a purchase order for a free offer. The response contains the
730    /// order ID and its processing status. For paid offers, use the full
731    /// checkout flow in the Epic Games launcher instead.
732    ///
733    /// Returns `None` on API errors.
734    pub async fn quick_purchase(
735        &self,
736        namespace: &str,
737        offer_id: &str,
738    ) -> Option<QuickPurchaseResponse> {
739        self.try_quick_purchase(namespace, offer_id).await.ok()
740    }
741
742    /// Like [`billing_account`](Self::billing_account), but returns a `Result` instead of swallowing errors.
743    pub async fn try_billing_account(&self) -> Result<BillingAccount, EpicAPIError> {
744        self.egs.billing_account().await
745    }
746
747    /// Fetch the default billing account for payment processing.
748    ///
749    /// Returns the account's billing country, which is used to determine
750    /// regional pricing and payment availability.
751    ///
752    /// Returns `None` on API errors.
753    pub async fn billing_account(&self) -> Option<BillingAccount> {
754        self.try_billing_account().await.ok()
755    }
756
757    /// Update the user's presence status.
758    ///
759    /// Sends a PATCH request to update the user's online presence (e.g.,
760    /// "online", "away") and optionally set an activity with custom properties.
761    /// The `session_id` is the OAuth session token from login. Returns `Ok(())`
762    /// on success (204 No Content) or an [`EpicAPIError`] on failure.
763    pub async fn update_presence(
764        &self,
765        session_id: &str,
766        body: &PresenceUpdate,
767    ) -> Result<(), EpicAPIError> {
768        self.egs.update_presence(session_id, body).await
769    }
770
771    /// Like [`fab_file_download_info`](Self::fab_file_download_info), but returns a `Result` instead of swallowing errors.
772    pub async fn try_fab_file_download_info(
773        &self,
774        listing_id: &str,
775        format_id: &str,
776        file_id: &str,
777    ) -> Result<DownloadInfo, EpicAPIError> {
778        self.egs
779            .fab_file_download_info(listing_id, format_id, file_id)
780            .await
781    }
782
783    /// Fetch download info for a specific file within a Fab listing.
784    ///
785    /// Returns signed [`DownloadInfo`] for a single file identified by
786    /// `listing_id`, `format_id`, and `file_id`. Use this for targeted
787    /// downloads of individual files from a Fab asset rather than fetching
788    /// the entire asset manifest.
789    ///
790    /// Returns `None` on API errors.
791    pub async fn fab_file_download_info(
792        &self,
793        listing_id: &str,
794        format_id: &str,
795        file_id: &str,
796    ) -> Option<DownloadInfo> {
797        self.try_fab_file_download_info(listing_id, format_id, file_id)
798            .await
799            .ok()
800    }
801
802    // ── Cloud Saves ──
803
804    /// List cloud save files for the logged-in user.
805    ///
806    /// If `app_name` is provided, lists saves for that specific game.
807    /// If `manifests` is true (only relevant when `app_name` is set), lists manifest files.
808    pub async fn cloud_save_list(
809        &self,
810        app_name: Option<&str>,
811        manifests: bool,
812    ) -> Result<CloudSaveResponse, EpicAPIError> {
813        self.egs.cloud_save_list(app_name, manifests).await
814    }
815
816    /// Query cloud save files by specific filenames.
817    ///
818    /// Returns metadata including read/write links for the specified files.
819    pub async fn cloud_save_query(
820        &self,
821        app_name: &str,
822        filenames: &[String],
823    ) -> Result<CloudSaveResponse, EpicAPIError> {
824        self.egs.cloud_save_query(app_name, filenames).await
825    }
826
827    /// Delete a cloud save file by its storage path.
828    pub async fn cloud_save_delete(&self, path: &str) -> Result<(), EpicAPIError> {
829        self.egs.cloud_save_delete(path).await
830    }
831
832    // ── Artifact Service & Manifests ──
833
834    /// Fetch an artifact service ticket for manifest retrieval via EOS Helper.
835    ///
836    /// The `sandbox_id` is typically the game's namespace and `artifact_id`
837    /// is the app name. Returns a signed ticket for use with
838    /// [`game_manifest_by_ticket`](Self::game_manifest_by_ticket).
839    pub async fn artifact_service_ticket(
840        &self,
841        sandbox_id: &str,
842        artifact_id: &str,
843        label: Option<&str>,
844        platform: Option<&str>,
845    ) -> Result<ArtifactServiceTicket, EpicAPIError> {
846        self.egs
847            .artifact_service_ticket(sandbox_id, artifact_id, label, platform)
848            .await
849    }
850
851    /// Fetch a game manifest using a signed artifact service ticket.
852    ///
853    /// Alternative to [`asset_manifest`](Self::asset_manifest) using ticket-based
854    /// auth from the EOS Helper service.
855    pub async fn game_manifest_by_ticket(
856        &self,
857        artifact_id: &str,
858        signed_ticket: &str,
859        label: Option<&str>,
860        platform: Option<&str>,
861    ) -> Result<AssetManifest, EpicAPIError> {
862        self.egs
863            .game_manifest_by_ticket(artifact_id, signed_ticket, label, platform)
864            .await
865    }
866
867    /// Fetch launcher manifests for self-update checks.
868    pub async fn launcher_manifests(
869        &self,
870        platform: Option<&str>,
871        label: Option<&str>,
872    ) -> Result<AssetManifest, EpicAPIError> {
873        self.egs.launcher_manifests(platform, label).await
874    }
875
876    /// Try to fetch a delta manifest for optimized patching between builds.
877    ///
878    /// Returns `None` if no delta is available or the builds are identical.
879    pub async fn delta_manifest(
880        &self,
881        base_url: &str,
882        old_build_id: &str,
883        new_build_id: &str,
884    ) -> Option<Vec<u8>> {
885        self.egs
886            .delta_manifest(base_url, old_build_id, new_build_id)
887            .await
888    }
889
890    // ── SID Auth ──
891
892    /// Authenticate via session ID (SID) from the Epic web login flow.
893    ///
894    /// Performs the multi-step web exchange: set-sid → CSRF → exchange code,
895    /// then starts a session with the resulting code. Returns `true` on success.
896    pub async fn auth_sid(&mut self, sid: &str) -> Result<bool, EpicAPIError> {
897        self.egs.auth_sid(sid).await
898    }
899
900    // ── Uplay / Ubisoft Store ──
901
902    /// Fetch Uplay codes linked to the user's Epic account.
903    pub async fn store_get_uplay_codes(
904        &self,
905    ) -> Result<UplayGraphQLResponse<UplayCodesResult>, EpicAPIError> {
906        self.egs.store_get_uplay_codes().await
907    }
908
909    /// Claim a Uplay code for a specific game.
910    pub async fn store_claim_uplay_code(
911        &self,
912        uplay_account_id: &str,
913        game_id: &str,
914    ) -> Result<UplayGraphQLResponse<UplayClaimResult>, EpicAPIError> {
915        self.egs
916            .store_claim_uplay_code(uplay_account_id, game_id)
917            .await
918    }
919
920    /// Redeem all pending Uplay codes for the user's account.
921    pub async fn store_redeem_uplay_codes(
922        &self,
923        uplay_account_id: &str,
924    ) -> Result<UplayGraphQLResponse<UplayRedeemResult>, EpicAPIError> {
925        self.egs.store_redeem_uplay_codes(uplay_account_id).await
926    }
927
928    // --- Cosmos Session ---
929
930    /// Set up a Cosmos cookie session from an exchange code.
931    /// Typically called with a code from `game_token()`.
932    pub async fn cosmos_session_setup(
933        &self,
934        exchange_code: &str,
935    ) -> Result<cosmos::CosmosAuthResponse, EpicAPIError> {
936        self.egs.cosmos_session_setup(exchange_code).await
937    }
938
939    /// Upgrade bearer token to Cosmos session (step 5 of session setup).
940    pub async fn cosmos_auth_upgrade(
941        &self,
942    ) -> Result<cosmos::CosmosAuthResponse, EpicAPIError> {
943        self.egs.cosmos_auth_upgrade().await
944    }
945
946    /// Check if a EULA has been accepted. Returns `None` on error.
947    pub async fn cosmos_eula_check(&self, eula_id: &str, locale: &str) -> Option<bool> {
948        self.egs
949            .cosmos_eula_check(eula_id, locale)
950            .await
951            .ok()
952            .map(|r| r.accepted)
953    }
954
955    /// Check if a EULA has been accepted. Returns full `Result`.
956    pub async fn try_cosmos_eula_check(
957        &self,
958        eula_id: &str,
959        locale: &str,
960    ) -> Result<cosmos::CosmosEulaResponse, EpicAPIError> {
961        self.egs.cosmos_eula_check(eula_id, locale).await
962    }
963
964    /// Accept a EULA. Returns `None` on error.
965    pub async fn cosmos_eula_accept(
966        &self,
967        eula_id: &str,
968        locale: &str,
969        version: u32,
970    ) -> Option<bool> {
971        self.egs
972            .cosmos_eula_accept(eula_id, locale, version)
973            .await
974            .ok()
975            .map(|r| r.accepted)
976    }
977
978    /// Accept a EULA. Returns full `Result`.
979    pub async fn try_cosmos_eula_accept(
980        &self,
981        eula_id: &str,
982        locale: &str,
983        version: u32,
984    ) -> Result<cosmos::CosmosEulaResponse, EpicAPIError> {
985        self.egs.cosmos_eula_accept(eula_id, locale, version).await
986    }
987
988    /// Get Cosmos account details. Returns `None` on error.
989    pub async fn cosmos_account(&self) -> Option<cosmos::CosmosAccount> {
990        self.egs.cosmos_account().await.ok()
991    }
992
993    /// Get Cosmos account details. Returns full `Result`.
994    pub async fn try_cosmos_account(&self) -> Result<cosmos::CosmosAccount, EpicAPIError> {
995        self.egs.cosmos_account().await
996    }
997
998    /// Fetch engine version download blobs for a platform. Returns `None` on error.
999    pub async fn engine_versions(
1000        &self,
1001        platform: &str,
1002    ) -> Option<engine_blob::EngineBlobsResponse> {
1003        self.egs.engine_versions(platform).await.ok()
1004    }
1005
1006    /// Fetch engine version download blobs. Returns full `Result`.
1007    pub async fn try_engine_versions(
1008        &self,
1009        platform: &str,
1010    ) -> Result<engine_blob::EngineBlobsResponse, EpicAPIError> {
1011        self.egs.engine_versions(platform).await
1012    }
1013
1014    // --- Fab Search/Browse ---
1015
1016    /// Search Fab listings. Returns `None` on error.
1017    pub async fn fab_search(
1018        &self,
1019        params: &fab_search::FabSearchParams,
1020    ) -> Option<fab_search::FabSearchResults> {
1021        self.egs.fab_search(params).await.ok()
1022    }
1023
1024    /// Search Fab listings. Returns full `Result`.
1025    pub async fn try_fab_search(
1026        &self,
1027        params: &fab_search::FabSearchParams,
1028    ) -> Result<fab_search::FabSearchResults, EpicAPIError> {
1029        self.egs.fab_search(params).await
1030    }
1031
1032    /// Get full listing detail. Returns `None` on error.
1033    pub async fn fab_listing(&self, uid: &str) -> Option<fab_search::FabListingDetail> {
1034        self.egs.fab_listing(uid).await.ok()
1035    }
1036
1037    /// Get full listing detail. Returns full `Result`.
1038    pub async fn try_fab_listing(
1039        &self,
1040        uid: &str,
1041    ) -> Result<fab_search::FabListingDetail, EpicAPIError> {
1042        self.egs.fab_listing(uid).await
1043    }
1044
1045    /// Get UE-specific format details for a listing. Returns `None` on error.
1046    pub async fn fab_listing_ue_formats(
1047        &self,
1048        uid: &str,
1049    ) -> Option<Vec<fab_search::FabListingUeFormat>> {
1050        self.egs.fab_listing_ue_formats(uid).await.ok()
1051    }
1052
1053    /// Get UE-specific format details. Returns full `Result`.
1054    pub async fn try_fab_listing_ue_formats(
1055        &self,
1056        uid: &str,
1057    ) -> Result<Vec<fab_search::FabListingUeFormat>, EpicAPIError> {
1058        self.egs.fab_listing_ue_formats(uid).await
1059    }
1060
1061    /// Get listing state (ownership, wishlist, review). Returns `None` on error.
1062    pub async fn fab_listing_state(
1063        &self,
1064        uid: &str,
1065    ) -> Option<fab_search::FabListingState> {
1066        self.egs.fab_listing_state(uid).await.ok()
1067    }
1068
1069    /// Get listing state. Returns full `Result`.
1070    pub async fn try_fab_listing_state(
1071        &self,
1072        uid: &str,
1073    ) -> Result<fab_search::FabListingState, EpicAPIError> {
1074        self.egs.fab_listing_state(uid).await
1075    }
1076
1077    /// Bulk check listing states. Returns `None` on error.
1078    pub async fn fab_listing_states_bulk(
1079        &self,
1080        listing_ids: &[&str],
1081    ) -> Option<Vec<fab_search::FabListingState>> {
1082        self.egs.fab_listing_states_bulk(listing_ids).await.ok()
1083    }
1084
1085    /// Bulk check listing states. Returns full `Result`.
1086    pub async fn try_fab_listing_states_bulk(
1087        &self,
1088        listing_ids: &[&str],
1089    ) -> Result<Vec<fab_search::FabListingState>, EpicAPIError> {
1090        self.egs.fab_listing_states_bulk(listing_ids).await
1091    }
1092
1093    /// Bulk fetch pricing for multiple offer IDs. Returns `None` on error.
1094    pub async fn fab_bulk_prices(
1095        &self,
1096        offer_ids: &[&str],
1097    ) -> Option<Vec<fab_search::FabPriceInfo>> {
1098        self.egs.fab_bulk_prices(offer_ids).await.ok()
1099    }
1100
1101    /// Bulk fetch pricing. Returns full `Result`.
1102    pub async fn try_fab_bulk_prices(
1103        &self,
1104        offer_ids: &[&str],
1105    ) -> Result<Vec<fab_search::FabPriceInfo>, EpicAPIError> {
1106        self.egs.fab_bulk_prices(offer_ids).await
1107    }
1108
1109    /// Get listing ownership info. Returns `None` on error.
1110    pub async fn fab_listing_ownership(
1111        &self,
1112        uid: &str,
1113    ) -> Option<fab_search::FabOwnership> {
1114        self.egs.fab_listing_ownership(uid).await.ok()
1115    }
1116
1117    /// Get listing ownership info. Returns full `Result`.
1118    pub async fn try_fab_listing_ownership(
1119        &self,
1120        uid: &str,
1121    ) -> Result<fab_search::FabOwnership, EpicAPIError> {
1122        self.egs.fab_listing_ownership(uid).await
1123    }
1124
1125    // --- Cosmos Policy/Communication ---
1126
1127    /// Check Age of Digital Consent policy. Returns `None` on error.
1128    pub async fn cosmos_policy_aodc(&self) -> Option<cosmos::CosmosPolicyAodc> {
1129        self.egs.cosmos_policy_aodc().await.ok()
1130    }
1131
1132    /// Check Age of Digital Consent policy. Returns full `Result`.
1133    pub async fn try_cosmos_policy_aodc(
1134        &self,
1135    ) -> Result<cosmos::CosmosPolicyAodc, EpicAPIError> {
1136        self.egs.cosmos_policy_aodc().await
1137    }
1138
1139    /// Check communication opt-in status. Returns `None` on error.
1140    pub async fn cosmos_comm_opt_in(
1141        &self,
1142        setting: &str,
1143    ) -> Option<cosmos::CosmosCommOptIn> {
1144        self.egs.cosmos_comm_opt_in(setting).await.ok()
1145    }
1146
1147    /// Check communication opt-in status. Returns full `Result`.
1148    pub async fn try_cosmos_comm_opt_in(
1149        &self,
1150        setting: &str,
1151    ) -> Result<cosmos::CosmosCommOptIn, EpicAPIError> {
1152        self.egs.cosmos_comm_opt_in(setting).await
1153    }
1154}
1155
1156#[cfg(test)]
1157mod facade_tests {
1158    use super::*;
1159    use crate::api::types::account::UserData;
1160    use chrono::{Duration, Utc};
1161
1162    #[test]
1163    fn new_creates_instance() {
1164        let egs = EpicGames::new();
1165        assert!(!egs.is_logged_in());
1166    }
1167
1168    #[test]
1169    fn default_same_as_new() {
1170        let egs = EpicGames::default();
1171        assert!(!egs.is_logged_in());
1172    }
1173
1174    #[test]
1175    fn user_details_default_empty() {
1176        let egs = EpicGames::new();
1177        assert!(egs.user_details().access_token.is_none());
1178    }
1179
1180    #[test]
1181    fn set_and_get_user_details() {
1182        let mut egs = EpicGames::new();
1183        let mut ud = UserData::new();
1184        ud.display_name = Some("TestUser".to_string());
1185        egs.set_user_details(ud);
1186        assert_eq!(egs.user_details().display_name, Some("TestUser".to_string()));
1187    }
1188
1189    #[test]
1190    fn is_logged_in_expired() {
1191        let mut egs = EpicGames::new();
1192        let mut ud = UserData::new();
1193        ud.expires_at = Some(Utc::now() - Duration::hours(1));
1194        egs.set_user_details(ud);
1195        assert!(!egs.is_logged_in());
1196    }
1197
1198    #[test]
1199    fn is_logged_in_valid() {
1200        let mut egs = EpicGames::new();
1201        let mut ud = UserData::new();
1202        ud.expires_at = Some(Utc::now() + Duration::hours(2));
1203        egs.set_user_details(ud);
1204        assert!(egs.is_logged_in());
1205    }
1206
1207    #[test]
1208    fn is_logged_in_within_600s_threshold() {
1209        let mut egs = EpicGames::new();
1210        let mut ud = UserData::new();
1211        ud.expires_at = Some(Utc::now() + Duration::seconds(500));
1212        egs.set_user_details(ud);
1213        assert!(!egs.is_logged_in());
1214    }
1215}