bauiv1lib.account.settings

Provides UI for account functionality.

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

Window for account related functionality.

AccountSettingsWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None, close_once_signed_in: bool = False)
 29    def __init__(
 30        self,
 31        transition: str | None = 'in_right',
 32        origin_widget: bui.Widget | None = None,
 33        close_once_signed_in: bool = False,
 34    ):
 35        # pylint: disable=too-many-statements
 36        # pylint: disable=too-many-locals
 37
 38        plus = bui.app.plus
 39        assert plus is not None
 40
 41        self._sign_in_v2_proxy_button: bui.Widget | None = None
 42        self._sign_in_device_button: bui.Widget | None = None
 43
 44        self._show_legacy_unlink_button = False
 45
 46        self._signing_in_adapter: bui.LoginAdapter | None = None
 47        self._close_once_signed_in = close_once_signed_in
 48        bui.set_analytics_screen('Account Window')
 49
 50        self._explicitly_signed_out_of_gpgs = False
 51
 52        self._r = 'accountSettingsWindow'
 53        self._needs_refresh = False
 54        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
 55        self._v1_account_state_num = plus.get_v1_account_state_num()
 56        self._check_sign_in_timer = bui.AppTimer(
 57            1.0, bui.WeakCall(self._update), repeat=True
 58        )
 59
 60        self._can_reset_achievements = False
 61
 62        app = bui.app
 63        assert app.classic is not None
 64        uiscale = app.ui_v1.uiscale
 65
 66        self._width = 980 if uiscale is bui.UIScale.SMALL else 660
 67        self._height = (
 68            600
 69            if uiscale is bui.UIScale.SMALL
 70            else 430 if uiscale is bui.UIScale.MEDIUM else 490
 71        )
 72
 73        # Do some fancy math to fill all available screen area up to the
 74        # size of our backing container. This lets us fit to the exact
 75        # screen shape at small ui scale.
 76        screensize = bui.get_virtual_screen_size()
 77
 78        scale = (
 79            1.9
 80            if uiscale is bui.UIScale.SMALL
 81            else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 82        )
 83        # Calc screen size in our local container space and clamp to a
 84        # bit smaller than our container size.
 85        target_width = min(self._width - 80, screensize[0] / scale)
 86        target_height = min(self._height - 80, screensize[1] / scale)
 87
 88        # To get top/left coords, go to the center of our window and
 89        # offset by half the width/height of our target area.
 90        yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
 91
 92        self._scroll_width = target_width
 93        self._scroll_height = target_height - 33
 94        scroll_bottom = yoffs - 61 - self._scroll_height
 95
 96        self._sign_in_button = None
 97        self._sign_in_text = None
 98
 99        self._sub_width = self._scroll_width - 20
100
101        # Determine which sign-in/sign-out buttons we should show.
102        self._show_sign_in_buttons: list[str] = []
103
104        if LoginType.GPGS in plus.accounts.login_adapters:
105            self._show_sign_in_buttons.append('Google Play')
106
107        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
108            self._show_sign_in_buttons.append('Game Center')
109
110        # Always want to show our web-based v2 login option.
111        self._show_sign_in_buttons.append('V2Proxy')
112
113        # Legacy v1 device accounts available only if the user has
114        # explicitly enabled deprecated login types.
115        if bui.app.config.resolve('Show Deprecated Login Types'):
116            self._show_sign_in_buttons.append('Device')
117
118        super().__init__(
119            root_widget=bui.containerwidget(
120                size=(self._width, self._height),
121                toolbar_visibility=(
122                    'menu_minimal'
123                    if uiscale is bui.UIScale.SMALL
124                    else 'menu_full'
125                ),
126                scale=scale,
127            ),
128            transition=transition,
129            origin_widget=origin_widget,
130            # We're affected by screen size only at small ui-scale.
131            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
132        )
133        if uiscale is bui.UIScale.SMALL:
134            self._back_button = None
135            bui.containerwidget(
136                edit=self._root_widget, on_cancel_call=self.main_window_back
137            )
138        else:
139            self._back_button = btn = bui.buttonwidget(
140                parent=self._root_widget,
141                position=(51, yoffs - 52.0),
142                size=(120, 60),
143                scale=0.8,
144                text_scale=1.2,
145                autoselect=True,
146                label=bui.Lstr(resource='backText'),
147                button_type='back',
148                on_activate_call=self.main_window_back,
149            )
150            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
151            bui.buttonwidget(
152                edit=btn,
153                button_type='backSmall',
154                size=(60, 56),
155                label=bui.charstr(bui.SpecialChar.BACK),
156            )
157
158        titleyoffs = -45.0 if uiscale is bui.UIScale.SMALL else -28.0
159        titlescale = 0.7 if uiscale is bui.UIScale.SMALL else 1.0
160        bui.textwidget(
161            parent=self._root_widget,
162            position=(
163                self._width * 0.5,
164                yoffs + titleyoffs,
165            ),
166            size=(0, 0),
167            text=bui.Lstr(resource=f'{self._r}.titleText'),
168            color=app.ui_v1.title_color,
169            scale=titlescale,
170            maxwidth=self._width - 340,
171            h_align='center',
172            v_align='center',
173        )
174
175        self._scrollwidget = bui.scrollwidget(
176            parent=self._root_widget,
177            highlight=False,
178            size=(self._scroll_width, self._scroll_height),
179            position=(
180                self._width * 0.5 - self._scroll_width * 0.5,
181                scroll_bottom,
182            ),
183            claims_left_right=True,
184            selection_loops_to_parent=True,
185            border_opacity=0.4,
186        )
187        self._subcontainer: bui.Widget | None = None
188        self._refresh()
189        self._restore_state()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
191    @override
192    def get_main_window_state(self) -> bui.MainWindowState:
193        # Support recreating our window for back/refresh purposes.
194        cls = type(self)
195        return bui.BasicMainWindowState(
196            create_call=lambda transition, origin_widget: cls(
197                transition=transition, origin_widget=origin_widget
198            )
199        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
201    @override
202    def on_main_window_close(self) -> None:
203        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

def show_what_is_legacy_unlinking_page() -> None:
1625def show_what_is_legacy_unlinking_page() -> None:
1626    """Show the webpage describing legacy unlinking."""
1627    plus = bui.app.plus
1628    assert plus is not None
1629
1630    bamasteraddr = plus.get_master_server_address(version=2)
1631    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.