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 api::types::fab_taxonomy;
116use api::types::fab_entitlement;
117use api::types::account::TokenVerification;
118use log::{error, info, warn};
119use crate::api::error::EpicAPIError;
120
121/// Module for authenticated API communication
122pub mod api;
123
124/// Client for the Epic Games Store API.
125///
126/// This is the main entry point for the library. Create an instance with
127/// [`EpicGames::new`], authenticate with [`EpicGames::auth_code`] or
128/// [`EpicGames::login`], then call API methods.
129///
130/// Most methods return `Option<T>` or `Vec<T>`, returning `None` / empty on
131/// errors. Fab methods return `Result<T, EpicAPIError>` for richer error
132/// handling (e.g., distinguishing timeouts from other failures).
133///
134/// Session state is stored in [`UserData`], which implements
135/// `Serialize` / `Deserialize` for persistence across runs.
136#[derive(Debug, Clone)]
137pub struct EpicGames {
138    egs: EpicAPI,
139}
140
141impl Default for EpicGames {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl EpicGames {
148    /// Creates a new [`EpicGames`] client.
149    pub fn new() -> Self {
150        EpicGames {
151            egs: EpicAPI::new(),
152        }
153    }
154
155    /// Check whether the user is logged in.
156    ///
157    /// Returns `true` if the access token exists and has more than 600 seconds
158    /// remaining before expiry.
159    pub fn is_logged_in(&self) -> bool {
160        if let Some(exp) = self.egs.user_data.expires_at {
161            let now = chrono::offset::Utc::now();
162            let td = exp - now;
163            if td.num_seconds() > 600 {
164                return true;
165            }
166        }
167        false
168    }
169
170    /// Returns a clone of the current session state.
171    ///
172    /// The returned [`UserData`] implements `Serialize` / `Deserialize`,
173    /// so you can persist it to disk and restore it later with
174    /// [`set_user_details`](Self::set_user_details).
175    pub fn user_details(&self) -> UserData {
176        self.egs.user_data.clone()
177    }
178
179    /// Restore session state from a previously saved [`UserData`].
180    ///
181    /// Only merges `Some` fields — existing values are preserved for any
182    /// field that is `None` in the input. Call [`login`](Self::login)
183    /// afterward to refresh the access token.
184    pub fn set_user_details(&mut self, user_details: UserData) {
185        self.egs.user_data.update(user_details);
186    }
187
188    /// Like [`auth_code`](Self::auth_code), but returns a `Result` instead of swallowing errors.
189    pub async fn try_auth_code(
190        &mut self,
191        exchange_token: Option<String>,
192        authorization_code: Option<String>,
193    ) -> Result<bool, EpicAPIError> {
194        self.egs
195            .start_session(exchange_token, authorization_code)
196            .await
197    }
198
199    /// Authenticate with an authorization code or exchange token.
200    ///
201    /// Returns `true` on success, `false` on failure. Returns `None` on API errors.
202    pub async fn auth_code(
203        &mut self,
204        exchange_token: Option<String>,
205        authorization_code: Option<String>,
206    ) -> bool {
207        self.try_auth_code(exchange_token, authorization_code)
208            .await
209            .unwrap_or(false)
210    }
211
212    /// Invalidate the current session and log out.
213    pub async fn logout(&mut self) -> bool {
214        self.egs.invalidate_sesion().await
215    }
216
217    /// Like [`login`](Self::login), but returns a `Result` instead of swallowing errors.
218    pub async fn try_login(&mut self) -> Result<bool, EpicAPIError> {
219        if let Some(exp) = self.egs.user_data.expires_at {
220            let now = chrono::offset::Utc::now();
221            let td = exp - now;
222            if td.num_seconds() > 600 {
223                info!("Trying to re-use existing login session... ");
224                let resumed = self.egs.resume_session().await.map_err(|e| {
225                    warn!("{}", e);
226                    e
227                })?;
228                if resumed {
229                    info!("Logged in");
230                    return Ok(true);
231                }
232                return Ok(false);
233            }
234        }
235        info!("Logging in...");
236        if let Some(exp) = self.egs.user_data.refresh_expires_at {
237            let now = chrono::offset::Utc::now();
238            let td = exp - now;
239            if td.num_seconds() > 600 {
240                let started = self.egs.start_session(None, None).await.map_err(|e| {
241                    error!("{}", e);
242                    e
243                })?;
244                if started {
245                    info!("Logged in");
246                    return Ok(true);
247                }
248                return Ok(false);
249            }
250        }
251        Ok(false)
252    }
253
254    /// Resume session using the saved refresh token.
255    ///
256    /// Returns `true` on success, `false` if the refresh token has expired or is invalid.
257    /// Unlike [`try_login`](Self::try_login), this method falls through to
258    /// refresh-token login if session resume fails.
259    pub async fn login(&mut self) -> bool {
260        if let Some(exp) = self.egs.user_data.expires_at {
261            let now = chrono::offset::Utc::now();
262            let td = exp - now;
263            if td.num_seconds() > 600 {
264                info!("Trying to re-use existing login session... ");
265                match self.egs.resume_session().await {
266                    Ok(b) => {
267                        if b {
268                            info!("Logged in");
269                            return true;
270                        }
271                        return false;
272                    }
273                    Err(e) => {
274                        warn!("{}", e)
275                    }
276                };
277            }
278        }
279        info!("Logging in...");
280        if let Some(exp) = self.egs.user_data.refresh_expires_at {
281            let now = chrono::offset::Utc::now();
282            let td = exp - now;
283            if td.num_seconds() > 600 {
284                match self.egs.start_session(None, None).await {
285                    Ok(b) => {
286                        if b {
287                            info!("Logged in");
288                            return true;
289                        }
290                        return false;
291                    }
292                    Err(e) => {
293                        error!("{}", e)
294                    }
295                }
296            }
297        }
298        false
299    }
300
301    /// Like [`list_assets`](Self::list_assets), but returns a `Result` instead of swallowing errors.
302    pub async fn try_list_assets(
303        &mut self,
304        platform: Option<String>,
305        label: Option<String>,
306    ) -> Result<Vec<EpicAsset>, EpicAPIError> {
307        self.egs.assets(platform, label).await
308    }
309
310    /// List all owned assets.
311    ///
312    /// Defaults to platform="Windows" and label="Live" if not specified.
313    /// Returns empty `Vec` on API errors.
314    pub async fn list_assets(
315        &mut self,
316        platform: Option<String>,
317        label: Option<String>,
318    ) -> Vec<EpicAsset> {
319        self.try_list_assets(platform, label)
320            .await
321            .unwrap_or_else(|_| Vec::new())
322    }
323
324    /// Like [`asset_manifest`](Self::asset_manifest), but returns a `Result` instead of swallowing errors.
325    pub async fn try_asset_manifest(
326        &mut self,
327        platform: Option<String>,
328        label: Option<String>,
329        namespace: Option<String>,
330        item_id: Option<String>,
331        app: Option<String>,
332    ) -> Result<AssetManifest, EpicAPIError> {
333        self.egs
334            .asset_manifest(platform, label, namespace, item_id, app)
335            .await
336    }
337
338    /// Fetch asset manifest with CDN download URLs.
339    ///
340    /// Defaults to platform="Windows" and label="Live" if not specified.
341    /// Returns `None` on API errors.
342    pub async fn asset_manifest(
343        &mut self,
344        platform: Option<String>,
345        label: Option<String>,
346        namespace: Option<String>,
347        item_id: Option<String>,
348        app: Option<String>,
349    ) -> Option<AssetManifest> {
350        self.try_asset_manifest(platform, label, namespace, item_id, app)
351            .await
352            .ok()
353    }
354
355    /// Fetch Fab asset manifest with signed distribution points.
356    ///
357    /// Returns `Result` to expose timeout errors (403 → `EpicAPIError::FabTimeout`).
358    pub async fn fab_asset_manifest(
359        &self,
360        artifact_id: &str,
361        namespace: &str,
362        asset_id: &str,
363        platform: Option<&str>,
364    ) -> Result<Vec<DownloadInfo>, EpicAPIError> {
365        match self
366            .egs
367            .fab_asset_manifest(artifact_id, namespace, asset_id, platform)
368            .await
369        {
370            Ok(a) => Ok(a),
371            Err(e) => Err(e),
372        }
373    }
374
375    /// Like [`asset_info`](Self::asset_info), but returns a `Result` instead of swallowing errors.
376    pub async fn try_asset_info(
377        &mut self,
378        asset: &EpicAsset,
379    ) -> Result<Option<AssetInfo>, EpicAPIError> {
380        let mut info = self.egs.asset_info(asset).await?;
381        Ok(info.remove(asset.catalog_item_id.as_str()))
382    }
383
384    /// Fetch catalog metadata for an asset (includes DLC tree).
385    ///
386    /// Returns `None` on API errors.
387    pub async fn asset_info(&mut self, asset: &EpicAsset) -> Option<AssetInfo> {
388        self.try_asset_info(asset).await.ok().flatten()
389    }
390
391    /// Like [`account_details`](Self::account_details), but returns a `Result` instead of swallowing errors.
392    pub async fn try_account_details(&mut self) -> Result<AccountData, EpicAPIError> {
393        self.egs.account_details().await
394    }
395
396    /// Fetch account details (email, display name, country, 2FA status).
397    ///
398    /// Returns `None` on API errors.
399    pub async fn account_details(&mut self) -> Option<AccountData> {
400        self.try_account_details().await.ok()
401    }
402
403    /// Like [`account_ids_details`](Self::account_ids_details), but returns a `Result` instead of swallowing errors.
404    pub async fn try_account_ids_details(
405        &mut self,
406        ids: Vec<String>,
407    ) -> Result<Vec<AccountInfo>, EpicAPIError> {
408        self.egs.account_ids_details(ids).await
409    }
410
411    /// Bulk lookup of account IDs to display names.
412    ///
413    /// Returns `None` on API errors.
414    pub async fn account_ids_details(&mut self, ids: Vec<String>) -> Option<Vec<AccountInfo>> {
415        self.try_account_ids_details(ids).await.ok()
416    }
417
418    /// Like [`account_friends`](Self::account_friends), but returns a `Result` instead of swallowing errors.
419    pub async fn try_account_friends(
420        &mut self,
421        include_pending: bool,
422    ) -> Result<Vec<Friend>, EpicAPIError> {
423        self.egs.account_friends(include_pending).await
424    }
425
426    /// Fetch friends list (including pending requests if `include_pending` is true).
427    ///
428    /// Returns `None` on API errors.
429    pub async fn account_friends(&mut self, include_pending: bool) -> Option<Vec<Friend>> {
430        self.try_account_friends(include_pending).await.ok()
431    }
432
433    /// Like [`game_token`](Self::game_token), but returns a `Result` instead of swallowing errors.
434    pub async fn try_game_token(&mut self) -> Result<GameToken, EpicAPIError> {
435        self.egs.game_token().await
436    }
437
438    /// Fetch a short-lived exchange code for game launches.
439    ///
440    /// Returns `None` on API errors.
441    pub async fn game_token(&mut self) -> Option<GameToken> {
442        self.try_game_token().await.ok()
443    }
444
445    /// Like [`ownership_token`](Self::ownership_token), but returns a `Result` instead of swallowing errors.
446    pub async fn try_ownership_token(&mut self, asset: &EpicAsset) -> Result<String, EpicAPIError> {
447        self.egs.ownership_token(asset).await.map(|a| a.token)
448    }
449
450    /// Fetch a JWT proving ownership of an asset.
451    ///
452    /// Returns `None` on API errors.
453    pub async fn ownership_token(&mut self, asset: &EpicAsset) -> Option<String> {
454        self.try_ownership_token(asset).await.ok()
455    }
456
457    /// Like [`user_entitlements`](Self::user_entitlements), but returns a `Result` instead of swallowing errors.
458    pub async fn try_user_entitlements(&mut self) -> Result<Vec<Entitlement>, EpicAPIError> {
459        self.egs.user_entitlements().await
460    }
461
462    /// Fetch all user entitlements (games, DLC, subscriptions).
463    ///
464    /// Returns empty `Vec` on API errors.
465    pub async fn user_entitlements(&mut self) -> Vec<Entitlement> {
466        self.try_user_entitlements().await.unwrap_or_else(|_| Vec::new())
467    }
468
469    /// Like [`library_items`](Self::library_items), but returns a `Result` instead of swallowing errors.
470    pub async fn try_library_items(&mut self, include_metadata: bool) -> Result<Library, EpicAPIError> {
471        self.egs.library_items(include_metadata).await
472    }
473
474    /// Fetch the user library with optional metadata.
475    ///
476    /// Paginates internally and returns all records at once. Returns `None` on API errors.
477    pub async fn library_items(&mut self, include_metadata: bool) -> Option<Library> {
478        self.try_library_items(include_metadata).await.ok()
479    }
480
481    /// Like [`fab_library_items`](Self::fab_library_items), but returns a `Result` instead of swallowing errors.
482    pub async fn try_fab_library_items(
483        &mut self,
484        account_id: String,
485    ) -> Result<api::types::fab_library::FabLibrary, EpicAPIError> {
486        self.egs.fab_library_items(account_id).await
487    }
488
489    /// Fetch the user Fab library.
490    ///
491    /// Paginates internally and returns all records at once. Returns `None` on API errors.
492    pub async fn fab_library_items(
493        &mut self,
494        account_id: String,
495    ) -> Option<api::types::fab_library::FabLibrary> {
496        self.try_fab_library_items(account_id).await.ok()
497    }
498
499    /// Parse download manifests from all CDN mirrors.
500    ///
501    /// Fetches from all mirrors, parses binary/JSON format, and populates custom fields
502    /// (BaseUrl, CatalogItemId, etc.). Returns empty `Vec` on API errors.
503    pub async fn asset_download_manifests(&self, manifest: AssetManifest) -> Vec<DownloadManifest> {
504        self.egs.asset_download_manifests(manifest).await
505    }
506
507    /// Parse a Fab download manifest from a specific distribution point.
508    ///
509    /// Checks signature expiration before fetching. Returns `Result` to expose timeout errors.
510    pub async fn fab_download_manifest(
511        &self,
512        download_info: DownloadInfo,
513        distribution_point_url: &str,
514    ) -> Result<DownloadManifest, EpicAPIError> {
515        self.egs
516            .fab_download_manifest(download_info, distribution_point_url)
517            .await
518    }
519
520    /// Like [`auth_client_credentials`](Self::auth_client_credentials), but returns a `Result` instead of swallowing errors.
521    pub async fn try_auth_client_credentials(&mut self) -> Result<bool, EpicAPIError> {
522        self.egs.start_client_credentials_session().await
523    }
524
525    /// Authenticate with client credentials (app-level, no user context).
526    ///
527    /// Uses the launcher's public client ID/secret to obtain an access token
528    /// without any user interaction. The resulting session has limited
529    /// permissions — it can query public endpoints (catalog, service status,
530    /// currencies) but cannot access user-specific data (library, entitlements).
531    ///
532    /// Returns `true` on success, `false` on failure.
533    pub async fn auth_client_credentials(&mut self) -> bool {
534        self.try_auth_client_credentials().await.unwrap_or(false)
535    }
536
537    /// Like [`external_auths`](Self::external_auths), but returns a `Result` instead of swallowing errors.
538    pub async fn try_external_auths(&self, account_id: &str) -> Result<Vec<ExternalAuth>, EpicAPIError> {
539        self.egs.external_auths(account_id).await
540    }
541
542    /// Fetch external auth connections linked to an account.
543    ///
544    /// Returns linked platform accounts (Steam, PSN, Xbox, Nintendo, etc.)
545    /// with external display names and account IDs. Requires a valid user session.
546    ///
547    /// Returns `None` on API errors.
548    pub async fn external_auths(&self, account_id: &str) -> Option<Vec<ExternalAuth>> {
549        self.try_external_auths(account_id).await.ok()
550    }
551
552    /// Like [`sso_domains`](Self::sso_domains), but returns a `Result` instead of swallowing errors.
553    pub async fn try_sso_domains(&self) -> Result<Vec<String>, EpicAPIError> {
554        self.egs.sso_domains().await
555    }
556
557    /// Fetch the list of SSO (Single Sign-On) domains.
558    ///
559    /// Returns domain strings that support Epic's SSO flow. Used by the
560    /// launcher to determine which domains can share authentication cookies.
561    ///
562    /// Returns `None` on API errors.
563    pub async fn sso_domains(&self) -> Option<Vec<String>> {
564        self.try_sso_domains().await.ok()
565    }
566
567    /// Like [`catalog_items`](Self::catalog_items), but returns a `Result` instead of swallowing errors.
568    pub async fn try_catalog_items(
569        &self,
570        namespace: &str,
571        start: i64,
572        count: i64,
573    ) -> Result<CatalogItemPage, EpicAPIError> {
574        self.egs.catalog_items(namespace, start, count).await
575    }
576
577    /// Fetch paginated catalog items for a namespace.
578    ///
579    /// Queries the Epic catalog service for items within a given namespace
580    /// (e.g., a game's namespace). Results are paginated — use `start` and
581    /// `count` to page through. Each [`CatalogItemPage`] includes a `paging`
582    /// field with the total count.
583    ///
584    /// Returns `None` on API errors.
585    pub async fn catalog_items(
586        &self,
587        namespace: &str,
588        start: i64,
589        count: i64,
590    ) -> Option<CatalogItemPage> {
591        self.try_catalog_items(namespace, start, count).await.ok()
592    }
593
594    /// Like [`catalog_offers`](Self::catalog_offers), but returns a `Result` instead of swallowing errors.
595    pub async fn try_catalog_offers(
596        &self,
597        namespace: &str,
598        start: i64,
599        count: i64,
600    ) -> Result<CatalogOfferPage, EpicAPIError> {
601        self.egs.catalog_offers(namespace, start, count).await
602    }
603
604    /// Fetch paginated catalog offers for a namespace.
605    ///
606    /// Queries the Epic catalog service for offers (purchasable items) within
607    /// a namespace. Offers include pricing metadata, seller info, and linked
608    /// catalog items. Use `start` and `count` to paginate.
609    ///
610    /// Returns `None` on API errors.
611    pub async fn catalog_offers(
612        &self,
613        namespace: &str,
614        start: i64,
615        count: i64,
616    ) -> Option<CatalogOfferPage> {
617        self.try_catalog_offers(namespace, start, count).await.ok()
618    }
619
620    /// Like [`bulk_catalog_items`](Self::bulk_catalog_items), but returns a `Result` instead of swallowing errors.
621    pub async fn try_bulk_catalog_items(
622        &self,
623        items: &[(&str, &str)],
624    ) -> Result<std::collections::HashMap<String, std::collections::HashMap<String, AssetInfo>>, EpicAPIError> {
625        self.egs.bulk_catalog_items(items).await
626    }
627
628    /// Bulk fetch catalog items across multiple namespaces.
629    ///
630    /// Accepts a slice of `(namespace, item_id)` pairs and returns them grouped
631    /// by namespace → item_id → [`AssetInfo`]. Useful for resolving catalog
632    /// metadata for items from different games in a single request.
633    ///
634    /// Returns `None` on API errors.
635    pub async fn bulk_catalog_items(
636        &self,
637        items: &[(&str, &str)],
638    ) -> Option<std::collections::HashMap<String, std::collections::HashMap<String, AssetInfo>>> {
639        self.try_bulk_catalog_items(items).await.ok()
640    }
641
642    /// Like [`currencies`](Self::currencies), but returns a `Result` instead of swallowing errors.
643    pub async fn try_currencies(&self, start: i64, count: i64) -> Result<CurrencyPage, EpicAPIError> {
644        self.egs.currencies(start, count).await
645    }
646
647    /// Fetch available currencies from the Epic catalog.
648    ///
649    /// Returns paginated currency definitions including code, symbol, and
650    /// decimal precision. Use `start` and `count` to paginate.
651    ///
652    /// Returns `None` on API errors.
653    pub async fn currencies(&self, start: i64, count: i64) -> Option<CurrencyPage> {
654        self.try_currencies(start, count).await.ok()
655    }
656
657    /// Like [`library_state_token_status`](Self::library_state_token_status), but returns a `Result` instead of swallowing errors.
658    pub async fn try_library_state_token_status(
659        &self,
660        token_id: &str,
661    ) -> Result<bool, EpicAPIError> {
662        self.egs.library_state_token_status(token_id).await
663    }
664
665    /// Check the validity of a library state token.
666    ///
667    /// Returns `Some(true)` if the token is still valid, `Some(false)` if
668    /// expired or invalid, or `None` on API errors. Library state tokens are
669    /// used to detect changes to the user's library since the last sync.
670    ///
671    /// Returns `None` on API errors.
672    pub async fn library_state_token_status(&self, token_id: &str) -> Option<bool> {
673        self.try_library_state_token_status(token_id).await.ok()
674    }
675
676    /// Like [`service_status`](Self::service_status), but returns a `Result` instead of swallowing errors.
677    pub async fn try_service_status(
678        &self,
679        service_id: &str,
680    ) -> Result<Vec<ServiceStatus>, EpicAPIError> {
681        self.egs.service_status(service_id).await
682    }
683
684    /// Fetch service status from Epic's lightswitch API.
685    ///
686    /// Returns the operational status of an Epic online service (e.g., a game's
687    /// backend). The response includes whether the service is UP/DOWN, any
688    /// maintenance message, and whether the current user is banned.
689    ///
690    /// Returns `None` on API errors.
691    pub async fn service_status(&self, service_id: &str) -> Option<Vec<ServiceStatus>> {
692        self.try_service_status(service_id).await.ok()
693    }
694
695    /// Like [`offer_prices`](Self::offer_prices), but returns a `Result` instead of swallowing errors.
696    pub async fn try_offer_prices(
697        &self,
698        namespace: &str,
699        offer_ids: &[String],
700        country: &str,
701    ) -> Result<PriceResponse, EpicAPIError> {
702        self.egs.offer_prices(namespace, offer_ids, country).await
703    }
704
705    /// Fetch offer prices from Epic's price engine.
706    ///
707    /// Queries current pricing for one or more offers within a namespace,
708    /// localized to a specific country. The response includes original price,
709    /// discount price, and pre-formatted display strings.
710    ///
711    /// Returns `None` on API errors.
712    pub async fn offer_prices(
713        &self,
714        namespace: &str,
715        offer_ids: &[String],
716        country: &str,
717    ) -> Option<PriceResponse> {
718        self.try_offer_prices(namespace, offer_ids, country).await.ok()
719    }
720
721    /// Like [`quick_purchase`](Self::quick_purchase), but returns a `Result` instead of swallowing errors.
722    pub async fn try_quick_purchase(
723        &self,
724        namespace: &str,
725        offer_id: &str,
726    ) -> Result<QuickPurchaseResponse, EpicAPIError> {
727        self.egs.quick_purchase(namespace, offer_id).await
728    }
729
730    /// Execute a quick purchase (typically for free game claims).
731    ///
732    /// Initiates a purchase order for a free offer. The response contains the
733    /// order ID and its processing status. For paid offers, use the full
734    /// checkout flow in the Epic Games launcher instead.
735    ///
736    /// Returns `None` on API errors.
737    pub async fn quick_purchase(
738        &self,
739        namespace: &str,
740        offer_id: &str,
741    ) -> Option<QuickPurchaseResponse> {
742        self.try_quick_purchase(namespace, offer_id).await.ok()
743    }
744
745    /// Like [`billing_account`](Self::billing_account), but returns a `Result` instead of swallowing errors.
746    pub async fn try_billing_account(&self) -> Result<BillingAccount, EpicAPIError> {
747        self.egs.billing_account().await
748    }
749
750    /// Fetch the default billing account for payment processing.
751    ///
752    /// Returns the account's billing country, which is used to determine
753    /// regional pricing and payment availability.
754    ///
755    /// Returns `None` on API errors.
756    pub async fn billing_account(&self) -> Option<BillingAccount> {
757        self.try_billing_account().await.ok()
758    }
759
760    /// Update the user's presence status.
761    ///
762    /// Sends a PATCH request to update the user's online presence (e.g.,
763    /// "online", "away") and optionally set an activity with custom properties.
764    /// The `session_id` is the OAuth session token from login. Returns `Ok(())`
765    /// on success (204 No Content) or an [`EpicAPIError`] on failure.
766    pub async fn update_presence(
767        &self,
768        session_id: &str,
769        body: &PresenceUpdate,
770    ) -> Result<(), EpicAPIError> {
771        self.egs.update_presence(session_id, body).await
772    }
773
774    /// Like [`fab_file_download_info`](Self::fab_file_download_info), but returns a `Result` instead of swallowing errors.
775    pub async fn try_fab_file_download_info(
776        &self,
777        listing_id: &str,
778        format_id: &str,
779        file_id: &str,
780    ) -> Result<DownloadInfo, EpicAPIError> {
781        self.egs
782            .fab_file_download_info(listing_id, format_id, file_id)
783            .await
784    }
785
786    /// Fetch download info for a specific file within a Fab listing.
787    ///
788    /// Returns signed [`DownloadInfo`] for a single file identified by
789    /// `listing_id`, `format_id`, and `file_id`. Use this for targeted
790    /// downloads of individual files from a Fab asset rather than fetching
791    /// the entire asset manifest.
792    ///
793    /// Returns `None` on API errors.
794    pub async fn fab_file_download_info(
795        &self,
796        listing_id: &str,
797        format_id: &str,
798        file_id: &str,
799    ) -> Option<DownloadInfo> {
800        self.try_fab_file_download_info(listing_id, format_id, file_id)
801            .await
802            .ok()
803    }
804
805    // ── Cloud Saves ──
806
807    /// List cloud save files for the logged-in user.
808    ///
809    /// If `app_name` is provided, lists saves for that specific game.
810    /// If `manifests` is true (only relevant when `app_name` is set), lists manifest files.
811    pub async fn cloud_save_list(
812        &self,
813        app_name: Option<&str>,
814        manifests: bool,
815    ) -> Result<CloudSaveResponse, EpicAPIError> {
816        self.egs.cloud_save_list(app_name, manifests).await
817    }
818
819    /// Query cloud save files by specific filenames.
820    ///
821    /// Returns metadata including read/write links for the specified files.
822    pub async fn cloud_save_query(
823        &self,
824        app_name: &str,
825        filenames: &[String],
826    ) -> Result<CloudSaveResponse, EpicAPIError> {
827        self.egs.cloud_save_query(app_name, filenames).await
828    }
829
830    /// Delete a cloud save file by its storage path.
831    pub async fn cloud_save_delete(&self, path: &str) -> Result<(), EpicAPIError> {
832        self.egs.cloud_save_delete(path).await
833    }
834
835    // ── Artifact Service & Manifests ──
836
837    /// Fetch an artifact service ticket for manifest retrieval via EOS Helper.
838    ///
839    /// The `sandbox_id` is typically the game's namespace and `artifact_id`
840    /// is the app name. Returns a signed ticket for use with
841    /// [`game_manifest_by_ticket`](Self::game_manifest_by_ticket).
842    pub async fn artifact_service_ticket(
843        &self,
844        sandbox_id: &str,
845        artifact_id: &str,
846        label: Option<&str>,
847        platform: Option<&str>,
848    ) -> Result<ArtifactServiceTicket, EpicAPIError> {
849        self.egs
850            .artifact_service_ticket(sandbox_id, artifact_id, label, platform)
851            .await
852    }
853
854    /// Fetch a game manifest using a signed artifact service ticket.
855    ///
856    /// Alternative to [`asset_manifest`](Self::asset_manifest) using ticket-based
857    /// auth from the EOS Helper service.
858    pub async fn game_manifest_by_ticket(
859        &self,
860        artifact_id: &str,
861        signed_ticket: &str,
862        label: Option<&str>,
863        platform: Option<&str>,
864    ) -> Result<AssetManifest, EpicAPIError> {
865        self.egs
866            .game_manifest_by_ticket(artifact_id, signed_ticket, label, platform)
867            .await
868    }
869
870    /// Fetch launcher manifests for self-update checks.
871    pub async fn launcher_manifests(
872        &self,
873        platform: Option<&str>,
874        label: Option<&str>,
875    ) -> Result<AssetManifest, EpicAPIError> {
876        self.egs.launcher_manifests(platform, label).await
877    }
878
879    /// Try to fetch a delta manifest for optimized patching between builds.
880    ///
881    /// Returns `None` if no delta is available or the builds are identical.
882    pub async fn delta_manifest(
883        &self,
884        base_url: &str,
885        old_build_id: &str,
886        new_build_id: &str,
887    ) -> Option<Vec<u8>> {
888        self.egs
889            .delta_manifest(base_url, old_build_id, new_build_id)
890            .await
891    }
892
893    // ── SID Auth ──
894
895    /// Authenticate via session ID (SID) from the Epic web login flow.
896    ///
897    /// Performs the multi-step web exchange: set-sid → CSRF → exchange code,
898    /// then starts a session with the resulting code. Returns `true` on success.
899    pub async fn auth_sid(&mut self, sid: &str) -> Result<bool, EpicAPIError> {
900        self.egs.auth_sid(sid).await
901    }
902
903    // ── Uplay / Ubisoft Store ──
904
905    /// Fetch Uplay codes linked to the user's Epic account.
906    pub async fn store_get_uplay_codes(
907        &self,
908    ) -> Result<UplayGraphQLResponse<UplayCodesResult>, EpicAPIError> {
909        self.egs.store_get_uplay_codes().await
910    }
911
912    /// Claim a Uplay code for a specific game.
913    pub async fn store_claim_uplay_code(
914        &self,
915        uplay_account_id: &str,
916        game_id: &str,
917    ) -> Result<UplayGraphQLResponse<UplayClaimResult>, EpicAPIError> {
918        self.egs
919            .store_claim_uplay_code(uplay_account_id, game_id)
920            .await
921    }
922
923    /// Redeem all pending Uplay codes for the user's account.
924    pub async fn store_redeem_uplay_codes(
925        &self,
926        uplay_account_id: &str,
927    ) -> Result<UplayGraphQLResponse<UplayRedeemResult>, EpicAPIError> {
928        self.egs.store_redeem_uplay_codes(uplay_account_id).await
929    }
930
931    // --- Cosmos Session ---
932
933    /// Set up a Cosmos cookie session from an exchange code.
934    /// Typically called with a code from `game_token()`.
935    pub async fn cosmos_session_setup(
936        &self,
937        exchange_code: &str,
938    ) -> Result<cosmos::CosmosAuthResponse, EpicAPIError> {
939        self.egs.cosmos_session_setup(exchange_code).await
940    }
941
942    /// Upgrade bearer token to Cosmos session (step 5 of session setup).
943    pub async fn cosmos_auth_upgrade(
944        &self,
945    ) -> Result<cosmos::CosmosAuthResponse, EpicAPIError> {
946        self.egs.cosmos_auth_upgrade().await
947    }
948
949    /// Check if a EULA has been accepted. Returns `None` on error.
950    pub async fn cosmos_eula_check(&self, eula_id: &str, locale: &str) -> Option<bool> {
951        self.egs
952            .cosmos_eula_check(eula_id, locale)
953            .await
954            .ok()
955            .map(|r| r.accepted)
956    }
957
958    /// Check if a EULA has been accepted. Returns full `Result`.
959    pub async fn try_cosmos_eula_check(
960        &self,
961        eula_id: &str,
962        locale: &str,
963    ) -> Result<cosmos::CosmosEulaResponse, EpicAPIError> {
964        self.egs.cosmos_eula_check(eula_id, locale).await
965    }
966
967    /// Accept a EULA. Returns `None` on error.
968    pub async fn cosmos_eula_accept(
969        &self,
970        eula_id: &str,
971        locale: &str,
972        version: u32,
973    ) -> Option<bool> {
974        self.egs
975            .cosmos_eula_accept(eula_id, locale, version)
976            .await
977            .ok()
978            .map(|r| r.accepted)
979    }
980
981    /// Accept a EULA. Returns full `Result`.
982    pub async fn try_cosmos_eula_accept(
983        &self,
984        eula_id: &str,
985        locale: &str,
986        version: u32,
987    ) -> Result<cosmos::CosmosEulaResponse, EpicAPIError> {
988        self.egs.cosmos_eula_accept(eula_id, locale, version).await
989    }
990
991    /// Get Cosmos account details. Returns `None` on error.
992    pub async fn cosmos_account(&self) -> Option<cosmos::CosmosAccount> {
993        self.egs.cosmos_account().await.ok()
994    }
995
996    /// Get Cosmos account details. Returns full `Result`.
997    pub async fn try_cosmos_account(&self) -> Result<cosmos::CosmosAccount, EpicAPIError> {
998        self.egs.cosmos_account().await
999    }
1000
1001    /// Fetch engine version download blobs for a platform. Returns `None` on error.
1002    pub async fn engine_versions(
1003        &self,
1004        platform: &str,
1005    ) -> Option<engine_blob::EngineBlobsResponse> {
1006        self.egs.engine_versions(platform).await.ok()
1007    }
1008
1009    /// Fetch engine version download blobs. Returns full `Result`.
1010    pub async fn try_engine_versions(
1011        &self,
1012        platform: &str,
1013    ) -> Result<engine_blob::EngineBlobsResponse, EpicAPIError> {
1014        self.egs.engine_versions(platform).await
1015    }
1016
1017    // --- Fab Search/Browse ---
1018
1019    /// Search Fab listings. Returns `None` on error.
1020    pub async fn fab_search(
1021        &self,
1022        params: &fab_search::FabSearchParams,
1023    ) -> Option<fab_search::FabSearchResults> {
1024        self.egs.fab_search(params).await.ok()
1025    }
1026
1027    /// Search Fab listings. Returns full `Result`.
1028    pub async fn try_fab_search(
1029        &self,
1030        params: &fab_search::FabSearchParams,
1031    ) -> Result<fab_search::FabSearchResults, EpicAPIError> {
1032        self.egs.fab_search(params).await
1033    }
1034
1035    /// Get full listing detail. Returns `None` on error.
1036    pub async fn fab_listing(&self, uid: &str) -> Option<fab_search::FabListingDetail> {
1037        self.egs.fab_listing(uid).await.ok()
1038    }
1039
1040    /// Get full listing detail. Returns full `Result`.
1041    pub async fn try_fab_listing(
1042        &self,
1043        uid: &str,
1044    ) -> Result<fab_search::FabListingDetail, EpicAPIError> {
1045        self.egs.fab_listing(uid).await
1046    }
1047
1048    /// Get UE-specific format details for a listing. Returns `None` on error.
1049    pub async fn fab_listing_ue_formats(
1050        &self,
1051        uid: &str,
1052    ) -> Option<Vec<fab_search::FabListingUeFormat>> {
1053        self.egs.fab_listing_ue_formats(uid).await.ok()
1054    }
1055
1056    /// Get UE-specific format details. Returns full `Result`.
1057    pub async fn try_fab_listing_ue_formats(
1058        &self,
1059        uid: &str,
1060    ) -> Result<Vec<fab_search::FabListingUeFormat>, EpicAPIError> {
1061        self.egs.fab_listing_ue_formats(uid).await
1062    }
1063
1064    /// Get listing state (ownership, wishlist, review). Returns `None` on error.
1065    pub async fn fab_listing_state(
1066        &self,
1067        uid: &str,
1068    ) -> Option<fab_search::FabListingState> {
1069        self.egs.fab_listing_state(uid).await.ok()
1070    }
1071
1072    /// Get listing state. Returns full `Result`.
1073    pub async fn try_fab_listing_state(
1074        &self,
1075        uid: &str,
1076    ) -> Result<fab_search::FabListingState, EpicAPIError> {
1077        self.egs.fab_listing_state(uid).await
1078    }
1079
1080    /// Bulk check listing states. Returns `None` on error.
1081    pub async fn fab_listing_states_bulk(
1082        &self,
1083        listing_ids: &[&str],
1084    ) -> Option<Vec<fab_search::FabListingState>> {
1085        self.egs.fab_listing_states_bulk(listing_ids).await.ok()
1086    }
1087
1088    /// Bulk check listing states. Returns full `Result`.
1089    pub async fn try_fab_listing_states_bulk(
1090        &self,
1091        listing_ids: &[&str],
1092    ) -> Result<Vec<fab_search::FabListingState>, EpicAPIError> {
1093        self.egs.fab_listing_states_bulk(listing_ids).await
1094    }
1095
1096    /// Bulk fetch pricing for multiple offer IDs. Returns `None` on error.
1097    pub async fn fab_bulk_prices(
1098        &self,
1099        offer_ids: &[&str],
1100    ) -> Option<fab_search::FabBulkPricesResponse> {
1101        self.egs.fab_bulk_prices(offer_ids).await.ok()
1102    }
1103
1104    /// Bulk fetch pricing. Returns full `Result`.
1105    pub async fn try_fab_bulk_prices(
1106        &self,
1107        offer_ids: &[&str],
1108    ) -> Result<fab_search::FabBulkPricesResponse, EpicAPIError> {
1109        self.egs.fab_bulk_prices(offer_ids).await
1110    }
1111
1112    /// Get listing ownership info. Returns `None` on error.
1113    pub async fn fab_listing_ownership(
1114        &self,
1115        uid: &str,
1116    ) -> Option<fab_search::FabOwnership> {
1117        self.egs.fab_listing_ownership(uid).await.ok()
1118    }
1119
1120    /// Get listing ownership info. Returns full `Result`.
1121    pub async fn try_fab_listing_ownership(
1122        &self,
1123        uid: &str,
1124    ) -> Result<fab_search::FabOwnership, EpicAPIError> {
1125        self.egs.fab_listing_ownership(uid).await
1126    }
1127
1128    /// Get pricing for a specific listing. Returns `None` on error.
1129    pub async fn fab_listing_prices(
1130        &self,
1131        uid: &str,
1132    ) -> Option<Vec<fab_search::FabPriceInfo>> {
1133        self.egs.fab_listing_prices(uid).await.ok()
1134    }
1135
1136    /// Get pricing for a specific listing. Returns full `Result`.
1137    pub async fn try_fab_listing_prices(
1138        &self,
1139        uid: &str,
1140    ) -> Result<Vec<fab_search::FabPriceInfo>, EpicAPIError> {
1141        self.egs.fab_listing_prices(uid).await
1142    }
1143
1144    /// Get reviews for a listing. Returns `None` on error.
1145    pub async fn fab_listing_reviews(
1146        &self,
1147        uid: &str,
1148        sort_by: Option<&str>,
1149        cursor: Option<&str>,
1150    ) -> Option<fab_search::FabReviewsResponse> {
1151        self.egs.fab_listing_reviews(uid, sort_by, cursor).await.ok()
1152    }
1153
1154    /// Get reviews for a listing. Returns full `Result`.
1155    pub async fn try_fab_listing_reviews(
1156        &self,
1157        uid: &str,
1158        sort_by: Option<&str>,
1159        cursor: Option<&str>,
1160    ) -> Result<fab_search::FabReviewsResponse, EpicAPIError> {
1161        self.egs.fab_listing_reviews(uid, sort_by, cursor).await
1162    }
1163
1164    // --- Fab Taxonomy ---
1165
1166    /// Fetch available license types. Returns `None` on error.
1167    pub async fn fab_licenses(&self) -> Option<Vec<fab_taxonomy::FabLicenseType>> {
1168        self.egs.fab_licenses().await.ok()
1169    }
1170
1171    /// Fetch available license types. Returns full `Result`.
1172    pub async fn try_fab_licenses(
1173        &self,
1174    ) -> Result<Vec<fab_taxonomy::FabLicenseType>, EpicAPIError> {
1175        self.egs.fab_licenses().await
1176    }
1177
1178    /// Fetch asset format groups. Returns `None` on error.
1179    pub async fn fab_format_groups(&self) -> Option<Vec<fab_taxonomy::FabFormatGroup>> {
1180        self.egs.fab_format_groups().await.ok()
1181    }
1182
1183    /// Fetch asset format groups. Returns full `Result`.
1184    pub async fn try_fab_format_groups(
1185        &self,
1186    ) -> Result<Vec<fab_taxonomy::FabFormatGroup>, EpicAPIError> {
1187        self.egs.fab_format_groups().await
1188    }
1189
1190    /// Fetch tag groups with nested tags. Returns `None` on error.
1191    pub async fn fab_tag_groups(&self) -> Option<Vec<fab_taxonomy::FabTagGroup>> {
1192        self.egs.fab_tag_groups().await.ok()
1193    }
1194
1195    /// Fetch tag groups with nested tags. Returns full `Result`.
1196    pub async fn try_fab_tag_groups(
1197        &self,
1198    ) -> Result<Vec<fab_taxonomy::FabTagGroup>, EpicAPIError> {
1199        self.egs.fab_tag_groups().await
1200    }
1201
1202    /// Fetch available UE versions. Returns `None` on error.
1203    pub async fn fab_ue_versions(&self) -> Option<Vec<String>> {
1204        self.egs.fab_ue_versions().await.ok()
1205    }
1206
1207    /// Fetch available UE versions. Returns full `Result`.
1208    pub async fn try_fab_ue_versions(&self) -> Result<Vec<String>, EpicAPIError> {
1209        self.egs.fab_ue_versions().await
1210    }
1211
1212    /// Fetch channel info by slug. Returns `None` on error.
1213    pub async fn fab_channel(&self, slug: &str) -> Option<fab_taxonomy::FabChannel> {
1214        self.egs.fab_channel(slug).await.ok()
1215    }
1216
1217    /// Fetch channel info by slug. Returns full `Result`.
1218    pub async fn try_fab_channel(
1219        &self,
1220        slug: &str,
1221    ) -> Result<fab_taxonomy::FabChannel, EpicAPIError> {
1222        self.egs.fab_channel(slug).await
1223    }
1224
1225    // --- Fab Library Entitlements ---
1226
1227    /// Search library entitlements. Returns `None` on error.
1228    pub async fn fab_library_entitlements(
1229        &self,
1230        params: &fab_entitlement::FabEntitlementSearchParams,
1231    ) -> Option<fab_entitlement::FabEntitlementResults> {
1232        self.egs.fab_library_entitlements(params).await.ok()
1233    }
1234
1235    /// Search library entitlements. Returns full `Result`.
1236    pub async fn try_fab_library_entitlements(
1237        &self,
1238        params: &fab_entitlement::FabEntitlementSearchParams,
1239    ) -> Result<fab_entitlement::FabEntitlementResults, EpicAPIError> {
1240        self.egs.fab_library_entitlements(params).await
1241    }
1242
1243    // --- Fab Session ---
1244
1245    /// Initialize Fab CSRF token. Sets cookies on the shared HTTP client.
1246    pub async fn fab_csrf(&self) -> Result<(), EpicAPIError> {
1247        self.egs.fab_csrf().await
1248    }
1249
1250    /// Fetch Fab user context. Returns `None` on error.
1251    pub async fn fab_user_context(&self) -> Option<fab_search::FabUserContext> {
1252        self.egs.fab_user_context().await.ok()
1253    }
1254
1255    /// Fetch Fab user context. Returns full `Result`.
1256    pub async fn try_fab_user_context(
1257        &self,
1258    ) -> Result<fab_search::FabUserContext, EpicAPIError> {
1259        self.egs.fab_user_context().await
1260    }
1261
1262    /// Add a free listing to the user's library. Returns `Ok(())` on success.
1263    pub async fn fab_add_to_library(&self, listing_uid: &str) -> Result<(), EpicAPIError> {
1264        self.egs.fab_add_to_library(listing_uid).await
1265    }
1266
1267    /// Fetch all available asset formats for a listing. Returns `None` on error.
1268    pub async fn fab_listing_formats(
1269        &self,
1270        listing_uid: &str,
1271    ) -> Option<Vec<fab_search::FabListingFormat>> {
1272        self.egs.fab_listing_formats(listing_uid).await.ok()
1273    }
1274
1275    /// Fetch all available asset formats. Returns full `Result`.
1276    pub async fn try_fab_listing_formats(
1277        &self,
1278        listing_uid: &str,
1279    ) -> Result<Vec<fab_search::FabListingFormat>, EpicAPIError> {
1280        self.egs.fab_listing_formats(listing_uid).await
1281    }
1282
1283    // --- Cosmos Policy/Communication ---
1284
1285    /// Check Age of Digital Consent policy. Returns `None` on error.
1286    pub async fn cosmos_policy_aodc(&self) -> Option<cosmos::CosmosPolicyAodc> {
1287        self.egs.cosmos_policy_aodc().await.ok()
1288    }
1289
1290    /// Check Age of Digital Consent policy. Returns full `Result`.
1291    pub async fn try_cosmos_policy_aodc(
1292        &self,
1293    ) -> Result<cosmos::CosmosPolicyAodc, EpicAPIError> {
1294        self.egs.cosmos_policy_aodc().await
1295    }
1296
1297    /// Check communication opt-in status. Returns `None` on error.
1298    pub async fn cosmos_comm_opt_in(
1299        &self,
1300        setting: &str,
1301    ) -> Option<cosmos::CosmosCommOptIn> {
1302        self.egs.cosmos_comm_opt_in(setting).await.ok()
1303    }
1304
1305    /// Check communication opt-in status. Returns full `Result`.
1306    pub async fn try_cosmos_comm_opt_in(
1307        &self,
1308        setting: &str,
1309    ) -> Result<cosmos::CosmosCommOptIn, EpicAPIError> {
1310        self.egs.cosmos_comm_opt_in(setting).await
1311    }
1312
1313    /// Search unrealengine.com content. Requires an active Cosmos session.
1314    ///
1315    /// Returns `None` on any error.
1316    pub async fn cosmos_search(
1317        &self,
1318        query: &str,
1319        slug: Option<&str>,
1320        locale: Option<&str>,
1321        filter: Option<&str>,
1322    ) -> Option<cosmos::CosmosSearchResults> {
1323        self.try_cosmos_search(query, slug, locale, filter).await.ok()
1324    }
1325
1326    /// Like [`cosmos_search`](Self::cosmos_search), but returns a `Result` instead of swallowing errors.
1327    pub async fn try_cosmos_search(
1328        &self,
1329        query: &str,
1330        slug: Option<&str>,
1331        locale: Option<&str>,
1332        filter: Option<&str>,
1333    ) -> Result<cosmos::CosmosSearchResults, EpicAPIError> {
1334        self.egs.cosmos_search(query, slug, locale, filter).await
1335    }
1336
1337    /// Verify the current OAuth token and get account/session info.
1338    ///
1339    /// Returns `None` on any error.
1340    pub async fn verify_access_token(&self, include_perms: bool) -> Option<TokenVerification> {
1341        self.try_verify_access_token(include_perms).await.ok()
1342    }
1343
1344    /// Like [`verify_access_token`](Self::verify_access_token), but returns a `Result` instead of swallowing errors.
1345    pub async fn try_verify_access_token(
1346        &self,
1347        include_perms: bool,
1348    ) -> Result<TokenVerification, EpicAPIError> {
1349        self.egs.verify_token(include_perms).await
1350    }
1351}
1352
1353#[cfg(test)]
1354mod facade_tests {
1355    use super::*;
1356    use crate::api::types::account::UserData;
1357    use chrono::{Duration, Utc};
1358
1359    #[test]
1360    fn new_creates_instance() {
1361        let egs = EpicGames::new();
1362        assert!(!egs.is_logged_in());
1363    }
1364
1365    #[test]
1366    fn default_same_as_new() {
1367        let egs = EpicGames::default();
1368        assert!(!egs.is_logged_in());
1369    }
1370
1371    #[test]
1372    fn user_details_default_empty() {
1373        let egs = EpicGames::new();
1374        assert!(egs.user_details().access_token.is_none());
1375    }
1376
1377    #[test]
1378    fn set_and_get_user_details() {
1379        let mut egs = EpicGames::new();
1380        let mut ud = UserData::new();
1381        ud.display_name = Some("TestUser".to_string());
1382        egs.set_user_details(ud);
1383        assert_eq!(egs.user_details().display_name, Some("TestUser".to_string()));
1384    }
1385
1386    #[test]
1387    fn is_logged_in_expired() {
1388        let mut egs = EpicGames::new();
1389        let mut ud = UserData::new();
1390        ud.expires_at = Some(Utc::now() - Duration::hours(1));
1391        egs.set_user_details(ud);
1392        assert!(!egs.is_logged_in());
1393    }
1394
1395    #[test]
1396    fn is_logged_in_valid() {
1397        let mut egs = EpicGames::new();
1398        let mut ud = UserData::new();
1399        ud.expires_at = Some(Utc::now() + Duration::hours(2));
1400        egs.set_user_details(ud);
1401        assert!(egs.is_logged_in());
1402    }
1403
1404    #[test]
1405    fn is_logged_in_within_600s_threshold() {
1406        let mut egs = EpicGames::new();
1407        let mut ud = UserData::new();
1408        ud.expires_at = Some(Utc::now() + Duration::seconds(500));
1409        egs.set_user_details(ud);
1410        assert!(!egs.is_logged_in());
1411    }
1412}