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