bastd.ui.account.v2

V2 account ui bits.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""V2 account ui bits."""
  4
  5from __future__ import annotations
  6
  7import logging
  8from typing import TYPE_CHECKING
  9
 10import ba
 11import _ba
 12
 13from efro.error import CommunicationError
 14import bacommon.cloud
 15
 16if TYPE_CHECKING:
 17    pass
 18
 19STATUS_CHECK_INTERVAL_SECONDS = 2.0
 20
 21
 22class V2SignInWindow(ba.Window):
 23    """A window allowing signing in to a v2 account."""
 24
 25    def __init__(self, origin_widget: ba.Widget):
 26        self._width = 600
 27        self._height = 550
 28        self._proxyid: str | None = None
 29        self._proxykey: str | None = None
 30
 31        uiscale = ba.app.ui.uiscale
 32        super().__init__(root_widget=ba.containerwidget(
 33            size=(self._width, self._height),
 34            transition='in_scale',
 35            scale_origin_stack_offset=origin_widget.get_screen_space_center(),
 36            scale=(1.25 if uiscale is ba.UIScale.SMALL else
 37                   1.05 if uiscale is ba.UIScale.MEDIUM else 0.9)))
 38
 39        self._loading_text = ba.textwidget(
 40            parent=self._root_widget,
 41            position=(self._width * 0.5, self._height * 0.5),
 42            h_align='center',
 43            v_align='center',
 44            size=(0, 0),
 45            maxwidth=0.9 * self._width,
 46            text=ba.Lstr(value='${A}...',
 47                         subs=[('${A}', ba.Lstr(resource='loadingText'))]),
 48        )
 49
 50        self._cancel_button = ba.buttonwidget(
 51            parent=self._root_widget,
 52            position=(30, self._height - 65),
 53            size=(130, 50),
 54            scale=0.8,
 55            label=ba.Lstr(resource='cancelText'),
 56            on_activate_call=self._done,
 57            autoselect=True,
 58            textcolor=(0.75, 0.7, 0.8),
 59        )
 60        ba.containerwidget(edit=self._root_widget,
 61                           cancel_button=self._cancel_button)
 62
 63        self._update_timer: ba.Timer | None = None
 64
 65        # Ask the cloud for a proxy login id.
 66        ba.app.cloud.send_message_cb(bacommon.cloud.LoginProxyRequestMessage(),
 67                                     on_response=ba.WeakCall(
 68                                         self._on_proxy_request_response))
 69
 70    def _on_proxy_request_response(
 71        self, response: bacommon.cloud.LoginProxyRequestResponse | Exception
 72    ) -> None:
 73        from ba.internal import is_browser_likely_available
 74
 75        # Something went wrong. Show an error message and that's it.
 76        if isinstance(response, Exception):
 77            ba.textwidget(
 78                edit=self._loading_text,
 79                text=ba.Lstr(resource='internal.unavailableNoConnectionText'),
 80                color=(1, 0, 0))
 81            return
 82
 83        # Show link(s) the user can use to log in.
 84        address = _ba.get_master_server_address(version=2) + response.url
 85        address_pretty = address.removeprefix('https://')
 86
 87        ba.textwidget(
 88            parent=self._root_widget,
 89            position=(self._width * 0.5, self._height - 95),
 90            size=(0, 0),
 91            text=ba.Lstr(
 92                resource='accountSettingsWindow.v2LinkInstructionsText'),
 93            color=ba.app.ui.title_color,
 94            maxwidth=self._width * 0.9,
 95            h_align='center',
 96            v_align='center')
 97        button_width = 450
 98        if is_browser_likely_available():
 99            ba.buttonwidget(parent=self._root_widget,
100                            position=((self._width * 0.5 - button_width * 0.5),
101                                      self._height - 185),
102                            autoselect=True,
103                            size=(button_width, 60),
104                            label=ba.Lstr(value=address_pretty),
105                            color=(0.55, 0.5, 0.6),
106                            textcolor=(0.75, 0.7, 0.8),
107                            on_activate_call=lambda: ba.open_url(address))
108            qroffs = 0.0
109        else:
110            ba.textwidget(parent=self._root_widget,
111                          position=(self._width * 0.5, self._height - 145),
112                          size=(0, 0),
113                          text=ba.Lstr(value=address_pretty),
114                          flatness=1.0,
115                          maxwidth=self._width,
116                          scale=0.75,
117                          h_align='center',
118                          v_align='center')
119            qroffs = 20.0
120
121        qr_size = 270
122        ba.imagewidget(parent=self._root_widget,
123                       position=(self._width * 0.5 - qr_size * 0.5,
124                                 self._height * 0.36 + qroffs - qr_size * 0.5),
125                       size=(qr_size, qr_size),
126                       texture=_ba.get_qrcode_texture(address))
127
128        # Start querying for results.
129        self._proxyid = response.proxyid
130        self._proxykey = response.proxykey
131        ba.timer(STATUS_CHECK_INTERVAL_SECONDS,
132                 ba.WeakCall(self._ask_for_status))
133
134    def _ask_for_status(self) -> None:
135        assert self._proxyid is not None
136        assert self._proxykey is not None
137        ba.app.cloud.send_message_cb(
138            bacommon.cloud.LoginProxyStateQueryMessage(
139                proxyid=self._proxyid, proxykey=self._proxykey),
140            on_response=ba.WeakCall(self._got_status))
141
142    def _got_status(
143        self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception
144    ) -> None:
145
146        # For now, if anything goes wrong on the server-side, just abort
147        # with a vague error message. Can be more verbose later if need be.
148        if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
149                and response.state is response.State.FAIL):
150            ba.playsound(ba.getsound('error'))
151            ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
152            self._done()
153            return
154
155        # If we got a token, set ourself as signed in. Hooray!
156        if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
157                and response.state is response.State.SUCCESS):
158            assert response.credentials is not None
159            ba.app.accounts_v2.set_primary_credentials(response.credentials)
160
161            # As a courtesy, tell the server we're done with this proxy
162            # so it can clean up (not a huge deal if this fails)
163            assert self._proxyid is not None
164            try:
165                ba.app.cloud.send_message_cb(
166                    bacommon.cloud.LoginProxyCompleteMessage(
167                        proxyid=self._proxyid),
168                    on_response=ba.WeakCall(self._proxy_complete_response))
169            except CommunicationError:
170                pass
171            except Exception:
172                logging.warning(
173                    'Unexpected error sending login-proxy-complete message',
174                    exc_info=True)
175
176            self._done()
177            return
178
179        # If we're still waiting, ask again soon.
180        if (isinstance(response, Exception)
181                or response.state is response.State.WAITING):
182            ba.timer(STATUS_CHECK_INTERVAL_SECONDS,
183                     ba.WeakCall(self._ask_for_status))
184
185    def _proxy_complete_response(self, response: None | Exception) -> None:
186        del response  # Not used.
187        # We could do something smart like retry on exceptions here, but
188        # this isn't critical so we'll just let anything slide.
189
190    def _done(self) -> None:
191        ba.containerwidget(edit=self._root_widget, transition='out_scale')
class V2SignInWindow(ba.ui.Window):
 23class V2SignInWindow(ba.Window):
 24    """A window allowing signing in to a v2 account."""
 25
 26    def __init__(self, origin_widget: ba.Widget):
 27        self._width = 600
 28        self._height = 550
 29        self._proxyid: str | None = None
 30        self._proxykey: str | None = None
 31
 32        uiscale = ba.app.ui.uiscale
 33        super().__init__(root_widget=ba.containerwidget(
 34            size=(self._width, self._height),
 35            transition='in_scale',
 36            scale_origin_stack_offset=origin_widget.get_screen_space_center(),
 37            scale=(1.25 if uiscale is ba.UIScale.SMALL else
 38                   1.05 if uiscale is ba.UIScale.MEDIUM else 0.9)))
 39
 40        self._loading_text = ba.textwidget(
 41            parent=self._root_widget,
 42            position=(self._width * 0.5, self._height * 0.5),
 43            h_align='center',
 44            v_align='center',
 45            size=(0, 0),
 46            maxwidth=0.9 * self._width,
 47            text=ba.Lstr(value='${A}...',
 48                         subs=[('${A}', ba.Lstr(resource='loadingText'))]),
 49        )
 50
 51        self._cancel_button = ba.buttonwidget(
 52            parent=self._root_widget,
 53            position=(30, self._height - 65),
 54            size=(130, 50),
 55            scale=0.8,
 56            label=ba.Lstr(resource='cancelText'),
 57            on_activate_call=self._done,
 58            autoselect=True,
 59            textcolor=(0.75, 0.7, 0.8),
 60        )
 61        ba.containerwidget(edit=self._root_widget,
 62                           cancel_button=self._cancel_button)
 63
 64        self._update_timer: ba.Timer | None = None
 65
 66        # Ask the cloud for a proxy login id.
 67        ba.app.cloud.send_message_cb(bacommon.cloud.LoginProxyRequestMessage(),
 68                                     on_response=ba.WeakCall(
 69                                         self._on_proxy_request_response))
 70
 71    def _on_proxy_request_response(
 72        self, response: bacommon.cloud.LoginProxyRequestResponse | Exception
 73    ) -> None:
 74        from ba.internal import is_browser_likely_available
 75
 76        # Something went wrong. Show an error message and that's it.
 77        if isinstance(response, Exception):
 78            ba.textwidget(
 79                edit=self._loading_text,
 80                text=ba.Lstr(resource='internal.unavailableNoConnectionText'),
 81                color=(1, 0, 0))
 82            return
 83
 84        # Show link(s) the user can use to log in.
 85        address = _ba.get_master_server_address(version=2) + response.url
 86        address_pretty = address.removeprefix('https://')
 87
 88        ba.textwidget(
 89            parent=self._root_widget,
 90            position=(self._width * 0.5, self._height - 95),
 91            size=(0, 0),
 92            text=ba.Lstr(
 93                resource='accountSettingsWindow.v2LinkInstructionsText'),
 94            color=ba.app.ui.title_color,
 95            maxwidth=self._width * 0.9,
 96            h_align='center',
 97            v_align='center')
 98        button_width = 450
 99        if is_browser_likely_available():
100            ba.buttonwidget(parent=self._root_widget,
101                            position=((self._width * 0.5 - button_width * 0.5),
102                                      self._height - 185),
103                            autoselect=True,
104                            size=(button_width, 60),
105                            label=ba.Lstr(value=address_pretty),
106                            color=(0.55, 0.5, 0.6),
107                            textcolor=(0.75, 0.7, 0.8),
108                            on_activate_call=lambda: ba.open_url(address))
109            qroffs = 0.0
110        else:
111            ba.textwidget(parent=self._root_widget,
112                          position=(self._width * 0.5, self._height - 145),
113                          size=(0, 0),
114                          text=ba.Lstr(value=address_pretty),
115                          flatness=1.0,
116                          maxwidth=self._width,
117                          scale=0.75,
118                          h_align='center',
119                          v_align='center')
120            qroffs = 20.0
121
122        qr_size = 270
123        ba.imagewidget(parent=self._root_widget,
124                       position=(self._width * 0.5 - qr_size * 0.5,
125                                 self._height * 0.36 + qroffs - qr_size * 0.5),
126                       size=(qr_size, qr_size),
127                       texture=_ba.get_qrcode_texture(address))
128
129        # Start querying for results.
130        self._proxyid = response.proxyid
131        self._proxykey = response.proxykey
132        ba.timer(STATUS_CHECK_INTERVAL_SECONDS,
133                 ba.WeakCall(self._ask_for_status))
134
135    def _ask_for_status(self) -> None:
136        assert self._proxyid is not None
137        assert self._proxykey is not None
138        ba.app.cloud.send_message_cb(
139            bacommon.cloud.LoginProxyStateQueryMessage(
140                proxyid=self._proxyid, proxykey=self._proxykey),
141            on_response=ba.WeakCall(self._got_status))
142
143    def _got_status(
144        self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception
145    ) -> None:
146
147        # For now, if anything goes wrong on the server-side, just abort
148        # with a vague error message. Can be more verbose later if need be.
149        if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
150                and response.state is response.State.FAIL):
151            ba.playsound(ba.getsound('error'))
152            ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
153            self._done()
154            return
155
156        # If we got a token, set ourself as signed in. Hooray!
157        if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
158                and response.state is response.State.SUCCESS):
159            assert response.credentials is not None
160            ba.app.accounts_v2.set_primary_credentials(response.credentials)
161
162            # As a courtesy, tell the server we're done with this proxy
163            # so it can clean up (not a huge deal if this fails)
164            assert self._proxyid is not None
165            try:
166                ba.app.cloud.send_message_cb(
167                    bacommon.cloud.LoginProxyCompleteMessage(
168                        proxyid=self._proxyid),
169                    on_response=ba.WeakCall(self._proxy_complete_response))
170            except CommunicationError:
171                pass
172            except Exception:
173                logging.warning(
174                    'Unexpected error sending login-proxy-complete message',
175                    exc_info=True)
176
177            self._done()
178            return
179
180        # If we're still waiting, ask again soon.
181        if (isinstance(response, Exception)
182                or response.state is response.State.WAITING):
183            ba.timer(STATUS_CHECK_INTERVAL_SECONDS,
184                     ba.WeakCall(self._ask_for_status))
185
186    def _proxy_complete_response(self, response: None | Exception) -> None:
187        del response  # Not used.
188        # We could do something smart like retry on exceptions here, but
189        # this isn't critical so we'll just let anything slide.
190
191    def _done(self) -> None:
192        ba.containerwidget(edit=self._root_widget, transition='out_scale')

A window allowing signing in to a v2 account.

V2SignInWindow(origin_widget: _ba.Widget)
26    def __init__(self, origin_widget: ba.Widget):
27        self._width = 600
28        self._height = 550
29        self._proxyid: str | None = None
30        self._proxykey: str | None = None
31
32        uiscale = ba.app.ui.uiscale
33        super().__init__(root_widget=ba.containerwidget(
34            size=(self._width, self._height),
35            transition='in_scale',
36            scale_origin_stack_offset=origin_widget.get_screen_space_center(),
37            scale=(1.25 if uiscale is ba.UIScale.SMALL else
38                   1.05 if uiscale is ba.UIScale.MEDIUM else 0.9)))
39
40        self._loading_text = ba.textwidget(
41            parent=self._root_widget,
42            position=(self._width * 0.5, self._height * 0.5),
43            h_align='center',
44            v_align='center',
45            size=(0, 0),
46            maxwidth=0.9 * self._width,
47            text=ba.Lstr(value='${A}...',
48                         subs=[('${A}', ba.Lstr(resource='loadingText'))]),
49        )
50
51        self._cancel_button = ba.buttonwidget(
52            parent=self._root_widget,
53            position=(30, self._height - 65),
54            size=(130, 50),
55            scale=0.8,
56            label=ba.Lstr(resource='cancelText'),
57            on_activate_call=self._done,
58            autoselect=True,
59            textcolor=(0.75, 0.7, 0.8),
60        )
61        ba.containerwidget(edit=self._root_widget,
62                           cancel_button=self._cancel_button)
63
64        self._update_timer: ba.Timer | None = None
65
66        # Ask the cloud for a proxy login id.
67        ba.app.cloud.send_message_cb(bacommon.cloud.LoginProxyRequestMessage(),
68                                     on_response=ba.WeakCall(
69                                         self._on_proxy_request_response))
Inherited Members
ba.ui.Window
get_root_widget