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

Window for account related functionality.

AccountSettingsWindow( transition: str = 'in_right', modal: bool = False, origin_widget: _bauiv1.Widget | None = None, close_once_signed_in: bool = False)
 25    def __init__(
 26        self,
 27        transition: str = 'in_right',
 28        modal: bool = False,
 29        origin_widget: bui.Widget | None = None,
 30        close_once_signed_in: bool = False,
 31    ):
 32        # pylint: disable=too-many-statements
 33
 34        plus = bui.app.plus
 35        assert plus is not None
 36
 37        self._sign_in_v2_proxy_button: bui.Widget | None = None
 38        self._sign_in_device_button: bui.Widget | None = None
 39
 40        self._show_legacy_unlink_button = False
 41
 42        self._signing_in_adapter: bui.LoginAdapter | None = None
 43        self._close_once_signed_in = close_once_signed_in
 44        bui.set_analytics_screen('Account Window')
 45
 46        self._explicitly_signed_out_of_gpgs = False
 47
 48        # If they provided an origin-widget, scale up from that.
 49        scale_origin: tuple[float, float] | None
 50        if origin_widget is not None:
 51            self._transition_out = 'out_scale'
 52            scale_origin = origin_widget.get_screen_space_center()
 53            transition = 'in_scale'
 54        else:
 55            self._transition_out = 'out_right'
 56            scale_origin = None
 57
 58        self._r = 'accountSettingsWindow'
 59        self._modal = modal
 60        self._needs_refresh = False
 61        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
 62        self._v1_account_state_num = plus.get_v1_account_state_num()
 63        self._check_sign_in_timer = bui.AppTimer(
 64            1.0, bui.WeakCall(self._update), repeat=True
 65        )
 66
 67        self._can_reset_achievements = False
 68
 69        app = bui.app
 70        assert app.classic is not None
 71        uiscale = app.ui_v1.uiscale
 72
 73        self._width = 860 if uiscale is bui.UIScale.SMALL else 660
 74        x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
 75        self._height = (
 76            390
 77            if uiscale is bui.UIScale.SMALL
 78            else 430 if uiscale is bui.UIScale.MEDIUM else 490
 79        )
 80
 81        self._sign_in_button = None
 82        self._sign_in_text = None
 83
 84        self._scroll_width = self._width - (100 + x_offs * 2)
 85        self._scroll_height = self._height - 120
 86        self._sub_width = self._scroll_width - 20
 87
 88        # Determine which sign-in/sign-out buttons we should show.
 89        self._show_sign_in_buttons: list[str] = []
 90
 91        if LoginType.GPGS in plus.accounts.login_adapters:
 92            self._show_sign_in_buttons.append('Google Play')
 93
 94        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
 95            self._show_sign_in_buttons.append('Game Center')
 96
 97        # Always want to show our web-based v2 login option.
 98        self._show_sign_in_buttons.append('V2Proxy')
 99
100        # Legacy v1 device accounts available only if the user
101        # has explicitly enabled deprecated login types.
102        if bui.app.config.resolve('Show Deprecated Login Types'):
103            self._show_sign_in_buttons.append('Device')
104
105        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
106        super().__init__(
107            root_widget=bui.containerwidget(
108                size=(self._width, self._height + top_extra),
109                transition=transition,
110                toolbar_visibility='menu_minimal',
111                scale_origin_stack_offset=scale_origin,
112                scale=(
113                    2.09
114                    if uiscale is bui.UIScale.SMALL
115                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
116                ),
117                stack_offset=(
118                    (0, -19) if uiscale is bui.UIScale.SMALL else (0, 0)
119                ),
120            )
121        )
122        if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars:
123            self._back_button = None
124            bui.containerwidget(
125                edit=self._root_widget, on_cancel_call=self._back
126            )
127        else:
128            self._back_button = btn = bui.buttonwidget(
129                parent=self._root_widget,
130                position=(51 + x_offs, self._height - 62),
131                size=(120, 60),
132                scale=0.8,
133                text_scale=1.2,
134                autoselect=True,
135                label=bui.Lstr(
136                    resource='doneText' if self._modal else 'backText'
137                ),
138                button_type='regular' if self._modal else 'back',
139                on_activate_call=self._back,
140            )
141            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
142            if not self._modal:
143                bui.buttonwidget(
144                    edit=btn,
145                    button_type='backSmall',
146                    size=(60, 56),
147                    label=bui.charstr(bui.SpecialChar.BACK),
148                )
149
150        bui.textwidget(
151            parent=self._root_widget,
152            position=(self._width * 0.5, self._height - 41),
153            size=(0, 0),
154            text=bui.Lstr(resource=self._r + '.titleText'),
155            color=app.ui_v1.title_color,
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()
Inherited Members
bauiv1._uitypes.Window
get_root_widget
def show_what_is_v2_page() -> None:
1763def show_what_is_v2_page() -> None:
1764    """Show the webpage describing V2 accounts."""
1765    plus = bui.app.plus
1766    assert plus is not None
1767
1768    bamasteraddr = plus.get_master_server_address(version=2)
1769    bui.open_url(f'{bamasteraddr}/whatisv2')

Show the webpage describing V2 accounts.

def show_what_is_legacy_unlinking_page() -> None:
1772def show_what_is_legacy_unlinking_page() -> None:
1773    """Show the webpage describing legacy unlinking."""
1774    plus = bui.app.plus
1775    assert plus is not None
1776
1777    bamasteraddr = plus.get_master_server_address(version=2)
1778    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.