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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
182    @override
183    def on_main_window_close(self) -> None:
184        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:
1609def show_what_is_legacy_unlinking_page() -> None:
1610    """Show the webpage describing legacy unlinking."""
1611    plus = bui.app.plus
1612    assert plus is not None
1613
1614    bamasteraddr = plus.get_master_server_address(version=2)
1615    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.