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

Return a WindowState to recreate this window, if supported.

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

Show the webpage describing legacy unlinking.