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

Show the webpage describing V2 accounts.

def show_what_is_legacy_unlinking_page() -> None:
1739def show_what_is_legacy_unlinking_page() -> None:
1740    """Show the webpage describing legacy unlinking."""
1741    plus = bui.app.plus
1742    assert plus is not None
1743
1744    bamasteraddr = plus.get_master_server_address(version=2)
1745    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.