diff --git a/ankihub/ankihub_client/ankihub_client.py b/ankihub/ankihub_client/ankihub_client.py index 5bde61efe..81baeb898 100644 --- a/ankihub/ankihub_client/ankihub_client.py +++ b/ankihub/ankihub_client/ankihub_client.py @@ -1259,12 +1259,15 @@ def media_upload_finished(self, ah_did: uuid.UUID) -> None: if response.status_code != 204: raise AnkiHubHTTPError(response) - def owned_deck_ids(self) -> List[uuid.UUID]: + def get_user_details(self) -> Dict[str, Any]: response = self._send_request("GET", API.ANKIHUB, "/users/me") if response.status_code != 200: raise AnkiHubHTTPError(response) - data = response.json() + return response.json() + + def owned_deck_ids(self) -> List[uuid.UUID]: + data = self.get_user_details() result = [uuid.UUID(deck["id"]) for deck in data["created_decks"]] return result @@ -1290,6 +1293,10 @@ def send_daily_card_review_summaries( if response.status_code != 201: raise AnkiHubHTTPError(response) + def is_premium_or_trialing(self) -> bool: + data = self.get_user_details() + return data["is_premium"] or data["is_trialing"] + class ThreadLocalSession: def __init__(self): diff --git a/ankihub/gui/js_message_handling.py b/ankihub/gui/js_message_handling.py index b8cfa8d4a..b0206a664 100644 --- a/ankihub/gui/js_message_handling.py +++ b/ankihub/gui/js_message_handling.py @@ -16,9 +16,8 @@ from jinja2 import Template from ..db import ankihub_db -from ..settings import url_plans_page, url_view_note +from ..settings import url_view_note from .operations.scheduling import suspend_notes, unsuspend_notes -from .utils import show_dialog VIEW_NOTE_PYCMD = "ankihub_view_note" VIEW_NOTE_BUTTON_ID = "ankihub-view-note-button" @@ -27,7 +26,6 @@ UNSUSPEND_NOTES_PYCMD = "ankihub_unsuspend_notes" SUSPEND_NOTES_PYCMD = "ankihub_suspend_notes" GET_NOTE_SUSPENSION_STATES_PYCMD = "ankihub_get_note_suspension_states" -ANKIHUB_UPSELL = "ankihub_ai_upsell" COPY_TO_CLIPBOARD_PYCMD = "ankihub_copy_to_clipboard" OPEN_LINK_PYCMD = "ankihub_open_link" @@ -95,22 +93,6 @@ def _on_js_message(handled: Tuple[bool, Any], message: str, context: Any) -> Any web=reviewer_sidebar.content_webview, ) return (True, None) - elif message == ANKIHUB_UPSELL: - - def on_button_clicked(button_index: int) -> None: - if button_index == 1: - openLink(url_plans_page()) - - show_dialog( - text="Upgrade your membership to Premium to access this feature 🌟", - title="Your trial has ended!", - buttons=[ - ("Cancel", aqt.QDialogButtonBox.ButtonRole.RejectRole), - ("Upgrade", aqt.QDialogButtonBox.ButtonRole.ActionRole), - ], - default_button_idx=1, - callback=on_button_clicked, - ) elif message.startswith(COPY_TO_CLIPBOARD_PYCMD): kwargs = parse_js_message_kwargs(message) content = kwargs.get("content") diff --git a/ankihub/gui/reviewer.py b/ankihub/gui/reviewer.py index 6044297b9..62231f4b2 100644 --- a/ankihub/gui/reviewer.py +++ b/ankihub/gui/reviewer.py @@ -29,11 +29,7 @@ from ..gui.webview import AuthenticationRequestInterceptor, CustomWebPage # noqa: F401 from ..main.utils import mh_tag_to_resource_title_and_slug from ..settings import config, url_login, url_mh_integrations_preview -from .js_message_handling import ( - ANKIHUB_UPSELL, - VIEW_NOTE_PYCMD, - parse_js_message_kwargs, -) +from .js_message_handling import VIEW_NOTE_PYCMD, parse_js_message_kwargs from .utils import get_ah_did_of_deck_or_ancestor_deck, using_qt5 from .web.templates import ( get_empty_state_html, @@ -619,9 +615,6 @@ def _on_js_message(handled: Tuple[bool, Any], message: str, context: Any) -> Any openLink(url) return True, None - elif message == ANKIHUB_UPSELL: - reviewer_sidebar.close_sidebar() - return True, None return handled diff --git a/ankihub/gui/web/media/_chatbot_icon_sleeping.svg b/ankihub/gui/web/media/_chatbot_icon_sleeping.svg new file mode 100644 index 000000000..a25189162 --- /dev/null +++ b/ankihub/gui/web/media/_chatbot_icon_sleeping.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ankihub/gui/web/media/_chatbot_icon_sleeping_dark_theme.svg b/ankihub/gui/web/media/_chatbot_icon_sleeping_dark_theme.svg new file mode 100644 index 000000000..a2023e7e1 --- /dev/null +++ b/ankihub/gui/web/media/_chatbot_icon_sleeping_dark_theme.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ankihub/gui/web/reviewer_buttons.js b/ankihub/gui/web/reviewer_buttons.js index 3afe9f239..3f97c0055 100644 --- a/ankihub/gui/web/reviewer_buttons.js +++ b/ankihub/gui/web/reviewer_buttons.js @@ -3,6 +3,7 @@ class AnkiHubReviewerButtons { constructor() { this.theme = "{{ THEME }}"; + this.isPremiumOrTrialing = "{{ IS_PREMIUM_OR_TRIALING }}" == "True"; this.isAnKingDeck = null; this.bbCount = 0; this.faCount = 0; @@ -35,7 +36,8 @@ class AnkiHubReviewerButtons { }, { name: "chatbot", - iconPath: "/_chatbot_icon.svg", + iconPath: this.isPremiumOrTrialing ? "/_chatbot_icon.svg" : "/_chatbot_icon_sleeping.svg", + iconPathDarkTheme: this.isPremiumOrTrialing ? null : "/_chatbot_icon_sleeping_dark_theme.svg", active: false, tooltip: "AI Chatbot" }, diff --git a/ankihub/gui/web/templates.py b/ankihub/gui/web/templates.py index 8a023678f..2353489f6 100644 --- a/ankihub/gui/web/templates.py +++ b/ankihub/gui/web/templates.py @@ -3,6 +3,8 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape +from ...addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient + TEMPLATES_PATH = (pathlib.Path(__file__).parent).absolute() env = Environment( @@ -24,10 +26,12 @@ def get_header_webview_html( def get_reviewer_buttons_js(theme: str, enabled_buttons: List[str]) -> str: + client = AnkiHubClient() return env.get_template("reviewer_buttons.js").render( { "THEME": theme, "ENABLED_BUTTONS": ",".join(enabled_buttons), + "IS_PREMIUM_OR_TRIALING": str(client.is_premium_or_trialing()), } ) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 2db16ce1d..13fc07b39 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -6019,9 +6019,15 @@ def mock_using_qt5_to_return_false(mocker: MockerFixture): mocker.patch("ankihub.gui.reviewer.using_qt5", return_value=False) +@pytest.fixture +def mock_user_details(mocker: MockerFixture): + user_details = {"is_premium": True, "is_trialing": False} + mocker.patch.object(AnkiHubClient, "get_user_details", return_value=user_details) + + # The mock_using_qt5_to_return_false fixture is used to test the AnkiHub AI feature on Qt5, # even though the feature is disabled on Qt5. (In CI we are only running test on Qt5.) -@pytest.mark.usefixtures("mock_using_qt5_to_return_false") +@pytest.mark.usefixtures("mock_using_qt5_to_return_false", "mock_user_details") class TestAnkiHubAIInReviewer: @pytest.mark.sequential @pytest.mark.parametrize(