itsjafer / schwab-api

A python library for placing trades on Charles Schwab
MIT License
213 stars 66 forks source link

Login fails when voice id is pending activation #72

Open JasonGross opened 1 month ago

JasonGross commented 1 month ago

If Voice ID is partially set up, the login page redirects to https://client.schwab.com/service/forms/VoiceBiometric instead of whichever page was selected: debug_screenshot The relevant HTML fragment for clicking "Cancel", which just skips voice activation enabling and going to the correct post-login page, is

<div class="background-Highlight">
    <div id="divEnroll" class="section-msg">
        <div id="divCmsReadyToEnroll">
            <b>You're almost done!</b> Click Activate Voice ID and use your new voice ID the next time you call Schwab.
        </div>
        <div id="divButtonAreaEnroll" class="space-bttonarea">
            <a role="button" id="btnCancel" class="button-secondary" href="#">
                <span>Cancel</span>
            </a>
                <a role="button" id="btnActivateVoiceId" class="button-primary" href="/Service/Forms/VoiceBiometric/VoiceBiometricActivate">
                    <span>Activate Voice ID</span>
                </a>
        </div>
    </div>
</div>

(Full page html is voice_activation_page_redacted.html.zip) It would be nice if https://github.com/itsjafer/schwab-api/blob/4ecc6242c58d93c6423bca2f1aa06e2260f25b31/schwab_api/authentication.py#L167 would also wait for service/forms/VoiceBiometric, and cancel if necessary.

JasonGross commented 1 month ago

For future reference, I debugged this with

        try:
            await self.page.frame(name=login_frame).press("[placeholder=\"Password\"]", "Enter")
            await self.page.wait_for_url(re.compile(r"app/trade"), wait_until="domcontentloaded") # Making it more robust than specifying an exact url which may change.
        except TimeoutError as e:
            # Capture screenshot
            await self.page.screenshot(path="debug_screenshot.png")

            # Save page content
            page_content = await self.page.content()
            with open("debug_page_content.html", "w", encoding="utf-8") as f:
                f.write(page_content)

            print(f"Current URL: {self.page.url}")
            raise RuntimeError(f"Login was not successful; please check username and password. Debug info saved to {os.path.abspath('debug_screenshot.png')} and {os.path.abspath('debug_page_content.html')}") from e