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

Show the webpage describing V2 accounts.

def show_what_is_legacy_unlinking_page() -> None:
1583def show_what_is_legacy_unlinking_page() -> None:
1584    """Show the webpage describing legacy unlinking."""
1585    plus = bui.app.plus
1586    assert plus is not None
1587
1588    bamasteraddr = plus.get_master_server_address(version=2)
1589    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.