bauiv1lib.account.settings

Provides UI for account functionality.

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

Window for account related functionality.

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

Show the webpage describing V2 accounts.

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

Show the webpage describing legacy unlinking.