From b31038d561e2229f0c4c340456c22e08baf98510 Mon Sep 17 00:00:00 2001
From: Jakub Fidler <31575114+RisingOrange@users.noreply.github.com>
Date: Wed, 3 Jan 2024 12:51:43 +0100
Subject: [PATCH 1/2] chore: Fix flaky optional tag suggestion dialog test
 (#857)

---
 tests/addon/test_unit.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/addon/test_unit.py b/tests/addon/test_unit.py
index 8de76459a..46d3f3fb3 100644
--- a/tests/addon/test_unit.py
+++ b/tests/addon/test_unit.py
@@ -2563,5 +2563,6 @@ def test_submit_without_review_checkbox_hidden_when_user_cant_use_it(
             )
 
             dialog.show()
+            qtbot.wait(500)
 
             assert dialog.auto_accept_cb.isVisible() == expected_checkbox_is_visible

From e059fd6697ffdf55d8fc792596bdb124c5b07c70 Mon Sep 17 00:00:00 2001
From: Jakub Fidler <31575114+RisingOrange@users.noreply.github.com>
Date: Fri, 5 Jan 2024 19:27:22 +0100
Subject: [PATCH 2/2] feat: update addon login window (#842)

---
 ankihub/gui/menu.py      | 46 ++++++++++++++++++++++++++++++----
 tests/addon/test_unit.py | 53 ++++++++++++++++++++++++++++++++++++++--
 2 files changed, 92 insertions(+), 7 deletions(-)

diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py
index 53c96c31a..107e574df 100644
--- a/ankihub/gui/menu.py
+++ b/ankihub/gui/menu.py
@@ -100,23 +100,51 @@ def __init__(self):
 
         # Password
         self.password_box = QHBoxLayout()
+
         self.password_box_label = QLabel("Password:")
+
         self.password_box_text = QLineEdit("", self)
         self.password_box_text.setEchoMode(QLineEdit.EchoMode.Password)
         self.password_box_text.setMinimumWidth(300)
         qconnect(self.password_box_text.returnPressed, self.login)
+
+        self.toggle_button = QPushButton("Show")
+        self.toggle_button.setCheckable(True)
+        self.toggle_button.setFixedHeight(30)
+        qconnect(self.toggle_button.toggled, self.refresh_password_visibility)
+
         self.password_box.addWidget(self.password_box_label)
         self.password_box.addWidget(self.password_box_text)
+        self.password_box.addWidget(self.toggle_button)
         self.box_left.addLayout(self.password_box)
 
-        # Login
-        self.login_button = QPushButton("Login", self)
+        # Sign in button
+        self.login_button = QPushButton("Sign in", self)
         self.bottom_box_section.addWidget(self.login_button)
         qconnect(self.login_button.clicked, self.login)
         self.login_button.setDefault(True)
-
+        self.bottom_box_section.setContentsMargins(0, 12, 0, 12)
         self.box_left.addLayout(self.bottom_box_section)
 
+        # Sign up / forgot password text
+        self.sign_up_and_recover_password_container = QVBoxLayout()
+        self.sign_up_and_recover_password_container.setSpacing(8)
+        self.sign_up_and_recover_password_container.setContentsMargins(0, 0, 0, 5)
+        self.login_button.setDefault(True)
+        self.sign_up_help_text = QLabel(
+            'Don\'t have a AnkiHub account? <a href="https://app.ankihub.net/accounts/signup/">Register now</a>'
+        )
+        self.sign_up_help_text.setOpenExternalLinks(True)
+        self.recover_password_help_text = QLabel(
+            '<a href="https://app.ankihub.net/accounts/password/reset/">Forgot password?</a>'
+        )
+        self.recover_password_help_text.setOpenExternalLinks(True)
+        self.sign_up_and_recover_password_container.addWidget(self.sign_up_help_text)
+        self.sign_up_and_recover_password_container.addWidget(
+            self.recover_password_help_text
+        )
+        self.box_left.addLayout(self.sign_up_and_recover_password_container)
+
         # Add left and right layouts to upper
         self.box_upper.addLayout(self.box_left)
         self.box_upper.addSpacing(20)
@@ -127,12 +155,20 @@ def __init__(self):
         self.box_top.addStretch(1)
         self.setLayout(self.box_top)
 
-        self.setMinimumWidth(500)
+        self.setContentsMargins(20, 5, 0, 5)
         self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
-        self.setWindowTitle("Login to AnkiHub.")
+        self.setWindowTitle("Sign in to AnkiHub.")
         self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)  # type: ignore
         self.show()
 
+    def refresh_password_visibility(self) -> None:
+        if self.toggle_button.isChecked():
+            self.password_box_text.setEchoMode(QLineEdit.EchoMode.Normal)
+            self.toggle_button.setText("Hide")
+        else:
+            self.password_box_text.setEchoMode(QLineEdit.EchoMode.Password)
+            self.toggle_button.setText("Show")
+
     def login(self):
         username_or_email = self.username_or_email_box_text.text()
         password = self.password_box_text.text()
diff --git a/tests/addon/test_unit.py b/tests/addon/test_unit.py
index 46d3f3fb3..471669529 100644
--- a/tests/addon/test_unit.py
+++ b/tests/addon/test_unit.py
@@ -10,14 +10,14 @@
 from textwrap import dedent
 from time import sleep
 from typing import Callable, ContextManager, Generator, List, Optional, Protocol, Tuple
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
 
 import aqt
 import pytest
 from anki.decks import DeckId
 from anki.models import NotetypeDict
 from anki.notes import Note, NoteId
-from aqt import QMenu
+from aqt import QLineEdit, QMenu
 from aqt.qt import QDialog, QDialogButtonBox, Qt, QTimer, QWidget
 from pytest import MonkeyPatch
 from pytest_anki import AnkiSession
@@ -590,6 +590,55 @@ def test_login(
         assert config.user() == username
         assert config.token() == token
 
+    @patch("ankihub.gui.menu.AnkiHubClient.login")
+    def test_password_visibility_toggle(self, login_mock, qtbot: QtBot):
+        password = "test_password"
+
+        AnkiHubLogin.display_login()
+
+        window: AnkiHubLogin = AnkiHubLogin._window
+        window.password_box_text.setText(password)
+
+        # assert password is not visible and toggle button is at the initial state
+        assert window.password_box_text.echoMode() == QLineEdit.EchoMode.Password
+        assert window.toggle_button.isChecked() is False
+
+        window.toggle_button.click()
+        qtbot.wait_until(
+            lambda: window.password_box_text.echoMode() == QLineEdit.EchoMode.Normal
+        )
+
+        assert window.password_box_text.echoMode() == QLineEdit.EchoMode.Normal
+        assert window.toggle_button.isChecked() is True
+
+        window.toggle_button.click()
+        qtbot.wait_until(
+            lambda: window.password_box_text.echoMode() == QLineEdit.EchoMode.Password
+        )
+
+        assert window.password_box_text.echoMode() == QLineEdit.EchoMode.Password
+        assert window.toggle_button.isChecked() is False
+
+    @patch("ankihub.gui.menu.AnkiHubClient.login")
+    def test_forgot_password_and_sign_up_links_are_present(
+        self, login_mock, qtbot: QtBot
+    ):
+        AnkiHubLogin.display_login()
+
+        window: AnkiHubLogin = AnkiHubLogin._window
+
+        assert window.sign_up_help_text.openExternalLinks() is True
+        assert (
+            window.sign_up_help_text.text()
+            == 'Don\'t have a AnkiHub account? <a href="https://app.ankihub.net/accounts/signup/">Register now</a>'
+        )
+
+        assert window.recover_password_help_text.openExternalLinks() is True
+        assert (
+            window.recover_password_help_text.text()
+            == '<a href="https://app.ankihub.net/accounts/password/reset/">Forgot password?</a>'
+        )
+
 
 class TestSuggestionDialog:
     @pytest.mark.parametrize(