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

Window for account related functionality.

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

Show the webpage describing V2 accounts.

def show_what_is_legacy_unlinking_page() -> None:
1538def show_what_is_legacy_unlinking_page() -> None:
1539    """Show the webpage describing legacy unlinking."""
1540    bamasteraddr = ba.internal.get_master_server_address(version=2)
1541    ba.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.