bauiv1lib.account.settings

Provides UI for account functionality.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""Provides UI for account functionality."""
   4# pylint: disable=too-many-lines
   5
   6from __future__ import annotations
   7
   8import time
   9import logging
  10from typing import override
  11
  12from bacommon.cloud import WebLocation
  13from bacommon.login import LoginType
  14import bacommon.cloud
  15import bauiv1 as bui
  16
  17
  18# These days we're directing people to the web based account settings
  19# for V2 account linking and trying to get them to disconnect remaining
  20# V1 links, but leaving this escape hatch here in case needed.
  21FORCE_ENABLE_V1_LINKING = False
  22
  23
  24class AccountSettingsWindow(bui.MainWindow):
  25    """Window for account related functionality."""
  26
  27    def __init__(
  28        self,
  29        transition: str | None = 'in_right',
  30        modal: bool = False,
  31        origin_widget: bui.Widget | None = None,
  32        close_once_signed_in: bool = False,
  33    ):
  34        # pylint: disable=too-many-statements
  35
  36        plus = bui.app.plus
  37        assert plus is not None
  38
  39        self._sign_in_v2_proxy_button: bui.Widget | None = None
  40        self._sign_in_device_button: bui.Widget | None = None
  41
  42        self._show_legacy_unlink_button = False
  43
  44        self._signing_in_adapter: bui.LoginAdapter | None = None
  45        self._close_once_signed_in = close_once_signed_in
  46        bui.set_analytics_screen('Account Window')
  47
  48        self._explicitly_signed_out_of_gpgs = False
  49
  50        self._r = 'accountSettingsWindow'
  51        self._modal = modal
  52        self._needs_refresh = False
  53        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
  54        self._v1_account_state_num = plus.get_v1_account_state_num()
  55        self._check_sign_in_timer = bui.AppTimer(
  56            1.0, bui.WeakCall(self._update), repeat=True
  57        )
  58
  59        self._can_reset_achievements = False
  60
  61        app = bui.app
  62        assert app.classic is not None
  63        uiscale = app.ui_v1.uiscale
  64
  65        self._width = 850 if uiscale is bui.UIScale.SMALL else 660
  66        x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
  67        self._height = (
  68            380
  69            if uiscale is bui.UIScale.SMALL
  70            else 430 if uiscale is bui.UIScale.MEDIUM else 490
  71        )
  72
  73        self._sign_in_button = None
  74        self._sign_in_text = None
  75
  76        self._scroll_width = self._width - (100 + x_offs * 2)
  77        self._scroll_height = self._height - 120
  78        self._sub_width = self._scroll_width - 20
  79
  80        # Determine which sign-in/sign-out buttons we should show.
  81        self._show_sign_in_buttons: list[str] = []
  82
  83        if LoginType.GPGS in plus.accounts.login_adapters:
  84            self._show_sign_in_buttons.append('Google Play')
  85
  86        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
  87            self._show_sign_in_buttons.append('Game Center')
  88
  89        # Always want to show our web-based v2 login option.
  90        self._show_sign_in_buttons.append('V2Proxy')
  91
  92        # Legacy v1 device accounts available only if the user
  93        # has explicitly enabled deprecated login types.
  94        if bui.app.config.resolve('Show Deprecated Login Types'):
  95            self._show_sign_in_buttons.append('Device')
  96
  97        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
  98        super().__init__(
  99            root_widget=bui.containerwidget(
 100                size=(self._width, self._height + top_extra),
 101                # transition=transition,
 102                toolbar_visibility=(
 103                    'menu_minimal'
 104                    if uiscale is bui.UIScale.SMALL
 105                    else 'menu_full'
 106                ),
 107                scale=(
 108                    2.07
 109                    if uiscale is bui.UIScale.SMALL
 110                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 111                ),
 112                stack_offset=(
 113                    (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
 114                ),
 115            ),
 116            transition=transition,
 117            origin_widget=origin_widget,
 118        )
 119        if uiscale is bui.UIScale.SMALL:
 120            self._back_button = None
 121            bui.containerwidget(
 122                edit=self._root_widget, on_cancel_call=self.main_window_back
 123            )
 124        else:
 125            self._back_button = btn = bui.buttonwidget(
 126                parent=self._root_widget,
 127                position=(51 + x_offs, self._height - 62),
 128                size=(120, 60),
 129                scale=0.8,
 130                text_scale=1.2,
 131                autoselect=True,
 132                label=bui.Lstr(
 133                    resource='doneText' if self._modal else 'backText'
 134                ),
 135                button_type='regular' if self._modal else 'back',
 136                on_activate_call=self.main_window_back,
 137            )
 138            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 139            if not self._modal:
 140                bui.buttonwidget(
 141                    edit=btn,
 142                    button_type='backSmall',
 143                    size=(60, 56),
 144                    label=bui.charstr(bui.SpecialChar.BACK),
 145                )
 146
 147        titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
 148        titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
 149        bui.textwidget(
 150            parent=self._root_widget,
 151            position=(self._width * 0.5, self._height - 41 + titleyoffs),
 152            size=(0, 0),
 153            text=bui.Lstr(resource=f'{self._r}.titleText'),
 154            color=app.ui_v1.title_color,
 155            scale=titlescale,
 156            maxwidth=self._width - 340,
 157            h_align='center',
 158            v_align='center',
 159        )
 160
 161        self._scrollwidget = bui.scrollwidget(
 162            parent=self._root_widget,
 163            highlight=False,
 164            position=(
 165                (self._width - self._scroll_width) * 0.5,
 166                self._height - 65 - self._scroll_height,
 167            ),
 168            size=(self._scroll_width, self._scroll_height),
 169            claims_left_right=True,
 170            claims_tab=True,
 171            selection_loops_to_parent=True,
 172        )
 173        self._subcontainer: bui.Widget | None = None
 174        self._refresh()
 175        self._restore_state()
 176
 177    @override
 178    def get_main_window_state(self) -> bui.MainWindowState:
 179        # Support recreating our window for back/refresh purposes.
 180        cls = type(self)
 181        return bui.BasicMainWindowState(
 182            create_call=lambda transition, origin_widget: cls(
 183                transition=transition, origin_widget=origin_widget
 184            )
 185        )
 186
 187    @override
 188    def on_main_window_close(self) -> None:
 189        self._save_state()
 190
 191    def _update(self) -> None:
 192        plus = bui.app.plus
 193        assert plus is not None
 194
 195        # If they want us to close once we're signed in, do so.
 196        if self._close_once_signed_in and self._v1_signed_in:
 197            self.main_window_back()
 198            return
 199
 200        # Hmm should update this to use get_account_state_num.
 201        # Theoretically if we switch from one signed-in account to another
 202        # in the background this would break.
 203        v1_account_state_num = plus.get_v1_account_state_num()
 204        v1_account_state = plus.get_v1_account_state()
 205        show_legacy_unlink_button = self._should_show_legacy_unlink_button()
 206
 207        if (
 208            v1_account_state_num != self._v1_account_state_num
 209            or show_legacy_unlink_button != self._show_legacy_unlink_button
 210            or self._needs_refresh
 211        ):
 212            self._v1_account_state_num = v1_account_state_num
 213            self._v1_signed_in = v1_account_state == 'signed_in'
 214            self._show_legacy_unlink_button = show_legacy_unlink_button
 215            self._refresh()
 216
 217        # Go ahead and refresh some individual things
 218        # that may change under us.
 219        self._update_linked_accounts_text()
 220        self._update_unlink_accounts_button()
 221        self._refresh_campaign_progress_text()
 222        self._refresh_achievements()
 223        self._refresh_tickets_text()
 224        self._refresh_account_name_text()
 225
 226    def _refresh(self) -> None:
 227        # pylint: disable=too-many-statements
 228        # pylint: disable=too-many-branches
 229        # pylint: disable=too-many-locals
 230        # pylint: disable=cyclic-import
 231
 232        plus = bui.app.plus
 233        assert plus is not None
 234
 235        via_lines: list[str] = []
 236
 237        primary_v2_account = plus.accounts.primary
 238
 239        v1_state = plus.get_v1_account_state()
 240        v1_account_type = (
 241            plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown'
 242        )
 243
 244        # We expose GPGS-specific functionality only if it is 'active'
 245        # (meaning the current GPGS player matches one of our account's
 246        # logins).
 247        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
 248        gpgs_active = adapter is not None and adapter.is_back_end_active()
 249
 250        # Ditto for Game Center.
 251        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
 252        game_center_active = (
 253            adapter is not None and adapter.is_back_end_active()
 254        )
 255
 256        show_signed_in_as = self._v1_signed_in
 257        signed_in_as_space = 95.0
 258
 259        # To reduce confusion about the whole V2 account situation for
 260        # people used to seeing their Google Play Games or Game Center
 261        # account name and icon and whatnot, let's show those underneath
 262        # the V2 tag to help communicate that they are in fact logged in
 263        # through that account.
 264        via_space = 25.0
 265        if show_signed_in_as and bui.app.plus is not None:
 266            accounts = bui.app.plus.accounts
 267            if accounts.primary is not None:
 268                # For these login types, we show 'via' IF there is a
 269                # login of that type attached to our account AND it is
 270                # currently active (We don't want to show 'via Game
 271                # Center' if we're signed out of Game Center or
 272                # currently running on Steam, even if there is a Game
 273                # Center login attached to our account).
 274                for ltype, lchar in [
 275                    (LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 276                    (LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
 277                ]:
 278                    linfo = accounts.primary.logins.get(ltype)
 279                    ladapter = accounts.login_adapters.get(ltype)
 280                    if (
 281                        linfo is not None
 282                        and ladapter is not None
 283                        and ladapter.is_back_end_active()
 284                    ):
 285                        via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
 286
 287                # TEMP TESTING
 288                if bool(False):
 289                    icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 290                    via_lines.append(f'{icontxt}FloofDibble')
 291                    icontxt = bui.charstr(
 292                        bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
 293                    )
 294                    via_lines.append(f'{icontxt}StinkBobble')
 295
 296        show_sign_in_benefits = not self._v1_signed_in
 297        sign_in_benefits_space = 80.0
 298
 299        show_signing_in_text = (
 300            v1_state == 'signing_in' or self._signing_in_adapter is not None
 301        )
 302        signing_in_text_space = 80.0
 303
 304        show_google_play_sign_in_button = (
 305            v1_state == 'signed_out'
 306            and self._signing_in_adapter is None
 307            and 'Google Play' in self._show_sign_in_buttons
 308        )
 309        show_game_center_sign_in_button = (
 310            v1_state == 'signed_out'
 311            and self._signing_in_adapter is None
 312            and 'Game Center' in self._show_sign_in_buttons
 313        )
 314        show_v2_proxy_sign_in_button = (
 315            v1_state == 'signed_out'
 316            and self._signing_in_adapter is None
 317            and 'V2Proxy' in self._show_sign_in_buttons
 318        )
 319        show_device_sign_in_button = (
 320            v1_state == 'signed_out'
 321            and self._signing_in_adapter is None
 322            and 'Device' in self._show_sign_in_buttons
 323        )
 324        sign_in_button_space = 70.0
 325        deprecated_space = 60
 326
 327        # Game Center currently has a single UI for everything.
 328        show_game_service_button = game_center_active
 329        game_service_button_space = 60.0
 330
 331        # Phasing this out (for V2 accounts at least).
 332        show_linked_accounts_text = (
 333            self._v1_signed_in and v1_account_type != 'V2'
 334        )
 335        linked_accounts_text_space = 60.0
 336
 337        # Update: No longer showing this since its visible on main
 338        # toolbar.
 339        show_achievements_text = False
 340        achievements_text_space = 27.0
 341
 342        show_leaderboards_button = self._v1_signed_in and gpgs_active
 343        leaderboards_button_space = 60.0
 344
 345        # Update: No longer showing this; trying to get progress type
 346        # stuff out of the account panel.
 347        # show_campaign_progress = self._v1_signed_in
 348        show_campaign_progress = False
 349        campaign_progress_space = 27.0
 350
 351        # show_tickets = self._v1_signed_in
 352        show_tickets = False
 353        tickets_space = 27.0
 354
 355        show_manage_account_button = primary_v2_account is not None
 356        manage_account_button_space = 70.0
 357
 358        show_delete_account_button = primary_v2_account is not None
 359        delete_account_button_space = 70.0
 360
 361        show_link_accounts_button = self._v1_signed_in and (
 362            primary_v2_account is None or FORCE_ENABLE_V1_LINKING
 363        )
 364        link_accounts_button_space = 70.0
 365
 366        show_unlink_accounts_button = show_link_accounts_button
 367        unlink_accounts_button_space = 90.0
 368
 369        # Phasing this out.
 370        show_v2_link_info = False
 371        v2_link_info_space = 70.0
 372
 373        legacy_unlink_button_space = 120.0
 374
 375        show_sign_out_button = primary_v2_account is not None or (
 376            self._v1_signed_in and v1_account_type == 'Local'
 377        )
 378        sign_out_button_space = 70.0
 379
 380        # We can show cancel if we're either waiting on an adapter to
 381        # provide us with v2 credentials or waiting for those
 382        # credentials to be verified.
 383        show_cancel_sign_in_button = self._signing_in_adapter is not None or (
 384            plus.accounts.have_primary_credentials()
 385            and primary_v2_account is None
 386        )
 387        cancel_sign_in_button_space = 70.0
 388
 389        if self._subcontainer is not None:
 390            self._subcontainer.delete()
 391        self._sub_height = 60.0
 392        if show_signed_in_as:
 393            self._sub_height += signed_in_as_space
 394        self._sub_height += via_space * len(via_lines)
 395        if show_signing_in_text:
 396            self._sub_height += signing_in_text_space
 397        if show_google_play_sign_in_button:
 398            self._sub_height += sign_in_button_space
 399        if show_game_center_sign_in_button:
 400            self._sub_height += sign_in_button_space
 401        if show_v2_proxy_sign_in_button:
 402            self._sub_height += sign_in_button_space
 403        if show_device_sign_in_button:
 404            self._sub_height += sign_in_button_space + deprecated_space
 405        if show_game_service_button:
 406            self._sub_height += game_service_button_space
 407        if show_linked_accounts_text:
 408            self._sub_height += linked_accounts_text_space
 409        if show_achievements_text:
 410            self._sub_height += achievements_text_space
 411        if show_leaderboards_button:
 412            self._sub_height += leaderboards_button_space
 413        if show_campaign_progress:
 414            self._sub_height += campaign_progress_space
 415        if show_tickets:
 416            self._sub_height += tickets_space
 417        if show_sign_in_benefits:
 418            self._sub_height += sign_in_benefits_space
 419        if show_manage_account_button:
 420            self._sub_height += manage_account_button_space
 421        if show_link_accounts_button:
 422            self._sub_height += link_accounts_button_space
 423        if show_unlink_accounts_button:
 424            self._sub_height += unlink_accounts_button_space
 425        if show_v2_link_info:
 426            self._sub_height += v2_link_info_space
 427        if self._show_legacy_unlink_button:
 428            self._sub_height += legacy_unlink_button_space
 429        if show_sign_out_button:
 430            self._sub_height += sign_out_button_space
 431        if show_delete_account_button:
 432            self._sub_height += delete_account_button_space
 433        if show_cancel_sign_in_button:
 434            self._sub_height += cancel_sign_in_button_space
 435        self._subcontainer = bui.containerwidget(
 436            parent=self._scrollwidget,
 437            size=(self._sub_width, self._sub_height),
 438            background=False,
 439            claims_left_right=True,
 440            claims_tab=True,
 441            selection_loops_to_parent=True,
 442        )
 443
 444        first_selectable = None
 445        v = self._sub_height - 10.0
 446
 447        assert bui.app.classic is not None
 448        self._account_name_text: bui.Widget | None
 449        if show_signed_in_as:
 450            v -= signed_in_as_space * 0.2
 451            txt = bui.Lstr(
 452                resource='accountSettingsWindow.youAreSignedInAsText',
 453                fallback_resource='accountSettingsWindow.youAreLoggedInAsText',
 454            )
 455            bui.textwidget(
 456                parent=self._subcontainer,
 457                position=(self._sub_width * 0.5, v),
 458                size=(0, 0),
 459                text=txt,
 460                scale=0.9,
 461                color=bui.app.ui_v1.title_color,
 462                maxwidth=self._sub_width * 0.9,
 463                h_align='center',
 464                v_align='center',
 465            )
 466            v -= signed_in_as_space * 0.5
 467            self._account_name_text = bui.textwidget(
 468                parent=self._subcontainer,
 469                position=(self._sub_width * 0.5, v),
 470                size=(0, 0),
 471                scale=1.5,
 472                maxwidth=self._sub_width * 0.9,
 473                res_scale=1.5,
 474                color=(1, 1, 1, 1),
 475                h_align='center',
 476                v_align='center',
 477            )
 478
 479            self._refresh_account_name_text()
 480
 481            v -= signed_in_as_space * 0.4
 482
 483            for via in via_lines:
 484                v -= via_space * 0.1
 485                sscale = 0.7
 486                swidth = (
 487                    bui.get_string_width(via, suppress_warning=True) * sscale
 488                )
 489                bui.textwidget(
 490                    parent=self._subcontainer,
 491                    position=(self._sub_width * 0.5, v),
 492                    size=(0, 0),
 493                    text=via,
 494                    scale=sscale,
 495                    color=(0.6, 0.6, 0.6),
 496                    flatness=1.0,
 497                    shadow=0.0,
 498                    h_align='center',
 499                    v_align='center',
 500                )
 501                bui.textwidget(
 502                    parent=self._subcontainer,
 503                    position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
 504                    size=(0, 0),
 505                    text=bui.Lstr(
 506                        value='(${VIA}',
 507                        subs=[('${VIA}', bui.Lstr(resource='viaText'))],
 508                    ),
 509                    scale=0.5,
 510                    color=(0.4, 0.6, 0.4, 0.5),
 511                    flatness=1.0,
 512                    shadow=0.0,
 513                    h_align='right',
 514                    v_align='center',
 515                )
 516                bui.textwidget(
 517                    parent=self._subcontainer,
 518                    position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
 519                    size=(0, 0),
 520                    text=')',
 521                    scale=0.5,
 522                    color=(0.4, 0.6, 0.4, 0.5),
 523                    flatness=1.0,
 524                    shadow=0.0,
 525                    h_align='right',
 526                    v_align='center',
 527                )
 528
 529                v -= via_space * 0.9
 530
 531        else:
 532            self._account_name_text = None
 533
 534        if self._back_button is None:
 535            bbtn = bui.get_special_widget('back_button')
 536        else:
 537            bbtn = self._back_button
 538
 539        if show_sign_in_benefits:
 540            v -= sign_in_benefits_space
 541            bui.textwidget(
 542                parent=self._subcontainer,
 543                position=(
 544                    self._sub_width * 0.5,
 545                    v + sign_in_benefits_space * 0.4,
 546                ),
 547                size=(0, 0),
 548                text=bui.Lstr(resource=f'{self._r}.signInInfoText'),
 549                max_height=sign_in_benefits_space * 0.9,
 550                scale=0.9,
 551                color=(0.75, 0.7, 0.8),
 552                maxwidth=self._sub_width * 0.8,
 553                h_align='center',
 554                v_align='center',
 555            )
 556
 557        if show_signing_in_text:
 558            v -= signing_in_text_space
 559
 560            bui.textwidget(
 561                parent=self._subcontainer,
 562                position=(
 563                    self._sub_width * 0.5,
 564                    v + signing_in_text_space * 0.5,
 565                ),
 566                size=(0, 0),
 567                text=bui.Lstr(resource='accountSettingsWindow.signingInText'),
 568                scale=0.9,
 569                color=(0, 1, 0),
 570                maxwidth=self._sub_width * 0.8,
 571                h_align='center',
 572                v_align='center',
 573            )
 574
 575        if show_google_play_sign_in_button:
 576            button_width = 350
 577            v -= sign_in_button_space
 578            self._sign_in_google_play_button = btn = bui.buttonwidget(
 579                parent=self._subcontainer,
 580                position=((self._sub_width - button_width) * 0.5, v - 20),
 581                autoselect=True,
 582                size=(button_width, 60),
 583                label=bui.Lstr(
 584                    value='${A} ${B}',
 585                    subs=[
 586                        (
 587                            '${A}',
 588                            bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 589                        ),
 590                        (
 591                            '${B}',
 592                            bui.Lstr(
 593                                resource=f'{self._r}.signInWithText',
 594                                subs=[
 595                                    (
 596                                        '${SERVICE}',
 597                                        bui.Lstr(resource='googlePlayText'),
 598                                    )
 599                                ],
 600                            ),
 601                        ),
 602                    ],
 603                ),
 604                on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
 605            )
 606            if first_selectable is None:
 607                first_selectable = btn
 608            bui.widget(
 609                edit=btn, right_widget=bui.get_special_widget('squad_button')
 610            )
 611            bui.widget(edit=btn, left_widget=bbtn)
 612            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 613            self._sign_in_text = None
 614
 615        if show_game_center_sign_in_button:
 616            button_width = 350
 617            v -= sign_in_button_space
 618            self._sign_in_google_play_button = btn = bui.buttonwidget(
 619                parent=self._subcontainer,
 620                position=((self._sub_width - button_width) * 0.5, v - 20),
 621                autoselect=True,
 622                size=(button_width, 60),
 623                # Note: Apparently Game Center is just called 'Game Center'
 624                # in all languages. Can revisit if not true.
 625                # https://developer.apple.com/forums/thread/725779
 626                label=bui.Lstr(
 627                    value='${A} ${B}',
 628                    subs=[
 629                        (
 630                            '${A}',
 631                            bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
 632                        ),
 633                        (
 634                            '${B}',
 635                            bui.Lstr(
 636                                resource=f'{self._r}.signInWithText',
 637                                subs=[('${SERVICE}', 'Game Center')],
 638                            ),
 639                        ),
 640                    ],
 641                ),
 642                on_activate_call=lambda: self._sign_in_press(
 643                    LoginType.GAME_CENTER
 644                ),
 645            )
 646            if first_selectable is None:
 647                first_selectable = btn
 648            bui.widget(
 649                edit=btn, right_widget=bui.get_special_widget('squad_button')
 650            )
 651            bui.widget(edit=btn, left_widget=bbtn)
 652            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 653            self._sign_in_text = None
 654
 655        if show_v2_proxy_sign_in_button:
 656            button_width = 350
 657            v -= sign_in_button_space
 658            self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
 659                parent=self._subcontainer,
 660                position=((self._sub_width - button_width) * 0.5, v - 20),
 661                autoselect=True,
 662                size=(button_width, 60),
 663                label='',
 664                on_activate_call=self._v2_proxy_sign_in_press,
 665            )
 666
 667            v2labeltext: bui.Lstr | str = (
 668                bui.Lstr(resource=f'{self._r}.signInWithAnEmailAddressText')
 669                if show_game_center_sign_in_button
 670                or show_google_play_sign_in_button
 671                or show_device_sign_in_button
 672                else bui.Lstr(resource=f'{self._r}.signInText')
 673            )
 674            v2infotext: bui.Lstr | str | None = None
 675
 676            bui.textwidget(
 677                parent=self._subcontainer,
 678                draw_controller=btn,
 679                h_align='center',
 680                v_align='center',
 681                size=(0, 0),
 682                position=(
 683                    self._sub_width * 0.5,
 684                    v + (17 if v2infotext is not None else 10),
 685                ),
 686                text=bui.Lstr(
 687                    value='${A} ${B}',
 688                    subs=[
 689                        ('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
 690                        (
 691                            '${B}',
 692                            v2labeltext,
 693                        ),
 694                    ],
 695                ),
 696                maxwidth=button_width * 0.8,
 697                color=(0.75, 1.0, 0.7),
 698            )
 699            if v2infotext is not None:
 700                bui.textwidget(
 701                    parent=self._subcontainer,
 702                    draw_controller=btn,
 703                    h_align='center',
 704                    v_align='center',
 705                    size=(0, 0),
 706                    position=(self._sub_width * 0.5, v - 4),
 707                    text=v2infotext,
 708                    flatness=1.0,
 709                    scale=0.57,
 710                    maxwidth=button_width * 0.9,
 711                    color=(0.55, 0.8, 0.5),
 712                )
 713            if first_selectable is None:
 714                first_selectable = btn
 715            bui.widget(
 716                edit=btn, right_widget=bui.get_special_widget('squad_button')
 717            )
 718            bui.widget(edit=btn, left_widget=bbtn)
 719            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 720            self._sign_in_text = None
 721
 722        if show_device_sign_in_button:
 723            button_width = 350
 724            v -= sign_in_button_space + deprecated_space
 725            self._sign_in_device_button = btn = bui.buttonwidget(
 726                parent=self._subcontainer,
 727                position=((self._sub_width - button_width) * 0.5, v - 20),
 728                autoselect=True,
 729                size=(button_width, 60),
 730                label='',
 731                on_activate_call=lambda: self._sign_in_press('Local'),
 732            )
 733            bui.textwidget(
 734                parent=self._subcontainer,
 735                h_align='center',
 736                v_align='center',
 737                size=(0, 0),
 738                position=(self._sub_width * 0.5, v + 60),
 739                text=bui.Lstr(resource='deprecatedText'),
 740                scale=0.8,
 741                maxwidth=300,
 742                color=(0.6, 0.55, 0.45),
 743            )
 744
 745            bui.textwidget(
 746                parent=self._subcontainer,
 747                draw_controller=btn,
 748                h_align='center',
 749                v_align='center',
 750                size=(0, 0),
 751                position=(self._sub_width * 0.5, v + 17),
 752                text=bui.Lstr(
 753                    value='${A} ${B}',
 754                    subs=[
 755                        ('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
 756                        (
 757                            '${B}',
 758                            bui.Lstr(
 759                                resource=f'{self._r}.signInWithDeviceText'
 760                            ),
 761                        ),
 762                    ],
 763                ),
 764                maxwidth=button_width * 0.8,
 765                color=(0.75, 1.0, 0.7),
 766            )
 767            bui.textwidget(
 768                parent=self._subcontainer,
 769                draw_controller=btn,
 770                h_align='center',
 771                v_align='center',
 772                size=(0, 0),
 773                position=(self._sub_width * 0.5, v - 4),
 774                text=bui.Lstr(resource=f'{self._r}.signInWithDeviceInfoText'),
 775                flatness=1.0,
 776                scale=0.57,
 777                maxwidth=button_width * 0.9,
 778                color=(0.55, 0.8, 0.5),
 779            )
 780            if first_selectable is None:
 781                first_selectable = btn
 782            bui.widget(
 783                edit=btn, right_widget=bui.get_special_widget('squad_button')
 784            )
 785            bui.widget(edit=btn, left_widget=bbtn)
 786            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 787            self._sign_in_text = None
 788
 789        if show_manage_account_button:
 790            button_width = 300
 791            v -= manage_account_button_space
 792            self._manage_button = btn = bui.buttonwidget(
 793                parent=self._subcontainer,
 794                position=((self._sub_width - button_width) * 0.5, v),
 795                autoselect=True,
 796                size=(button_width, 60),
 797                label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
 798                color=(0.55, 0.5, 0.6),
 799                icon=bui.gettexture('settingsIcon'),
 800                textcolor=(0.75, 0.7, 0.8),
 801                on_activate_call=bui.WeakCall(self._on_manage_account_press),
 802            )
 803            if first_selectable is None:
 804                first_selectable = btn
 805            bui.widget(
 806                edit=btn, right_widget=bui.get_special_widget('squad_button')
 807            )
 808            bui.widget(edit=btn, left_widget=bbtn)
 809
 810        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
 811        if show_game_service_button:
 812            button_width = 300
 813            v -= game_service_button_space * 0.6
 814            if game_center_active:
 815                # Note: Apparently Game Center is just called 'Game Center'
 816                # in all languages. Can revisit if not true.
 817                # https://developer.apple.com/forums/thread/725779
 818                game_service_button_label = bui.Lstr(
 819                    value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 820                    + 'Game Center'
 821                )
 822            else:
 823                raise ValueError(
 824                    "unknown account type: '" + str(v1_account_type) + "'"
 825                )
 826            self._game_service_button = btn = bui.buttonwidget(
 827                parent=self._subcontainer,
 828                position=((self._sub_width - button_width) * 0.5, v),
 829                color=(0.55, 0.5, 0.6),
 830                textcolor=(0.75, 0.7, 0.8),
 831                autoselect=True,
 832                on_activate_call=self._on_game_service_button_press,
 833                size=(button_width, 50),
 834                label=game_service_button_label,
 835            )
 836            if first_selectable is None:
 837                first_selectable = btn
 838            bui.widget(
 839                edit=btn, right_widget=bui.get_special_widget('squad_button')
 840            )
 841            bui.widget(edit=btn, left_widget=bbtn)
 842            v -= game_service_button_space * 0.4
 843        else:
 844            self.game_service_button = None
 845
 846        self._achievements_text: bui.Widget | None
 847        if show_achievements_text:
 848            v -= achievements_text_space * 0.5
 849            self._achievements_text = bui.textwidget(
 850                parent=self._subcontainer,
 851                position=(self._sub_width * 0.5, v),
 852                size=(0, 0),
 853                scale=0.9,
 854                color=(0.75, 0.7, 0.8),
 855                maxwidth=self._sub_width * 0.8,
 856                h_align='center',
 857                v_align='center',
 858            )
 859            v -= achievements_text_space * 0.5
 860        else:
 861            self._achievements_text = None
 862
 863        if show_achievements_text:
 864            self._refresh_achievements()
 865
 866        self._leaderboards_button: bui.Widget | None
 867        if show_leaderboards_button:
 868            button_width = 300
 869            v -= leaderboards_button_space * 0.85
 870            self._leaderboards_button = btn = bui.buttonwidget(
 871                parent=self._subcontainer,
 872                position=((self._sub_width - button_width) * 0.5, v),
 873                color=(0.55, 0.5, 0.6),
 874                textcolor=(0.75, 0.7, 0.8),
 875                autoselect=True,
 876                icon=bui.gettexture('googlePlayLeaderboardsIcon'),
 877                icon_color=(0.8, 0.95, 0.7),
 878                on_activate_call=self._on_leaderboards_press,
 879                size=(button_width, 50),
 880                label=bui.Lstr(resource='leaderboardsText'),
 881            )
 882            if first_selectable is None:
 883                first_selectable = btn
 884            bui.widget(
 885                edit=btn, right_widget=bui.get_special_widget('squad_button')
 886            )
 887            bui.widget(edit=btn, left_widget=bbtn)
 888            v -= leaderboards_button_space * 0.15
 889        else:
 890            self._leaderboards_button = None
 891
 892        self._campaign_progress_text: bui.Widget | None
 893        if show_campaign_progress:
 894            v -= campaign_progress_space * 0.5
 895            self._campaign_progress_text = bui.textwidget(
 896                parent=self._subcontainer,
 897                position=(self._sub_width * 0.5, v),
 898                size=(0, 0),
 899                scale=0.9,
 900                color=(0.75, 0.7, 0.8),
 901                maxwidth=self._sub_width * 0.8,
 902                h_align='center',
 903                v_align='center',
 904            )
 905            v -= campaign_progress_space * 0.5
 906            self._refresh_campaign_progress_text()
 907        else:
 908            self._campaign_progress_text = None
 909
 910        self._tickets_text: bui.Widget | None
 911        if show_tickets:
 912            v -= tickets_space * 0.5
 913            self._tickets_text = bui.textwidget(
 914                parent=self._subcontainer,
 915                position=(self._sub_width * 0.5, v),
 916                size=(0, 0),
 917                scale=0.9,
 918                color=(0.75, 0.7, 0.8),
 919                maxwidth=self._sub_width * 0.8,
 920                flatness=1.0,
 921                h_align='center',
 922                v_align='center',
 923            )
 924            v -= tickets_space * 0.5
 925            self._refresh_tickets_text()
 926
 927        else:
 928            self._tickets_text = None
 929
 930        # bit of spacing before the reset/sign-out section
 931        # v -= 5
 932
 933        button_width = 300
 934
 935        self._linked_accounts_text: bui.Widget | None
 936        if show_linked_accounts_text:
 937            v -= linked_accounts_text_space * 0.8
 938            self._linked_accounts_text = bui.textwidget(
 939                parent=self._subcontainer,
 940                position=(self._sub_width * 0.5, v),
 941                size=(0, 0),
 942                scale=0.9,
 943                color=(0.75, 0.7, 0.8),
 944                maxwidth=self._sub_width * 0.95,
 945                text=bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
 946                h_align='center',
 947                v_align='center',
 948            )
 949            v -= linked_accounts_text_space * 0.2
 950            self._update_linked_accounts_text()
 951        else:
 952            self._linked_accounts_text = None
 953
 954        # Show link/unlink buttons only for V1 accounts.
 955
 956        if show_link_accounts_button:
 957            v -= link_accounts_button_space
 958            self._link_accounts_button = btn = bui.buttonwidget(
 959                parent=self._subcontainer,
 960                position=((self._sub_width - button_width) * 0.5, v),
 961                autoselect=True,
 962                size=(button_width, 60),
 963                label='',
 964                color=(0.55, 0.5, 0.6),
 965                on_activate_call=self._link_accounts_press,
 966            )
 967            bui.textwidget(
 968                parent=self._subcontainer,
 969                draw_controller=btn,
 970                h_align='center',
 971                v_align='center',
 972                size=(0, 0),
 973                position=(self._sub_width * 0.5, v + 17 + 20),
 974                text=bui.Lstr(resource=f'{self._r}.linkAccountsText'),
 975                maxwidth=button_width * 0.8,
 976                color=(0.75, 0.7, 0.8),
 977            )
 978            bui.textwidget(
 979                parent=self._subcontainer,
 980                draw_controller=btn,
 981                h_align='center',
 982                v_align='center',
 983                size=(0, 0),
 984                position=(self._sub_width * 0.5, v - 4 + 20),
 985                text=bui.Lstr(resource=f'{self._r}.linkAccountsInfoText'),
 986                flatness=1.0,
 987                scale=0.5,
 988                maxwidth=button_width * 0.8,
 989                color=(0.75, 0.7, 0.8),
 990            )
 991            if first_selectable is None:
 992                first_selectable = btn
 993            bui.widget(
 994                edit=btn, right_widget=bui.get_special_widget('squad_button')
 995            )
 996            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
 997
 998        self._unlink_accounts_button: bui.Widget | None
 999        if show_unlink_accounts_button:
1000            v -= unlink_accounts_button_space
1001            self._unlink_accounts_button = btn = bui.buttonwidget(
1002                parent=self._subcontainer,
1003                position=((self._sub_width - button_width) * 0.5, v + 25),
1004                autoselect=True,
1005                size=(button_width, 60),
1006                label='',
1007                color=(0.55, 0.5, 0.6),
1008                on_activate_call=self._unlink_accounts_press,
1009            )
1010            self._unlink_accounts_button_label = bui.textwidget(
1011                parent=self._subcontainer,
1012                draw_controller=btn,
1013                h_align='center',
1014                v_align='center',
1015                size=(0, 0),
1016                position=(self._sub_width * 0.5, v + 55),
1017                text=bui.Lstr(resource=f'{self._r}.unlinkAccountsText'),
1018                maxwidth=button_width * 0.8,
1019                color=(0.75, 0.7, 0.8),
1020            )
1021            if first_selectable is None:
1022                first_selectable = btn
1023            bui.widget(
1024                edit=btn, right_widget=bui.get_special_widget('squad_button')
1025            )
1026            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
1027            self._update_unlink_accounts_button()
1028        else:
1029            self._unlink_accounts_button = None
1030
1031        if show_v2_link_info:
1032            v -= v2_link_info_space
1033            bui.textwidget(
1034                parent=self._subcontainer,
1035                h_align='center',
1036                v_align='center',
1037                size=(0, 0),
1038                position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
1039                text=bui.Lstr(resource='v2AccountLinkingInfoText'),
1040                flatness=1.0,
1041                scale=0.8,
1042                maxwidth=450,
1043                color=(0.5, 0.45, 0.55),
1044            )
1045
1046        if self._show_legacy_unlink_button:
1047            v -= legacy_unlink_button_space
1048            button_width_w = button_width * 1.5
1049            bui.textwidget(
1050                parent=self._subcontainer,
1051                position=(self._sub_width * 0.5 - 150.0, v + 75),
1052                size=(300.0, 60),
1053                text=bui.Lstr(resource='whatIsThisText'),
1054                scale=0.8,
1055                color=(0.3, 0.7, 0.05),
1056                maxwidth=200.0,
1057                h_align='center',
1058                v_align='center',
1059                autoselect=True,
1060                selectable=True,
1061                on_activate_call=show_what_is_legacy_unlinking_page,
1062                click_activate=True,
1063            )
1064            btn = bui.buttonwidget(
1065                parent=self._subcontainer,
1066                position=((self._sub_width - button_width_w) * 0.5, v + 25),
1067                autoselect=True,
1068                size=(button_width_w, 60),
1069                label=bui.Lstr(
1070                    resource=f'{self._r}.unlinkLegacyV1AccountsText'
1071                ),
1072                textcolor=(0.8, 0.4, 0),
1073                color=(0.55, 0.5, 0.6),
1074                on_activate_call=self._unlink_accounts_press,
1075            )
1076
1077        if show_sign_out_button:
1078            v -= sign_out_button_space
1079            self._sign_out_button = btn = bui.buttonwidget(
1080                parent=self._subcontainer,
1081                position=((self._sub_width - button_width) * 0.5, v),
1082                size=(button_width, 60),
1083                label=bui.Lstr(resource=f'{self._r}.signOutText'),
1084                color=(0.55, 0.5, 0.6),
1085                textcolor=(0.75, 0.7, 0.8),
1086                autoselect=True,
1087                on_activate_call=self._sign_out_press,
1088            )
1089            if first_selectable is None:
1090                first_selectable = btn
1091            bui.widget(
1092                edit=btn, right_widget=bui.get_special_widget('squad_button')
1093            )
1094            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1095
1096        if show_cancel_sign_in_button:
1097            v -= cancel_sign_in_button_space
1098            self._cancel_sign_in_button = btn = bui.buttonwidget(
1099                parent=self._subcontainer,
1100                position=((self._sub_width - button_width) * 0.5, v),
1101                size=(button_width, 60),
1102                label=bui.Lstr(resource='cancelText'),
1103                color=(0.55, 0.5, 0.6),
1104                textcolor=(0.75, 0.7, 0.8),
1105                autoselect=True,
1106                on_activate_call=self._cancel_sign_in_press,
1107            )
1108            if first_selectable is None:
1109                first_selectable = btn
1110            bui.widget(
1111                edit=btn, right_widget=bui.get_special_widget('squad_button')
1112            )
1113            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1114
1115        if show_delete_account_button:
1116            v -= delete_account_button_space
1117            self._delete_account_button = btn = bui.buttonwidget(
1118                parent=self._subcontainer,
1119                position=((self._sub_width - button_width) * 0.5, v),
1120                size=(button_width, 60),
1121                label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
1122                color=(0.85, 0.5, 0.6),
1123                textcolor=(0.9, 0.7, 0.8),
1124                autoselect=True,
1125                on_activate_call=self._on_delete_account_press,
1126            )
1127            if first_selectable is None:
1128                first_selectable = btn
1129            bui.widget(
1130                edit=btn, right_widget=bui.get_special_widget('squad_button')
1131            )
1132            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1133
1134        # Whatever the topmost selectable thing is, we want it to scroll all
1135        # the way up when we select it.
1136        if first_selectable is not None:
1137            bui.widget(
1138                edit=first_selectable, up_widget=bbtn, show_buffer_top=400
1139            )
1140            # (this should re-scroll us to the top..)
1141            bui.containerwidget(
1142                edit=self._subcontainer, visible_child=first_selectable
1143            )
1144        self._needs_refresh = False
1145
1146    def _on_game_service_button_press(self) -> None:
1147        if bui.app.plus is not None:
1148            bui.app.plus.show_game_service_ui()
1149        else:
1150            logging.warning(
1151                'game-service-ui not available without plus feature-set.'
1152            )
1153
1154    def _on_custom_achievements_press(self) -> None:
1155        if bui.app.plus is not None:
1156            bui.apptimer(
1157                0.15,
1158                bui.Call(bui.app.plus.show_game_service_ui, 'achievements'),
1159            )
1160        else:
1161            logging.warning('show_game_service_ui requires plus feature-set.')
1162
1163    def _on_manage_account_press(self) -> None:
1164        self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR)
1165
1166    def _on_delete_account_press(self) -> None:
1167        self._do_manage_account_press(WebLocation.ACCOUNT_DELETE_SECTION)
1168
1169    def _do_manage_account_press(self, weblocation: WebLocation) -> None:
1170        plus = bui.app.plus
1171        assert plus is not None
1172
1173        # Preemptively fail if it looks like we won't be able to talk to
1174        # the server anyway.
1175        if not plus.cloud.connected:
1176            bui.screenmessage(
1177                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1178                color=(1, 0, 0),
1179            )
1180            bui.getsound('error').play()
1181            return
1182
1183        bui.screenmessage(bui.Lstr(resource='oneMomentText'))
1184
1185        # We expect to have a v2 account signed in if we get here.
1186        if plus.accounts.primary is None:
1187            logging.exception(
1188                'got manage-account press without v2 account present'
1189            )
1190            return
1191
1192        with plus.accounts.primary:
1193            plus.cloud.send_message_cb(
1194                bacommon.cloud.ManageAccountMessage(weblocation=weblocation),
1195                on_response=bui.WeakCall(self._on_manage_account_response),
1196            )
1197
1198    def _on_manage_account_response(
1199        self, response: bacommon.cloud.ManageAccountResponse | Exception
1200    ) -> None:
1201        if isinstance(response, Exception) or response.url is None:
1202            logging.warning(
1203                'Got error in manage-account-response: %s.', response
1204            )
1205            bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
1206            bui.getsound('error').play()
1207            return
1208
1209        bui.open_url(response.url)
1210
1211    def _on_leaderboards_press(self) -> None:
1212        if bui.app.plus is not None:
1213            bui.apptimer(
1214                0.15,
1215                bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'),
1216            )
1217        else:
1218            logging.warning('show_game_service_ui requires classic')
1219
1220    def _have_unlinkable_v1_accounts(self) -> bool:
1221        plus = bui.app.plus
1222        assert plus is not None
1223
1224        # if this is not present, we haven't had contact from the server so
1225        # let's not proceed..
1226        if plus.get_v1_account_public_login_id() is None:
1227            return False
1228        accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1229        return len(accounts) > 1
1230
1231    def _update_unlink_accounts_button(self) -> None:
1232        if self._unlink_accounts_button is None:
1233            return
1234        if self._have_unlinkable_v1_accounts():
1235            clr = (0.75, 0.7, 0.8, 1.0)
1236        else:
1237            clr = (1.0, 1.0, 1.0, 0.25)
1238        bui.textwidget(edit=self._unlink_accounts_button_label, color=clr)
1239
1240    def _should_show_legacy_unlink_button(self) -> bool:
1241        plus = bui.app.plus
1242        assert plus is not None
1243
1244        # Only show this when fully signed in to a v2 account.
1245        if not self._v1_signed_in or plus.accounts.primary is None:
1246            return False
1247
1248        out = self._have_unlinkable_v1_accounts()
1249        return out
1250
1251    def _update_linked_accounts_text(self) -> None:
1252        plus = bui.app.plus
1253        assert plus is not None
1254
1255        if self._linked_accounts_text is None:
1256            return
1257
1258        # Disable this by default when signed in to a V2 account
1259        # (since this shows V1 links which we should no longer care about).
1260        if plus.accounts.primary is not None and not FORCE_ENABLE_V1_LINKING:
1261            return
1262
1263        # if this is not present, we haven't had contact from the server so
1264        # let's not proceed..
1265        if plus.get_v1_account_public_login_id() is None:
1266            num = int(time.time()) % 4
1267            accounts_str = num * '.' + (4 - num) * ' '
1268        else:
1269            accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1270            # UPDATE - we now just print the number here; not the actual
1271            # accounts (they can see that in the unlink section if they're
1272            # curious)
1273            accounts_str = str(max(0, len(accounts) - 1))
1274        bui.textwidget(
1275            edit=self._linked_accounts_text,
1276            text=bui.Lstr(
1277                value='${L} ${A}',
1278                subs=[
1279                    (
1280                        '${L}',
1281                        bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
1282                    ),
1283                    ('${A}', accounts_str),
1284                ],
1285            ),
1286        )
1287
1288    def _refresh_campaign_progress_text(self) -> None:
1289        if self._campaign_progress_text is None:
1290            return
1291        p_str: str | bui.Lstr
1292        try:
1293            assert bui.app.classic is not None
1294            campaign = bui.app.classic.getcampaign('Default')
1295            levels = campaign.levels
1296            levels_complete = sum((1 if l.complete else 0) for l in levels)
1297
1298            # Last level cant be completed; hence the -1;
1299            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
1300            p_str = bui.Lstr(
1301                resource=f'{self._r}.campaignProgressText',
1302                subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')],
1303            )
1304        except Exception:
1305            p_str = '?'
1306            logging.exception('Error calculating co-op campaign progress.')
1307        bui.textwidget(edit=self._campaign_progress_text, text=p_str)
1308
1309    def _refresh_tickets_text(self) -> None:
1310        plus = bui.app.plus
1311        assert plus is not None
1312
1313        if self._tickets_text is None:
1314            return
1315        try:
1316            tc_str = str(plus.get_v1_account_ticket_count())
1317        except Exception:
1318            logging.exception('error refreshing tickets text')
1319            tc_str = '-'
1320        bui.textwidget(
1321            edit=self._tickets_text,
1322            text=bui.Lstr(
1323                resource=f'{self._r}.ticketsText', subs=[('${COUNT}', tc_str)]
1324            ),
1325        )
1326
1327    def _refresh_account_name_text(self) -> None:
1328        plus = bui.app.plus
1329        assert plus is not None
1330
1331        if self._account_name_text is None:
1332            return
1333        try:
1334            name_str = plus.get_v1_account_display_string()
1335        except Exception:
1336            logging.exception('error refreshing tickets text')
1337            name_str = '??'
1338
1339        bui.textwidget(edit=self._account_name_text, text=name_str)
1340
1341    def _refresh_achievements(self) -> None:
1342        assert bui.app.classic is not None
1343        if self._achievements_text is None:
1344            return
1345        complete = sum(
1346            1 if a.complete else 0 for a in bui.app.classic.ach.achievements
1347        )
1348        total = len(bui.app.classic.ach.achievements)
1349        txt_final = bui.Lstr(
1350            resource=f'{self._r}.achievementProgressText',
1351            subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))],
1352        )
1353
1354        if self._achievements_text is not None:
1355            bui.textwidget(edit=self._achievements_text, text=txt_final)
1356
1357    def _link_accounts_press(self) -> None:
1358        # pylint: disable=cyclic-import
1359        from bauiv1lib.account.link import AccountLinkWindow
1360
1361        AccountLinkWindow(origin_widget=self._link_accounts_button)
1362
1363    def _unlink_accounts_press(self) -> None:
1364        # pylint: disable=cyclic-import
1365        from bauiv1lib.account.unlink import AccountUnlinkWindow
1366
1367        if not self._have_unlinkable_v1_accounts():
1368            bui.getsound('error').play()
1369            return
1370
1371        AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
1372
1373    def _cancel_sign_in_press(self) -> None:
1374        # If we're waiting on an adapter to give us credentials, abort.
1375        self._signing_in_adapter = None
1376
1377        plus = bui.app.plus
1378        assert plus is not None
1379
1380        # Say we don't wanna be signed in anymore if we are.
1381        plus.accounts.set_primary_credentials(None)
1382
1383        self._needs_refresh = True
1384
1385        # Speed UI updates along.
1386        bui.apptimer(0.1, bui.WeakCall(self._update))
1387
1388    def _sign_out_press(self) -> None:
1389        plus = bui.app.plus
1390        assert plus is not None
1391
1392        if plus.accounts.have_primary_credentials():
1393            if (
1394                plus.accounts.primary is not None
1395                and LoginType.GPGS in plus.accounts.primary.logins
1396            ):
1397                self._explicitly_signed_out_of_gpgs = True
1398            plus.accounts.set_primary_credentials(None)
1399        else:
1400            plus.sign_out_v1()
1401
1402        cfg = bui.app.config
1403
1404        # Also take note that its our *explicit* intention to not be
1405        # signed in at this point (affects v1 accounts).
1406        cfg['Auto Account State'] = 'signed_out'
1407        cfg.commit()
1408        bui.buttonwidget(
1409            edit=self._sign_out_button,
1410            label=bui.Lstr(resource=f'{self._r}.signingOutText'),
1411        )
1412
1413        # Speed UI updates along.
1414        bui.apptimer(0.1, bui.WeakCall(self._update))
1415
1416    def _sign_in_press(self, login_type: str | LoginType) -> None:
1417        from bauiv1lib.connectivity import wait_for_connectivity
1418
1419        # If we're still waiting for our master-server connection,
1420        # keep the user informed of this instead of rushing in and
1421        # failing immediately.
1422        wait_for_connectivity(on_connected=lambda: self._sign_in(login_type))
1423
1424    def _sign_in(self, login_type: str | LoginType) -> None:
1425        plus = bui.app.plus
1426        assert plus is not None
1427
1428        # V1 login types are strings.
1429        if isinstance(login_type, str):
1430            plus.sign_in_v1(login_type)
1431
1432            # Make note of the type account we're *wanting*
1433            # to be signed in with.
1434            cfg = bui.app.config
1435            cfg['Auto Account State'] = login_type
1436            cfg.commit()
1437            self._needs_refresh = True
1438            bui.apptimer(0.1, bui.WeakCall(self._update))
1439            return
1440
1441        # V2 login sign-in buttons generally go through adapters.
1442        adapter = plus.accounts.login_adapters.get(login_type)
1443        if adapter is not None:
1444            self._signing_in_adapter = adapter
1445            adapter.sign_in(
1446                result_cb=bui.WeakCall(self._on_adapter_sign_in_result),
1447                description='account settings button',
1448            )
1449            # Will get 'Signing in...' to show.
1450            self._needs_refresh = True
1451            bui.apptimer(0.1, bui.WeakCall(self._update))
1452        else:
1453            bui.screenmessage(f'Unsupported login_type: {login_type.name}')
1454
1455    def _on_adapter_sign_in_result(
1456        self,
1457        adapter: bui.LoginAdapter,
1458        result: bui.LoginAdapter.SignInResult | Exception,
1459    ) -> None:
1460        is_us = self._signing_in_adapter is adapter
1461
1462        # If this isn't our current one we don't care.
1463        if not is_us:
1464            return
1465
1466        # If it is us, note that we're done.
1467        self._signing_in_adapter = None
1468
1469        if isinstance(result, Exception):
1470            # For now just make a bit of noise if anything went wrong;
1471            # can get more specific as needed later.
1472            logging.warning('Got error in v2 sign-in result: %s', result)
1473            bui.screenmessage(
1474                bui.Lstr(resource='internal.signInNoConnectionText'),
1475                color=(1, 0, 0),
1476            )
1477            bui.getsound('error').play()
1478        else:
1479            # Success! Plug in these credentials which will begin
1480            # verifying them and set our primary account-handle when
1481            # finished.
1482            plus = bui.app.plus
1483            assert plus is not None
1484            plus.accounts.set_primary_credentials(result.credentials)
1485
1486            # Special case - if the user has explicitly signed out and
1487            # signed in again with GPGS via this button, warn them that
1488            # they need to use the app if they want to switch to a
1489            # different GPGS account.
1490            if (
1491                self._explicitly_signed_out_of_gpgs
1492                and adapter.login_type is LoginType.GPGS
1493            ):
1494                # Delay this slightly so it hopefully pops up after
1495                # credentials go through and the account name shows up.
1496                bui.apptimer(
1497                    1.5,
1498                    bui.Call(
1499                        bui.screenmessage,
1500                        bui.Lstr(
1501                            resource=self._r
1502                            + '.googlePlayGamesAccountSwitchText'
1503                        ),
1504                    ),
1505                )
1506
1507        # Speed any UI updates along.
1508        self._needs_refresh = True
1509        bui.apptimer(0.1, bui.WeakCall(self._update))
1510
1511    def _v2_proxy_sign_in_press(self) -> None:
1512        # pylint: disable=cyclic-import
1513        from bauiv1lib.connectivity import wait_for_connectivity
1514
1515        # If we're still waiting for our master-server connection, keep
1516        # the user informed of this instead of rushing in and failing
1517        # immediately.
1518        wait_for_connectivity(on_connected=self._v2_proxy_sign_in)
1519
1520    def _v2_proxy_sign_in(self) -> None:
1521        # pylint: disable=cyclic-import
1522        from bauiv1lib.account.v2proxy import V2ProxySignInWindow
1523
1524        assert self._sign_in_v2_proxy_button is not None
1525        V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
1526
1527    def _save_state(self) -> None:
1528        try:
1529            sel = self._root_widget.get_selected_child()
1530            if sel == self._back_button:
1531                sel_name = 'Back'
1532            elif sel == self._scrollwidget:
1533                sel_name = 'Scroll'
1534            else:
1535                raise ValueError('unrecognized selection')
1536            assert bui.app.classic is not None
1537            bui.app.ui_v1.window_states[type(self)] = sel_name
1538        except Exception:
1539            logging.exception('Error saving state for %s.', self)
1540
1541    def _restore_state(self) -> None:
1542        try:
1543            assert bui.app.classic is not None
1544            sel_name = bui.app.ui_v1.window_states.get(type(self))
1545            if sel_name == 'Back':
1546                sel = self._back_button
1547            elif sel_name == 'Scroll':
1548                sel = self._scrollwidget
1549            else:
1550                sel = self._back_button
1551            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1552        except Exception:
1553            logging.exception('Error restoring state for %s.', self)
1554
1555
1556def show_what_is_legacy_unlinking_page() -> None:
1557    """Show the webpage describing legacy unlinking."""
1558    plus = bui.app.plus
1559    assert plus is not None
1560
1561    bamasteraddr = plus.get_master_server_address(version=2)
1562    bui.open_url(f'{bamasteraddr}/whatarev1links')
FORCE_ENABLE_V1_LINKING = False
class AccountSettingsWindow(bauiv1._uitypes.MainWindow):
  25class AccountSettingsWindow(bui.MainWindow):
  26    """Window for account related functionality."""
  27
  28    def __init__(
  29        self,
  30        transition: str | None = 'in_right',
  31        modal: bool = False,
  32        origin_widget: bui.Widget | None = None,
  33        close_once_signed_in: bool = False,
  34    ):
  35        # pylint: disable=too-many-statements
  36
  37        plus = bui.app.plus
  38        assert plus is not None
  39
  40        self._sign_in_v2_proxy_button: bui.Widget | None = None
  41        self._sign_in_device_button: bui.Widget | None = None
  42
  43        self._show_legacy_unlink_button = False
  44
  45        self._signing_in_adapter: bui.LoginAdapter | None = None
  46        self._close_once_signed_in = close_once_signed_in
  47        bui.set_analytics_screen('Account Window')
  48
  49        self._explicitly_signed_out_of_gpgs = False
  50
  51        self._r = 'accountSettingsWindow'
  52        self._modal = modal
  53        self._needs_refresh = False
  54        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
  55        self._v1_account_state_num = plus.get_v1_account_state_num()
  56        self._check_sign_in_timer = bui.AppTimer(
  57            1.0, bui.WeakCall(self._update), repeat=True
  58        )
  59
  60        self._can_reset_achievements = False
  61
  62        app = bui.app
  63        assert app.classic is not None
  64        uiscale = app.ui_v1.uiscale
  65
  66        self._width = 850 if uiscale is bui.UIScale.SMALL else 660
  67        x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
  68        self._height = (
  69            380
  70            if uiscale is bui.UIScale.SMALL
  71            else 430 if uiscale is bui.UIScale.MEDIUM else 490
  72        )
  73
  74        self._sign_in_button = None
  75        self._sign_in_text = None
  76
  77        self._scroll_width = self._width - (100 + x_offs * 2)
  78        self._scroll_height = self._height - 120
  79        self._sub_width = self._scroll_width - 20
  80
  81        # Determine which sign-in/sign-out buttons we should show.
  82        self._show_sign_in_buttons: list[str] = []
  83
  84        if LoginType.GPGS in plus.accounts.login_adapters:
  85            self._show_sign_in_buttons.append('Google Play')
  86
  87        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
  88            self._show_sign_in_buttons.append('Game Center')
  89
  90        # Always want to show our web-based v2 login option.
  91        self._show_sign_in_buttons.append('V2Proxy')
  92
  93        # Legacy v1 device accounts available only if the user
  94        # has explicitly enabled deprecated login types.
  95        if bui.app.config.resolve('Show Deprecated Login Types'):
  96            self._show_sign_in_buttons.append('Device')
  97
  98        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
  99        super().__init__(
 100            root_widget=bui.containerwidget(
 101                size=(self._width, self._height + top_extra),
 102                # transition=transition,
 103                toolbar_visibility=(
 104                    'menu_minimal'
 105                    if uiscale is bui.UIScale.SMALL
 106                    else 'menu_full'
 107                ),
 108                scale=(
 109                    2.07
 110                    if uiscale is bui.UIScale.SMALL
 111                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 112                ),
 113                stack_offset=(
 114                    (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
 115                ),
 116            ),
 117            transition=transition,
 118            origin_widget=origin_widget,
 119        )
 120        if uiscale is bui.UIScale.SMALL:
 121            self._back_button = None
 122            bui.containerwidget(
 123                edit=self._root_widget, on_cancel_call=self.main_window_back
 124            )
 125        else:
 126            self._back_button = btn = bui.buttonwidget(
 127                parent=self._root_widget,
 128                position=(51 + x_offs, self._height - 62),
 129                size=(120, 60),
 130                scale=0.8,
 131                text_scale=1.2,
 132                autoselect=True,
 133                label=bui.Lstr(
 134                    resource='doneText' if self._modal else 'backText'
 135                ),
 136                button_type='regular' if self._modal else 'back',
 137                on_activate_call=self.main_window_back,
 138            )
 139            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 140            if not self._modal:
 141                bui.buttonwidget(
 142                    edit=btn,
 143                    button_type='backSmall',
 144                    size=(60, 56),
 145                    label=bui.charstr(bui.SpecialChar.BACK),
 146                )
 147
 148        titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
 149        titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
 150        bui.textwidget(
 151            parent=self._root_widget,
 152            position=(self._width * 0.5, self._height - 41 + titleyoffs),
 153            size=(0, 0),
 154            text=bui.Lstr(resource=f'{self._r}.titleText'),
 155            color=app.ui_v1.title_color,
 156            scale=titlescale,
 157            maxwidth=self._width - 340,
 158            h_align='center',
 159            v_align='center',
 160        )
 161
 162        self._scrollwidget = bui.scrollwidget(
 163            parent=self._root_widget,
 164            highlight=False,
 165            position=(
 166                (self._width - self._scroll_width) * 0.5,
 167                self._height - 65 - self._scroll_height,
 168            ),
 169            size=(self._scroll_width, self._scroll_height),
 170            claims_left_right=True,
 171            claims_tab=True,
 172            selection_loops_to_parent=True,
 173        )
 174        self._subcontainer: bui.Widget | None = None
 175        self._refresh()
 176        self._restore_state()
 177
 178    @override
 179    def get_main_window_state(self) -> bui.MainWindowState:
 180        # Support recreating our window for back/refresh purposes.
 181        cls = type(self)
 182        return bui.BasicMainWindowState(
 183            create_call=lambda transition, origin_widget: cls(
 184                transition=transition, origin_widget=origin_widget
 185            )
 186        )
 187
 188    @override
 189    def on_main_window_close(self) -> None:
 190        self._save_state()
 191
 192    def _update(self) -> None:
 193        plus = bui.app.plus
 194        assert plus is not None
 195
 196        # If they want us to close once we're signed in, do so.
 197        if self._close_once_signed_in and self._v1_signed_in:
 198            self.main_window_back()
 199            return
 200
 201        # Hmm should update this to use get_account_state_num.
 202        # Theoretically if we switch from one signed-in account to another
 203        # in the background this would break.
 204        v1_account_state_num = plus.get_v1_account_state_num()
 205        v1_account_state = plus.get_v1_account_state()
 206        show_legacy_unlink_button = self._should_show_legacy_unlink_button()
 207
 208        if (
 209            v1_account_state_num != self._v1_account_state_num
 210            or show_legacy_unlink_button != self._show_legacy_unlink_button
 211            or self._needs_refresh
 212        ):
 213            self._v1_account_state_num = v1_account_state_num
 214            self._v1_signed_in = v1_account_state == 'signed_in'
 215            self._show_legacy_unlink_button = show_legacy_unlink_button
 216            self._refresh()
 217
 218        # Go ahead and refresh some individual things
 219        # that may change under us.
 220        self._update_linked_accounts_text()
 221        self._update_unlink_accounts_button()
 222        self._refresh_campaign_progress_text()
 223        self._refresh_achievements()
 224        self._refresh_tickets_text()
 225        self._refresh_account_name_text()
 226
 227    def _refresh(self) -> None:
 228        # pylint: disable=too-many-statements
 229        # pylint: disable=too-many-branches
 230        # pylint: disable=too-many-locals
 231        # pylint: disable=cyclic-import
 232
 233        plus = bui.app.plus
 234        assert plus is not None
 235
 236        via_lines: list[str] = []
 237
 238        primary_v2_account = plus.accounts.primary
 239
 240        v1_state = plus.get_v1_account_state()
 241        v1_account_type = (
 242            plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown'
 243        )
 244
 245        # We expose GPGS-specific functionality only if it is 'active'
 246        # (meaning the current GPGS player matches one of our account's
 247        # logins).
 248        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
 249        gpgs_active = adapter is not None and adapter.is_back_end_active()
 250
 251        # Ditto for Game Center.
 252        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
 253        game_center_active = (
 254            adapter is not None and adapter.is_back_end_active()
 255        )
 256
 257        show_signed_in_as = self._v1_signed_in
 258        signed_in_as_space = 95.0
 259
 260        # To reduce confusion about the whole V2 account situation for
 261        # people used to seeing their Google Play Games or Game Center
 262        # account name and icon and whatnot, let's show those underneath
 263        # the V2 tag to help communicate that they are in fact logged in
 264        # through that account.
 265        via_space = 25.0
 266        if show_signed_in_as and bui.app.plus is not None:
 267            accounts = bui.app.plus.accounts
 268            if accounts.primary is not None:
 269                # For these login types, we show 'via' IF there is a
 270                # login of that type attached to our account AND it is
 271                # currently active (We don't want to show 'via Game
 272                # Center' if we're signed out of Game Center or
 273                # currently running on Steam, even if there is a Game
 274                # Center login attached to our account).
 275                for ltype, lchar in [
 276                    (LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 277                    (LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
 278                ]:
 279                    linfo = accounts.primary.logins.get(ltype)
 280                    ladapter = accounts.login_adapters.get(ltype)
 281                    if (
 282                        linfo is not None
 283                        and ladapter is not None
 284                        and ladapter.is_back_end_active()
 285                    ):
 286                        via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
 287
 288                # TEMP TESTING
 289                if bool(False):
 290                    icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 291                    via_lines.append(f'{icontxt}FloofDibble')
 292                    icontxt = bui.charstr(
 293                        bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
 294                    )
 295                    via_lines.append(f'{icontxt}StinkBobble')
 296
 297        show_sign_in_benefits = not self._v1_signed_in
 298        sign_in_benefits_space = 80.0
 299
 300        show_signing_in_text = (
 301            v1_state == 'signing_in' or self._signing_in_adapter is not None
 302        )
 303        signing_in_text_space = 80.0
 304
 305        show_google_play_sign_in_button = (
 306            v1_state == 'signed_out'
 307            and self._signing_in_adapter is None
 308            and 'Google Play' in self._show_sign_in_buttons
 309        )
 310        show_game_center_sign_in_button = (
 311            v1_state == 'signed_out'
 312            and self._signing_in_adapter is None
 313            and 'Game Center' in self._show_sign_in_buttons
 314        )
 315        show_v2_proxy_sign_in_button = (
 316            v1_state == 'signed_out'
 317            and self._signing_in_adapter is None
 318            and 'V2Proxy' in self._show_sign_in_buttons
 319        )
 320        show_device_sign_in_button = (
 321            v1_state == 'signed_out'
 322            and self._signing_in_adapter is None
 323            and 'Device' in self._show_sign_in_buttons
 324        )
 325        sign_in_button_space = 70.0
 326        deprecated_space = 60
 327
 328        # Game Center currently has a single UI for everything.
 329        show_game_service_button = game_center_active
 330        game_service_button_space = 60.0
 331
 332        # Phasing this out (for V2 accounts at least).
 333        show_linked_accounts_text = (
 334            self._v1_signed_in and v1_account_type != 'V2'
 335        )
 336        linked_accounts_text_space = 60.0
 337
 338        # Update: No longer showing this since its visible on main
 339        # toolbar.
 340        show_achievements_text = False
 341        achievements_text_space = 27.0
 342
 343        show_leaderboards_button = self._v1_signed_in and gpgs_active
 344        leaderboards_button_space = 60.0
 345
 346        # Update: No longer showing this; trying to get progress type
 347        # stuff out of the account panel.
 348        # show_campaign_progress = self._v1_signed_in
 349        show_campaign_progress = False
 350        campaign_progress_space = 27.0
 351
 352        # show_tickets = self._v1_signed_in
 353        show_tickets = False
 354        tickets_space = 27.0
 355
 356        show_manage_account_button = primary_v2_account is not None
 357        manage_account_button_space = 70.0
 358
 359        show_delete_account_button = primary_v2_account is not None
 360        delete_account_button_space = 70.0
 361
 362        show_link_accounts_button = self._v1_signed_in and (
 363            primary_v2_account is None or FORCE_ENABLE_V1_LINKING
 364        )
 365        link_accounts_button_space = 70.0
 366
 367        show_unlink_accounts_button = show_link_accounts_button
 368        unlink_accounts_button_space = 90.0
 369
 370        # Phasing this out.
 371        show_v2_link_info = False
 372        v2_link_info_space = 70.0
 373
 374        legacy_unlink_button_space = 120.0
 375
 376        show_sign_out_button = primary_v2_account is not None or (
 377            self._v1_signed_in and v1_account_type == 'Local'
 378        )
 379        sign_out_button_space = 70.0
 380
 381        # We can show cancel if we're either waiting on an adapter to
 382        # provide us with v2 credentials or waiting for those
 383        # credentials to be verified.
 384        show_cancel_sign_in_button = self._signing_in_adapter is not None or (
 385            plus.accounts.have_primary_credentials()
 386            and primary_v2_account is None
 387        )
 388        cancel_sign_in_button_space = 70.0
 389
 390        if self._subcontainer is not None:
 391            self._subcontainer.delete()
 392        self._sub_height = 60.0
 393        if show_signed_in_as:
 394            self._sub_height += signed_in_as_space
 395        self._sub_height += via_space * len(via_lines)
 396        if show_signing_in_text:
 397            self._sub_height += signing_in_text_space
 398        if show_google_play_sign_in_button:
 399            self._sub_height += sign_in_button_space
 400        if show_game_center_sign_in_button:
 401            self._sub_height += sign_in_button_space
 402        if show_v2_proxy_sign_in_button:
 403            self._sub_height += sign_in_button_space
 404        if show_device_sign_in_button:
 405            self._sub_height += sign_in_button_space + deprecated_space
 406        if show_game_service_button:
 407            self._sub_height += game_service_button_space
 408        if show_linked_accounts_text:
 409            self._sub_height += linked_accounts_text_space
 410        if show_achievements_text:
 411            self._sub_height += achievements_text_space
 412        if show_leaderboards_button:
 413            self._sub_height += leaderboards_button_space
 414        if show_campaign_progress:
 415            self._sub_height += campaign_progress_space
 416        if show_tickets:
 417            self._sub_height += tickets_space
 418        if show_sign_in_benefits:
 419            self._sub_height += sign_in_benefits_space
 420        if show_manage_account_button:
 421            self._sub_height += manage_account_button_space
 422        if show_link_accounts_button:
 423            self._sub_height += link_accounts_button_space
 424        if show_unlink_accounts_button:
 425            self._sub_height += unlink_accounts_button_space
 426        if show_v2_link_info:
 427            self._sub_height += v2_link_info_space
 428        if self._show_legacy_unlink_button:
 429            self._sub_height += legacy_unlink_button_space
 430        if show_sign_out_button:
 431            self._sub_height += sign_out_button_space
 432        if show_delete_account_button:
 433            self._sub_height += delete_account_button_space
 434        if show_cancel_sign_in_button:
 435            self._sub_height += cancel_sign_in_button_space
 436        self._subcontainer = bui.containerwidget(
 437            parent=self._scrollwidget,
 438            size=(self._sub_width, self._sub_height),
 439            background=False,
 440            claims_left_right=True,
 441            claims_tab=True,
 442            selection_loops_to_parent=True,
 443        )
 444
 445        first_selectable = None
 446        v = self._sub_height - 10.0
 447
 448        assert bui.app.classic is not None
 449        self._account_name_text: bui.Widget | None
 450        if show_signed_in_as:
 451            v -= signed_in_as_space * 0.2
 452            txt = bui.Lstr(
 453                resource='accountSettingsWindow.youAreSignedInAsText',
 454                fallback_resource='accountSettingsWindow.youAreLoggedInAsText',
 455            )
 456            bui.textwidget(
 457                parent=self._subcontainer,
 458                position=(self._sub_width * 0.5, v),
 459                size=(0, 0),
 460                text=txt,
 461                scale=0.9,
 462                color=bui.app.ui_v1.title_color,
 463                maxwidth=self._sub_width * 0.9,
 464                h_align='center',
 465                v_align='center',
 466            )
 467            v -= signed_in_as_space * 0.5
 468            self._account_name_text = bui.textwidget(
 469                parent=self._subcontainer,
 470                position=(self._sub_width * 0.5, v),
 471                size=(0, 0),
 472                scale=1.5,
 473                maxwidth=self._sub_width * 0.9,
 474                res_scale=1.5,
 475                color=(1, 1, 1, 1),
 476                h_align='center',
 477                v_align='center',
 478            )
 479
 480            self._refresh_account_name_text()
 481
 482            v -= signed_in_as_space * 0.4
 483
 484            for via in via_lines:
 485                v -= via_space * 0.1
 486                sscale = 0.7
 487                swidth = (
 488                    bui.get_string_width(via, suppress_warning=True) * sscale
 489                )
 490                bui.textwidget(
 491                    parent=self._subcontainer,
 492                    position=(self._sub_width * 0.5, v),
 493                    size=(0, 0),
 494                    text=via,
 495                    scale=sscale,
 496                    color=(0.6, 0.6, 0.6),
 497                    flatness=1.0,
 498                    shadow=0.0,
 499                    h_align='center',
 500                    v_align='center',
 501                )
 502                bui.textwidget(
 503                    parent=self._subcontainer,
 504                    position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
 505                    size=(0, 0),
 506                    text=bui.Lstr(
 507                        value='(${VIA}',
 508                        subs=[('${VIA}', bui.Lstr(resource='viaText'))],
 509                    ),
 510                    scale=0.5,
 511                    color=(0.4, 0.6, 0.4, 0.5),
 512                    flatness=1.0,
 513                    shadow=0.0,
 514                    h_align='right',
 515                    v_align='center',
 516                )
 517                bui.textwidget(
 518                    parent=self._subcontainer,
 519                    position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
 520                    size=(0, 0),
 521                    text=')',
 522                    scale=0.5,
 523                    color=(0.4, 0.6, 0.4, 0.5),
 524                    flatness=1.0,
 525                    shadow=0.0,
 526                    h_align='right',
 527                    v_align='center',
 528                )
 529
 530                v -= via_space * 0.9
 531
 532        else:
 533            self._account_name_text = None
 534
 535        if self._back_button is None:
 536            bbtn = bui.get_special_widget('back_button')
 537        else:
 538            bbtn = self._back_button
 539
 540        if show_sign_in_benefits:
 541            v -= sign_in_benefits_space
 542            bui.textwidget(
 543                parent=self._subcontainer,
 544                position=(
 545                    self._sub_width * 0.5,
 546                    v + sign_in_benefits_space * 0.4,
 547                ),
 548                size=(0, 0),
 549                text=bui.Lstr(resource=f'{self._r}.signInInfoText'),
 550                max_height=sign_in_benefits_space * 0.9,
 551                scale=0.9,
 552                color=(0.75, 0.7, 0.8),
 553                maxwidth=self._sub_width * 0.8,
 554                h_align='center',
 555                v_align='center',
 556            )
 557
 558        if show_signing_in_text:
 559            v -= signing_in_text_space
 560
 561            bui.textwidget(
 562                parent=self._subcontainer,
 563                position=(
 564                    self._sub_width * 0.5,
 565                    v + signing_in_text_space * 0.5,
 566                ),
 567                size=(0, 0),
 568                text=bui.Lstr(resource='accountSettingsWindow.signingInText'),
 569                scale=0.9,
 570                color=(0, 1, 0),
 571                maxwidth=self._sub_width * 0.8,
 572                h_align='center',
 573                v_align='center',
 574            )
 575
 576        if show_google_play_sign_in_button:
 577            button_width = 350
 578            v -= sign_in_button_space
 579            self._sign_in_google_play_button = btn = bui.buttonwidget(
 580                parent=self._subcontainer,
 581                position=((self._sub_width - button_width) * 0.5, v - 20),
 582                autoselect=True,
 583                size=(button_width, 60),
 584                label=bui.Lstr(
 585                    value='${A} ${B}',
 586                    subs=[
 587                        (
 588                            '${A}',
 589                            bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 590                        ),
 591                        (
 592                            '${B}',
 593                            bui.Lstr(
 594                                resource=f'{self._r}.signInWithText',
 595                                subs=[
 596                                    (
 597                                        '${SERVICE}',
 598                                        bui.Lstr(resource='googlePlayText'),
 599                                    )
 600                                ],
 601                            ),
 602                        ),
 603                    ],
 604                ),
 605                on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
 606            )
 607            if first_selectable is None:
 608                first_selectable = btn
 609            bui.widget(
 610                edit=btn, right_widget=bui.get_special_widget('squad_button')
 611            )
 612            bui.widget(edit=btn, left_widget=bbtn)
 613            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 614            self._sign_in_text = None
 615
 616        if show_game_center_sign_in_button:
 617            button_width = 350
 618            v -= sign_in_button_space
 619            self._sign_in_google_play_button = btn = bui.buttonwidget(
 620                parent=self._subcontainer,
 621                position=((self._sub_width - button_width) * 0.5, v - 20),
 622                autoselect=True,
 623                size=(button_width, 60),
 624                # Note: Apparently Game Center is just called 'Game Center'
 625                # in all languages. Can revisit if not true.
 626                # https://developer.apple.com/forums/thread/725779
 627                label=bui.Lstr(
 628                    value='${A} ${B}',
 629                    subs=[
 630                        (
 631                            '${A}',
 632                            bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
 633                        ),
 634                        (
 635                            '${B}',
 636                            bui.Lstr(
 637                                resource=f'{self._r}.signInWithText',
 638                                subs=[('${SERVICE}', 'Game Center')],
 639                            ),
 640                        ),
 641                    ],
 642                ),
 643                on_activate_call=lambda: self._sign_in_press(
 644                    LoginType.GAME_CENTER
 645                ),
 646            )
 647            if first_selectable is None:
 648                first_selectable = btn
 649            bui.widget(
 650                edit=btn, right_widget=bui.get_special_widget('squad_button')
 651            )
 652            bui.widget(edit=btn, left_widget=bbtn)
 653            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 654            self._sign_in_text = None
 655
 656        if show_v2_proxy_sign_in_button:
 657            button_width = 350
 658            v -= sign_in_button_space
 659            self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
 660                parent=self._subcontainer,
 661                position=((self._sub_width - button_width) * 0.5, v - 20),
 662                autoselect=True,
 663                size=(button_width, 60),
 664                label='',
 665                on_activate_call=self._v2_proxy_sign_in_press,
 666            )
 667
 668            v2labeltext: bui.Lstr | str = (
 669                bui.Lstr(resource=f'{self._r}.signInWithAnEmailAddressText')
 670                if show_game_center_sign_in_button
 671                or show_google_play_sign_in_button
 672                or show_device_sign_in_button
 673                else bui.Lstr(resource=f'{self._r}.signInText')
 674            )
 675            v2infotext: bui.Lstr | str | None = None
 676
 677            bui.textwidget(
 678                parent=self._subcontainer,
 679                draw_controller=btn,
 680                h_align='center',
 681                v_align='center',
 682                size=(0, 0),
 683                position=(
 684                    self._sub_width * 0.5,
 685                    v + (17 if v2infotext is not None else 10),
 686                ),
 687                text=bui.Lstr(
 688                    value='${A} ${B}',
 689                    subs=[
 690                        ('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
 691                        (
 692                            '${B}',
 693                            v2labeltext,
 694                        ),
 695                    ],
 696                ),
 697                maxwidth=button_width * 0.8,
 698                color=(0.75, 1.0, 0.7),
 699            )
 700            if v2infotext is not None:
 701                bui.textwidget(
 702                    parent=self._subcontainer,
 703                    draw_controller=btn,
 704                    h_align='center',
 705                    v_align='center',
 706                    size=(0, 0),
 707                    position=(self._sub_width * 0.5, v - 4),
 708                    text=v2infotext,
 709                    flatness=1.0,
 710                    scale=0.57,
 711                    maxwidth=button_width * 0.9,
 712                    color=(0.55, 0.8, 0.5),
 713                )
 714            if first_selectable is None:
 715                first_selectable = btn
 716            bui.widget(
 717                edit=btn, right_widget=bui.get_special_widget('squad_button')
 718            )
 719            bui.widget(edit=btn, left_widget=bbtn)
 720            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 721            self._sign_in_text = None
 722
 723        if show_device_sign_in_button:
 724            button_width = 350
 725            v -= sign_in_button_space + deprecated_space
 726            self._sign_in_device_button = btn = bui.buttonwidget(
 727                parent=self._subcontainer,
 728                position=((self._sub_width - button_width) * 0.5, v - 20),
 729                autoselect=True,
 730                size=(button_width, 60),
 731                label='',
 732                on_activate_call=lambda: self._sign_in_press('Local'),
 733            )
 734            bui.textwidget(
 735                parent=self._subcontainer,
 736                h_align='center',
 737                v_align='center',
 738                size=(0, 0),
 739                position=(self._sub_width * 0.5, v + 60),
 740                text=bui.Lstr(resource='deprecatedText'),
 741                scale=0.8,
 742                maxwidth=300,
 743                color=(0.6, 0.55, 0.45),
 744            )
 745
 746            bui.textwidget(
 747                parent=self._subcontainer,
 748                draw_controller=btn,
 749                h_align='center',
 750                v_align='center',
 751                size=(0, 0),
 752                position=(self._sub_width * 0.5, v + 17),
 753                text=bui.Lstr(
 754                    value='${A} ${B}',
 755                    subs=[
 756                        ('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
 757                        (
 758                            '${B}',
 759                            bui.Lstr(
 760                                resource=f'{self._r}.signInWithDeviceText'
 761                            ),
 762                        ),
 763                    ],
 764                ),
 765                maxwidth=button_width * 0.8,
 766                color=(0.75, 1.0, 0.7),
 767            )
 768            bui.textwidget(
 769                parent=self._subcontainer,
 770                draw_controller=btn,
 771                h_align='center',
 772                v_align='center',
 773                size=(0, 0),
 774                position=(self._sub_width * 0.5, v - 4),
 775                text=bui.Lstr(resource=f'{self._r}.signInWithDeviceInfoText'),
 776                flatness=1.0,
 777                scale=0.57,
 778                maxwidth=button_width * 0.9,
 779                color=(0.55, 0.8, 0.5),
 780            )
 781            if first_selectable is None:
 782                first_selectable = btn
 783            bui.widget(
 784                edit=btn, right_widget=bui.get_special_widget('squad_button')
 785            )
 786            bui.widget(edit=btn, left_widget=bbtn)
 787            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 788            self._sign_in_text = None
 789
 790        if show_manage_account_button:
 791            button_width = 300
 792            v -= manage_account_button_space
 793            self._manage_button = btn = bui.buttonwidget(
 794                parent=self._subcontainer,
 795                position=((self._sub_width - button_width) * 0.5, v),
 796                autoselect=True,
 797                size=(button_width, 60),
 798                label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
 799                color=(0.55, 0.5, 0.6),
 800                icon=bui.gettexture('settingsIcon'),
 801                textcolor=(0.75, 0.7, 0.8),
 802                on_activate_call=bui.WeakCall(self._on_manage_account_press),
 803            )
 804            if first_selectable is None:
 805                first_selectable = btn
 806            bui.widget(
 807                edit=btn, right_widget=bui.get_special_widget('squad_button')
 808            )
 809            bui.widget(edit=btn, left_widget=bbtn)
 810
 811        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
 812        if show_game_service_button:
 813            button_width = 300
 814            v -= game_service_button_space * 0.6
 815            if game_center_active:
 816                # Note: Apparently Game Center is just called 'Game Center'
 817                # in all languages. Can revisit if not true.
 818                # https://developer.apple.com/forums/thread/725779
 819                game_service_button_label = bui.Lstr(
 820                    value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 821                    + 'Game Center'
 822                )
 823            else:
 824                raise ValueError(
 825                    "unknown account type: '" + str(v1_account_type) + "'"
 826                )
 827            self._game_service_button = btn = bui.buttonwidget(
 828                parent=self._subcontainer,
 829                position=((self._sub_width - button_width) * 0.5, v),
 830                color=(0.55, 0.5, 0.6),
 831                textcolor=(0.75, 0.7, 0.8),
 832                autoselect=True,
 833                on_activate_call=self._on_game_service_button_press,
 834                size=(button_width, 50),
 835                label=game_service_button_label,
 836            )
 837            if first_selectable is None:
 838                first_selectable = btn
 839            bui.widget(
 840                edit=btn, right_widget=bui.get_special_widget('squad_button')
 841            )
 842            bui.widget(edit=btn, left_widget=bbtn)
 843            v -= game_service_button_space * 0.4
 844        else:
 845            self.game_service_button = None
 846
 847        self._achievements_text: bui.Widget | None
 848        if show_achievements_text:
 849            v -= achievements_text_space * 0.5
 850            self._achievements_text = bui.textwidget(
 851                parent=self._subcontainer,
 852                position=(self._sub_width * 0.5, v),
 853                size=(0, 0),
 854                scale=0.9,
 855                color=(0.75, 0.7, 0.8),
 856                maxwidth=self._sub_width * 0.8,
 857                h_align='center',
 858                v_align='center',
 859            )
 860            v -= achievements_text_space * 0.5
 861        else:
 862            self._achievements_text = None
 863
 864        if show_achievements_text:
 865            self._refresh_achievements()
 866
 867        self._leaderboards_button: bui.Widget | None
 868        if show_leaderboards_button:
 869            button_width = 300
 870            v -= leaderboards_button_space * 0.85
 871            self._leaderboards_button = btn = bui.buttonwidget(
 872                parent=self._subcontainer,
 873                position=((self._sub_width - button_width) * 0.5, v),
 874                color=(0.55, 0.5, 0.6),
 875                textcolor=(0.75, 0.7, 0.8),
 876                autoselect=True,
 877                icon=bui.gettexture('googlePlayLeaderboardsIcon'),
 878                icon_color=(0.8, 0.95, 0.7),
 879                on_activate_call=self._on_leaderboards_press,
 880                size=(button_width, 50),
 881                label=bui.Lstr(resource='leaderboardsText'),
 882            )
 883            if first_selectable is None:
 884                first_selectable = btn
 885            bui.widget(
 886                edit=btn, right_widget=bui.get_special_widget('squad_button')
 887            )
 888            bui.widget(edit=btn, left_widget=bbtn)
 889            v -= leaderboards_button_space * 0.15
 890        else:
 891            self._leaderboards_button = None
 892
 893        self._campaign_progress_text: bui.Widget | None
 894        if show_campaign_progress:
 895            v -= campaign_progress_space * 0.5
 896            self._campaign_progress_text = bui.textwidget(
 897                parent=self._subcontainer,
 898                position=(self._sub_width * 0.5, v),
 899                size=(0, 0),
 900                scale=0.9,
 901                color=(0.75, 0.7, 0.8),
 902                maxwidth=self._sub_width * 0.8,
 903                h_align='center',
 904                v_align='center',
 905            )
 906            v -= campaign_progress_space * 0.5
 907            self._refresh_campaign_progress_text()
 908        else:
 909            self._campaign_progress_text = None
 910
 911        self._tickets_text: bui.Widget | None
 912        if show_tickets:
 913            v -= tickets_space * 0.5
 914            self._tickets_text = bui.textwidget(
 915                parent=self._subcontainer,
 916                position=(self._sub_width * 0.5, v),
 917                size=(0, 0),
 918                scale=0.9,
 919                color=(0.75, 0.7, 0.8),
 920                maxwidth=self._sub_width * 0.8,
 921                flatness=1.0,
 922                h_align='center',
 923                v_align='center',
 924            )
 925            v -= tickets_space * 0.5
 926            self._refresh_tickets_text()
 927
 928        else:
 929            self._tickets_text = None
 930
 931        # bit of spacing before the reset/sign-out section
 932        # v -= 5
 933
 934        button_width = 300
 935
 936        self._linked_accounts_text: bui.Widget | None
 937        if show_linked_accounts_text:
 938            v -= linked_accounts_text_space * 0.8
 939            self._linked_accounts_text = bui.textwidget(
 940                parent=self._subcontainer,
 941                position=(self._sub_width * 0.5, v),
 942                size=(0, 0),
 943                scale=0.9,
 944                color=(0.75, 0.7, 0.8),
 945                maxwidth=self._sub_width * 0.95,
 946                text=bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
 947                h_align='center',
 948                v_align='center',
 949            )
 950            v -= linked_accounts_text_space * 0.2
 951            self._update_linked_accounts_text()
 952        else:
 953            self._linked_accounts_text = None
 954
 955        # Show link/unlink buttons only for V1 accounts.
 956
 957        if show_link_accounts_button:
 958            v -= link_accounts_button_space
 959            self._link_accounts_button = btn = bui.buttonwidget(
 960                parent=self._subcontainer,
 961                position=((self._sub_width - button_width) * 0.5, v),
 962                autoselect=True,
 963                size=(button_width, 60),
 964                label='',
 965                color=(0.55, 0.5, 0.6),
 966                on_activate_call=self._link_accounts_press,
 967            )
 968            bui.textwidget(
 969                parent=self._subcontainer,
 970                draw_controller=btn,
 971                h_align='center',
 972                v_align='center',
 973                size=(0, 0),
 974                position=(self._sub_width * 0.5, v + 17 + 20),
 975                text=bui.Lstr(resource=f'{self._r}.linkAccountsText'),
 976                maxwidth=button_width * 0.8,
 977                color=(0.75, 0.7, 0.8),
 978            )
 979            bui.textwidget(
 980                parent=self._subcontainer,
 981                draw_controller=btn,
 982                h_align='center',
 983                v_align='center',
 984                size=(0, 0),
 985                position=(self._sub_width * 0.5, v - 4 + 20),
 986                text=bui.Lstr(resource=f'{self._r}.linkAccountsInfoText'),
 987                flatness=1.0,
 988                scale=0.5,
 989                maxwidth=button_width * 0.8,
 990                color=(0.75, 0.7, 0.8),
 991            )
 992            if first_selectable is None:
 993                first_selectable = btn
 994            bui.widget(
 995                edit=btn, right_widget=bui.get_special_widget('squad_button')
 996            )
 997            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
 998
 999        self._unlink_accounts_button: bui.Widget | None
1000        if show_unlink_accounts_button:
1001            v -= unlink_accounts_button_space
1002            self._unlink_accounts_button = btn = bui.buttonwidget(
1003                parent=self._subcontainer,
1004                position=((self._sub_width - button_width) * 0.5, v + 25),
1005                autoselect=True,
1006                size=(button_width, 60),
1007                label='',
1008                color=(0.55, 0.5, 0.6),
1009                on_activate_call=self._unlink_accounts_press,
1010            )
1011            self._unlink_accounts_button_label = bui.textwidget(
1012                parent=self._subcontainer,
1013                draw_controller=btn,
1014                h_align='center',
1015                v_align='center',
1016                size=(0, 0),
1017                position=(self._sub_width * 0.5, v + 55),
1018                text=bui.Lstr(resource=f'{self._r}.unlinkAccountsText'),
1019                maxwidth=button_width * 0.8,
1020                color=(0.75, 0.7, 0.8),
1021            )
1022            if first_selectable is None:
1023                first_selectable = btn
1024            bui.widget(
1025                edit=btn, right_widget=bui.get_special_widget('squad_button')
1026            )
1027            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
1028            self._update_unlink_accounts_button()
1029        else:
1030            self._unlink_accounts_button = None
1031
1032        if show_v2_link_info:
1033            v -= v2_link_info_space
1034            bui.textwidget(
1035                parent=self._subcontainer,
1036                h_align='center',
1037                v_align='center',
1038                size=(0, 0),
1039                position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
1040                text=bui.Lstr(resource='v2AccountLinkingInfoText'),
1041                flatness=1.0,
1042                scale=0.8,
1043                maxwidth=450,
1044                color=(0.5, 0.45, 0.55),
1045            )
1046
1047        if self._show_legacy_unlink_button:
1048            v -= legacy_unlink_button_space
1049            button_width_w = button_width * 1.5
1050            bui.textwidget(
1051                parent=self._subcontainer,
1052                position=(self._sub_width * 0.5 - 150.0, v + 75),
1053                size=(300.0, 60),
1054                text=bui.Lstr(resource='whatIsThisText'),
1055                scale=0.8,
1056                color=(0.3, 0.7, 0.05),
1057                maxwidth=200.0,
1058                h_align='center',
1059                v_align='center',
1060                autoselect=True,
1061                selectable=True,
1062                on_activate_call=show_what_is_legacy_unlinking_page,
1063                click_activate=True,
1064            )
1065            btn = bui.buttonwidget(
1066                parent=self._subcontainer,
1067                position=((self._sub_width - button_width_w) * 0.5, v + 25),
1068                autoselect=True,
1069                size=(button_width_w, 60),
1070                label=bui.Lstr(
1071                    resource=f'{self._r}.unlinkLegacyV1AccountsText'
1072                ),
1073                textcolor=(0.8, 0.4, 0),
1074                color=(0.55, 0.5, 0.6),
1075                on_activate_call=self._unlink_accounts_press,
1076            )
1077
1078        if show_sign_out_button:
1079            v -= sign_out_button_space
1080            self._sign_out_button = btn = bui.buttonwidget(
1081                parent=self._subcontainer,
1082                position=((self._sub_width - button_width) * 0.5, v),
1083                size=(button_width, 60),
1084                label=bui.Lstr(resource=f'{self._r}.signOutText'),
1085                color=(0.55, 0.5, 0.6),
1086                textcolor=(0.75, 0.7, 0.8),
1087                autoselect=True,
1088                on_activate_call=self._sign_out_press,
1089            )
1090            if first_selectable is None:
1091                first_selectable = btn
1092            bui.widget(
1093                edit=btn, right_widget=bui.get_special_widget('squad_button')
1094            )
1095            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1096
1097        if show_cancel_sign_in_button:
1098            v -= cancel_sign_in_button_space
1099            self._cancel_sign_in_button = btn = bui.buttonwidget(
1100                parent=self._subcontainer,
1101                position=((self._sub_width - button_width) * 0.5, v),
1102                size=(button_width, 60),
1103                label=bui.Lstr(resource='cancelText'),
1104                color=(0.55, 0.5, 0.6),
1105                textcolor=(0.75, 0.7, 0.8),
1106                autoselect=True,
1107                on_activate_call=self._cancel_sign_in_press,
1108            )
1109            if first_selectable is None:
1110                first_selectable = btn
1111            bui.widget(
1112                edit=btn, right_widget=bui.get_special_widget('squad_button')
1113            )
1114            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1115
1116        if show_delete_account_button:
1117            v -= delete_account_button_space
1118            self._delete_account_button = btn = bui.buttonwidget(
1119                parent=self._subcontainer,
1120                position=((self._sub_width - button_width) * 0.5, v),
1121                size=(button_width, 60),
1122                label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
1123                color=(0.85, 0.5, 0.6),
1124                textcolor=(0.9, 0.7, 0.8),
1125                autoselect=True,
1126                on_activate_call=self._on_delete_account_press,
1127            )
1128            if first_selectable is None:
1129                first_selectable = btn
1130            bui.widget(
1131                edit=btn, right_widget=bui.get_special_widget('squad_button')
1132            )
1133            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1134
1135        # Whatever the topmost selectable thing is, we want it to scroll all
1136        # the way up when we select it.
1137        if first_selectable is not None:
1138            bui.widget(
1139                edit=first_selectable, up_widget=bbtn, show_buffer_top=400
1140            )
1141            # (this should re-scroll us to the top..)
1142            bui.containerwidget(
1143                edit=self._subcontainer, visible_child=first_selectable
1144            )
1145        self._needs_refresh = False
1146
1147    def _on_game_service_button_press(self) -> None:
1148        if bui.app.plus is not None:
1149            bui.app.plus.show_game_service_ui()
1150        else:
1151            logging.warning(
1152                'game-service-ui not available without plus feature-set.'
1153            )
1154
1155    def _on_custom_achievements_press(self) -> None:
1156        if bui.app.plus is not None:
1157            bui.apptimer(
1158                0.15,
1159                bui.Call(bui.app.plus.show_game_service_ui, 'achievements'),
1160            )
1161        else:
1162            logging.warning('show_game_service_ui requires plus feature-set.')
1163
1164    def _on_manage_account_press(self) -> None:
1165        self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR)
1166
1167    def _on_delete_account_press(self) -> None:
1168        self._do_manage_account_press(WebLocation.ACCOUNT_DELETE_SECTION)
1169
1170    def _do_manage_account_press(self, weblocation: WebLocation) -> None:
1171        plus = bui.app.plus
1172        assert plus is not None
1173
1174        # Preemptively fail if it looks like we won't be able to talk to
1175        # the server anyway.
1176        if not plus.cloud.connected:
1177            bui.screenmessage(
1178                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1179                color=(1, 0, 0),
1180            )
1181            bui.getsound('error').play()
1182            return
1183
1184        bui.screenmessage(bui.Lstr(resource='oneMomentText'))
1185
1186        # We expect to have a v2 account signed in if we get here.
1187        if plus.accounts.primary is None:
1188            logging.exception(
1189                'got manage-account press without v2 account present'
1190            )
1191            return
1192
1193        with plus.accounts.primary:
1194            plus.cloud.send_message_cb(
1195                bacommon.cloud.ManageAccountMessage(weblocation=weblocation),
1196                on_response=bui.WeakCall(self._on_manage_account_response),
1197            )
1198
1199    def _on_manage_account_response(
1200        self, response: bacommon.cloud.ManageAccountResponse | Exception
1201    ) -> None:
1202        if isinstance(response, Exception) or response.url is None:
1203            logging.warning(
1204                'Got error in manage-account-response: %s.', response
1205            )
1206            bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
1207            bui.getsound('error').play()
1208            return
1209
1210        bui.open_url(response.url)
1211
1212    def _on_leaderboards_press(self) -> None:
1213        if bui.app.plus is not None:
1214            bui.apptimer(
1215                0.15,
1216                bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'),
1217            )
1218        else:
1219            logging.warning('show_game_service_ui requires classic')
1220
1221    def _have_unlinkable_v1_accounts(self) -> bool:
1222        plus = bui.app.plus
1223        assert plus is not None
1224
1225        # if this is not present, we haven't had contact from the server so
1226        # let's not proceed..
1227        if plus.get_v1_account_public_login_id() is None:
1228            return False
1229        accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1230        return len(accounts) > 1
1231
1232    def _update_unlink_accounts_button(self) -> None:
1233        if self._unlink_accounts_button is None:
1234            return
1235        if self._have_unlinkable_v1_accounts():
1236            clr = (0.75, 0.7, 0.8, 1.0)
1237        else:
1238            clr = (1.0, 1.0, 1.0, 0.25)
1239        bui.textwidget(edit=self._unlink_accounts_button_label, color=clr)
1240
1241    def _should_show_legacy_unlink_button(self) -> bool:
1242        plus = bui.app.plus
1243        assert plus is not None
1244
1245        # Only show this when fully signed in to a v2 account.
1246        if not self._v1_signed_in or plus.accounts.primary is None:
1247            return False
1248
1249        out = self._have_unlinkable_v1_accounts()
1250        return out
1251
1252    def _update_linked_accounts_text(self) -> None:
1253        plus = bui.app.plus
1254        assert plus is not None
1255
1256        if self._linked_accounts_text is None:
1257            return
1258
1259        # Disable this by default when signed in to a V2 account
1260        # (since this shows V1 links which we should no longer care about).
1261        if plus.accounts.primary is not None and not FORCE_ENABLE_V1_LINKING:
1262            return
1263
1264        # if this is not present, we haven't had contact from the server so
1265        # let's not proceed..
1266        if plus.get_v1_account_public_login_id() is None:
1267            num = int(time.time()) % 4
1268            accounts_str = num * '.' + (4 - num) * ' '
1269        else:
1270            accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1271            # UPDATE - we now just print the number here; not the actual
1272            # accounts (they can see that in the unlink section if they're
1273            # curious)
1274            accounts_str = str(max(0, len(accounts) - 1))
1275        bui.textwidget(
1276            edit=self._linked_accounts_text,
1277            text=bui.Lstr(
1278                value='${L} ${A}',
1279                subs=[
1280                    (
1281                        '${L}',
1282                        bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
1283                    ),
1284                    ('${A}', accounts_str),
1285                ],
1286            ),
1287        )
1288
1289    def _refresh_campaign_progress_text(self) -> None:
1290        if self._campaign_progress_text is None:
1291            return
1292        p_str: str | bui.Lstr
1293        try:
1294            assert bui.app.classic is not None
1295            campaign = bui.app.classic.getcampaign('Default')
1296            levels = campaign.levels
1297            levels_complete = sum((1 if l.complete else 0) for l in levels)
1298
1299            # Last level cant be completed; hence the -1;
1300            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
1301            p_str = bui.Lstr(
1302                resource=f'{self._r}.campaignProgressText',
1303                subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')],
1304            )
1305        except Exception:
1306            p_str = '?'
1307            logging.exception('Error calculating co-op campaign progress.')
1308        bui.textwidget(edit=self._campaign_progress_text, text=p_str)
1309
1310    def _refresh_tickets_text(self) -> None:
1311        plus = bui.app.plus
1312        assert plus is not None
1313
1314        if self._tickets_text is None:
1315            return
1316        try:
1317            tc_str = str(plus.get_v1_account_ticket_count())
1318        except Exception:
1319            logging.exception('error refreshing tickets text')
1320            tc_str = '-'
1321        bui.textwidget(
1322            edit=self._tickets_text,
1323            text=bui.Lstr(
1324                resource=f'{self._r}.ticketsText', subs=[('${COUNT}', tc_str)]
1325            ),
1326        )
1327
1328    def _refresh_account_name_text(self) -> None:
1329        plus = bui.app.plus
1330        assert plus is not None
1331
1332        if self._account_name_text is None:
1333            return
1334        try:
1335            name_str = plus.get_v1_account_display_string()
1336        except Exception:
1337            logging.exception('error refreshing tickets text')
1338            name_str = '??'
1339
1340        bui.textwidget(edit=self._account_name_text, text=name_str)
1341
1342    def _refresh_achievements(self) -> None:
1343        assert bui.app.classic is not None
1344        if self._achievements_text is None:
1345            return
1346        complete = sum(
1347            1 if a.complete else 0 for a in bui.app.classic.ach.achievements
1348        )
1349        total = len(bui.app.classic.ach.achievements)
1350        txt_final = bui.Lstr(
1351            resource=f'{self._r}.achievementProgressText',
1352            subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))],
1353        )
1354
1355        if self._achievements_text is not None:
1356            bui.textwidget(edit=self._achievements_text, text=txt_final)
1357
1358    def _link_accounts_press(self) -> None:
1359        # pylint: disable=cyclic-import
1360        from bauiv1lib.account.link import AccountLinkWindow
1361
1362        AccountLinkWindow(origin_widget=self._link_accounts_button)
1363
1364    def _unlink_accounts_press(self) -> None:
1365        # pylint: disable=cyclic-import
1366        from bauiv1lib.account.unlink import AccountUnlinkWindow
1367
1368        if not self._have_unlinkable_v1_accounts():
1369            bui.getsound('error').play()
1370            return
1371
1372        AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
1373
1374    def _cancel_sign_in_press(self) -> None:
1375        # If we're waiting on an adapter to give us credentials, abort.
1376        self._signing_in_adapter = None
1377
1378        plus = bui.app.plus
1379        assert plus is not None
1380
1381        # Say we don't wanna be signed in anymore if we are.
1382        plus.accounts.set_primary_credentials(None)
1383
1384        self._needs_refresh = True
1385
1386        # Speed UI updates along.
1387        bui.apptimer(0.1, bui.WeakCall(self._update))
1388
1389    def _sign_out_press(self) -> None:
1390        plus = bui.app.plus
1391        assert plus is not None
1392
1393        if plus.accounts.have_primary_credentials():
1394            if (
1395                plus.accounts.primary is not None
1396                and LoginType.GPGS in plus.accounts.primary.logins
1397            ):
1398                self._explicitly_signed_out_of_gpgs = True
1399            plus.accounts.set_primary_credentials(None)
1400        else:
1401            plus.sign_out_v1()
1402
1403        cfg = bui.app.config
1404
1405        # Also take note that its our *explicit* intention to not be
1406        # signed in at this point (affects v1 accounts).
1407        cfg['Auto Account State'] = 'signed_out'
1408        cfg.commit()
1409        bui.buttonwidget(
1410            edit=self._sign_out_button,
1411            label=bui.Lstr(resource=f'{self._r}.signingOutText'),
1412        )
1413
1414        # Speed UI updates along.
1415        bui.apptimer(0.1, bui.WeakCall(self._update))
1416
1417    def _sign_in_press(self, login_type: str | LoginType) -> None:
1418        from bauiv1lib.connectivity import wait_for_connectivity
1419
1420        # If we're still waiting for our master-server connection,
1421        # keep the user informed of this instead of rushing in and
1422        # failing immediately.
1423        wait_for_connectivity(on_connected=lambda: self._sign_in(login_type))
1424
1425    def _sign_in(self, login_type: str | LoginType) -> None:
1426        plus = bui.app.plus
1427        assert plus is not None
1428
1429        # V1 login types are strings.
1430        if isinstance(login_type, str):
1431            plus.sign_in_v1(login_type)
1432
1433            # Make note of the type account we're *wanting*
1434            # to be signed in with.
1435            cfg = bui.app.config
1436            cfg['Auto Account State'] = login_type
1437            cfg.commit()
1438            self._needs_refresh = True
1439            bui.apptimer(0.1, bui.WeakCall(self._update))
1440            return
1441
1442        # V2 login sign-in buttons generally go through adapters.
1443        adapter = plus.accounts.login_adapters.get(login_type)
1444        if adapter is not None:
1445            self._signing_in_adapter = adapter
1446            adapter.sign_in(
1447                result_cb=bui.WeakCall(self._on_adapter_sign_in_result),
1448                description='account settings button',
1449            )
1450            # Will get 'Signing in...' to show.
1451            self._needs_refresh = True
1452            bui.apptimer(0.1, bui.WeakCall(self._update))
1453        else:
1454            bui.screenmessage(f'Unsupported login_type: {login_type.name}')
1455
1456    def _on_adapter_sign_in_result(
1457        self,
1458        adapter: bui.LoginAdapter,
1459        result: bui.LoginAdapter.SignInResult | Exception,
1460    ) -> None:
1461        is_us = self._signing_in_adapter is adapter
1462
1463        # If this isn't our current one we don't care.
1464        if not is_us:
1465            return
1466
1467        # If it is us, note that we're done.
1468        self._signing_in_adapter = None
1469
1470        if isinstance(result, Exception):
1471            # For now just make a bit of noise if anything went wrong;
1472            # can get more specific as needed later.
1473            logging.warning('Got error in v2 sign-in result: %s', result)
1474            bui.screenmessage(
1475                bui.Lstr(resource='internal.signInNoConnectionText'),
1476                color=(1, 0, 0),
1477            )
1478            bui.getsound('error').play()
1479        else:
1480            # Success! Plug in these credentials which will begin
1481            # verifying them and set our primary account-handle when
1482            # finished.
1483            plus = bui.app.plus
1484            assert plus is not None
1485            plus.accounts.set_primary_credentials(result.credentials)
1486
1487            # Special case - if the user has explicitly signed out and
1488            # signed in again with GPGS via this button, warn them that
1489            # they need to use the app if they want to switch to a
1490            # different GPGS account.
1491            if (
1492                self._explicitly_signed_out_of_gpgs
1493                and adapter.login_type is LoginType.GPGS
1494            ):
1495                # Delay this slightly so it hopefully pops up after
1496                # credentials go through and the account name shows up.
1497                bui.apptimer(
1498                    1.5,
1499                    bui.Call(
1500                        bui.screenmessage,
1501                        bui.Lstr(
1502                            resource=self._r
1503                            + '.googlePlayGamesAccountSwitchText'
1504                        ),
1505                    ),
1506                )
1507
1508        # Speed any UI updates along.
1509        self._needs_refresh = True
1510        bui.apptimer(0.1, bui.WeakCall(self._update))
1511
1512    def _v2_proxy_sign_in_press(self) -> None:
1513        # pylint: disable=cyclic-import
1514        from bauiv1lib.connectivity import wait_for_connectivity
1515
1516        # If we're still waiting for our master-server connection, keep
1517        # the user informed of this instead of rushing in and failing
1518        # immediately.
1519        wait_for_connectivity(on_connected=self._v2_proxy_sign_in)
1520
1521    def _v2_proxy_sign_in(self) -> None:
1522        # pylint: disable=cyclic-import
1523        from bauiv1lib.account.v2proxy import V2ProxySignInWindow
1524
1525        assert self._sign_in_v2_proxy_button is not None
1526        V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
1527
1528    def _save_state(self) -> None:
1529        try:
1530            sel = self._root_widget.get_selected_child()
1531            if sel == self._back_button:
1532                sel_name = 'Back'
1533            elif sel == self._scrollwidget:
1534                sel_name = 'Scroll'
1535            else:
1536                raise ValueError('unrecognized selection')
1537            assert bui.app.classic is not None
1538            bui.app.ui_v1.window_states[type(self)] = sel_name
1539        except Exception:
1540            logging.exception('Error saving state for %s.', self)
1541
1542    def _restore_state(self) -> None:
1543        try:
1544            assert bui.app.classic is not None
1545            sel_name = bui.app.ui_v1.window_states.get(type(self))
1546            if sel_name == 'Back':
1547                sel = self._back_button
1548            elif sel_name == 'Scroll':
1549                sel = self._scrollwidget
1550            else:
1551                sel = self._back_button
1552            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1553        except Exception:
1554            logging.exception('Error restoring state for %s.', self)

Window for account related functionality.

AccountSettingsWindow( transition: str | None = 'in_right', modal: bool = False, origin_widget: _bauiv1.Widget | None = None, close_once_signed_in: bool = False)
 28    def __init__(
 29        self,
 30        transition: str | None = 'in_right',
 31        modal: bool = False,
 32        origin_widget: bui.Widget | None = None,
 33        close_once_signed_in: bool = False,
 34    ):
 35        # pylint: disable=too-many-statements
 36
 37        plus = bui.app.plus
 38        assert plus is not None
 39
 40        self._sign_in_v2_proxy_button: bui.Widget | None = None
 41        self._sign_in_device_button: bui.Widget | None = None
 42
 43        self._show_legacy_unlink_button = False
 44
 45        self._signing_in_adapter: bui.LoginAdapter | None = None
 46        self._close_once_signed_in = close_once_signed_in
 47        bui.set_analytics_screen('Account Window')
 48
 49        self._explicitly_signed_out_of_gpgs = False
 50
 51        self._r = 'accountSettingsWindow'
 52        self._modal = modal
 53        self._needs_refresh = False
 54        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
 55        self._v1_account_state_num = plus.get_v1_account_state_num()
 56        self._check_sign_in_timer = bui.AppTimer(
 57            1.0, bui.WeakCall(self._update), repeat=True
 58        )
 59
 60        self._can_reset_achievements = False
 61
 62        app = bui.app
 63        assert app.classic is not None
 64        uiscale = app.ui_v1.uiscale
 65
 66        self._width = 850 if uiscale is bui.UIScale.SMALL else 660
 67        x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
 68        self._height = (
 69            380
 70            if uiscale is bui.UIScale.SMALL
 71            else 430 if uiscale is bui.UIScale.MEDIUM else 490
 72        )
 73
 74        self._sign_in_button = None
 75        self._sign_in_text = None
 76
 77        self._scroll_width = self._width - (100 + x_offs * 2)
 78        self._scroll_height = self._height - 120
 79        self._sub_width = self._scroll_width - 20
 80
 81        # Determine which sign-in/sign-out buttons we should show.
 82        self._show_sign_in_buttons: list[str] = []
 83
 84        if LoginType.GPGS in plus.accounts.login_adapters:
 85            self._show_sign_in_buttons.append('Google Play')
 86
 87        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
 88            self._show_sign_in_buttons.append('Game Center')
 89
 90        # Always want to show our web-based v2 login option.
 91        self._show_sign_in_buttons.append('V2Proxy')
 92
 93        # Legacy v1 device accounts available only if the user
 94        # has explicitly enabled deprecated login types.
 95        if bui.app.config.resolve('Show Deprecated Login Types'):
 96            self._show_sign_in_buttons.append('Device')
 97
 98        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
 99        super().__init__(
100            root_widget=bui.containerwidget(
101                size=(self._width, self._height + top_extra),
102                # transition=transition,
103                toolbar_visibility=(
104                    'menu_minimal'
105                    if uiscale is bui.UIScale.SMALL
106                    else 'menu_full'
107                ),
108                scale=(
109                    2.07
110                    if uiscale is bui.UIScale.SMALL
111                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
112                ),
113                stack_offset=(
114                    (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
115                ),
116            ),
117            transition=transition,
118            origin_widget=origin_widget,
119        )
120        if uiscale is bui.UIScale.SMALL:
121            self._back_button = None
122            bui.containerwidget(
123                edit=self._root_widget, on_cancel_call=self.main_window_back
124            )
125        else:
126            self._back_button = btn = bui.buttonwidget(
127                parent=self._root_widget,
128                position=(51 + x_offs, self._height - 62),
129                size=(120, 60),
130                scale=0.8,
131                text_scale=1.2,
132                autoselect=True,
133                label=bui.Lstr(
134                    resource='doneText' if self._modal else 'backText'
135                ),
136                button_type='regular' if self._modal else 'back',
137                on_activate_call=self.main_window_back,
138            )
139            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
140            if not self._modal:
141                bui.buttonwidget(
142                    edit=btn,
143                    button_type='backSmall',
144                    size=(60, 56),
145                    label=bui.charstr(bui.SpecialChar.BACK),
146                )
147
148        titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
149        titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
150        bui.textwidget(
151            parent=self._root_widget,
152            position=(self._width * 0.5, self._height - 41 + titleyoffs),
153            size=(0, 0),
154            text=bui.Lstr(resource=f'{self._r}.titleText'),
155            color=app.ui_v1.title_color,
156            scale=titlescale,
157            maxwidth=self._width - 340,
158            h_align='center',
159            v_align='center',
160        )
161
162        self._scrollwidget = bui.scrollwidget(
163            parent=self._root_widget,
164            highlight=False,
165            position=(
166                (self._width - self._scroll_width) * 0.5,
167                self._height - 65 - self._scroll_height,
168            ),
169            size=(self._scroll_width, self._scroll_height),
170            claims_left_right=True,
171            claims_tab=True,
172            selection_loops_to_parent=True,
173        )
174        self._subcontainer: bui.Widget | None = None
175        self._refresh()
176        self._restore_state()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
178    @override
179    def get_main_window_state(self) -> bui.MainWindowState:
180        # Support recreating our window for back/refresh purposes.
181        cls = type(self)
182        return bui.BasicMainWindowState(
183            create_call=lambda transition, origin_widget: cls(
184                transition=transition, origin_widget=origin_widget
185            )
186        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
188    @override
189    def on_main_window_close(self) -> None:
190        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_is_auxiliary
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget
def show_what_is_legacy_unlinking_page() -> None:
1557def show_what_is_legacy_unlinking_page() -> None:
1558    """Show the webpage describing legacy unlinking."""
1559    plus = bui.app.plus
1560    assert plus is not None
1561
1562    bamasteraddr = plus.get_master_server_address(version=2)
1563    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.