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

Show the webpage describing V2 accounts.

def show_what_is_legacy_unlinking_page() -> None:
1801def show_what_is_legacy_unlinking_page() -> None:
1802    """Show the webpage describing legacy unlinking."""
1803    plus = bui.app.plus
1804    assert plus is not None
1805
1806    bamasteraddr = plus.get_master_server_address(version=2)
1807    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.