schwab-developer 0.1.0

A library for Charles Schwab API.
Documentation
use std::{io::Write as _, path::Path};
use chrono::Utc;

use crate::{common::client::Client, auth::{config::AuthConfig, oauth::OAuthApi, store::TokenStore}};

const TOKEN_FILE: &str = "schwab_tokens.json";

pub struct Provider {
    store: TokenStore,
    api: OAuthApi,
}

impl Provider {
    pub fn new() -> Self {
        let config = AuthConfig::new();
        let api = OAuthApi::new(config);
        let store = TokenStore::new(TOKEN_FILE);
        Self {
            api,
            store,
        }
    }

    pub async fn login(&self) -> anyhow::Result<()> {
        if !Path::new(TOKEN_FILE).exists() {
            println!("=== INITIAL SCHWAB OAUTH SETUP ===");
            println!("1. Click this URL and log in:\n{}\n", self.api.generate_auth_url());
            
            print!("2. Paste the full redirect URL (starts with https://127.0.0.1...): ");
            std::io::stdout().flush()?;
            
            let mut returned_url = String::new();
            std::io::stdin().read_line(&mut returned_url)?;
            let returned_url = returned_url.trim();

            let code = self.api.extract_code_from_url(returned_url)?;
            println!("Exchanging code for permanent tokens...");
            let token = self.api.exchange_code_for_tokens(&code).await?;
            self.store.save(&token)?;
            println!("✅ Tokens saved to schwab_tokens.json successfully!");
        } else {
            println!("Checking/Verifying existing token pipeline...");
            if let Some(token) = self.get_valid_token().await {
                let truncated = if token.len() > 10 { &token[..10] } else { &token };
                println!("Active Access Token: {}...[TRUNCATED]", truncated);
            }
        }
        Ok(())
    }

    async fn get_valid_token(&self) -> Option<String> {
        let Some(mut token_data) = self.store.load().await else {
            return None;
        };
        
        if Utc::now().timestamp() >= (token_data.expires_at - 120) {
            token_data = self.api.refresh_access_token(&token_data.refresh_token).await.ok()?;
        }
        Some(token_data.access_token)
    }

    pub async fn client(&self) -> anyhow::Result<Client> {
        let Some(token) = self.get_valid_token().await else {
            anyhow::bail!("Please login first");
        };
        Ok(Client::new(&token))
    }
}