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