bauiv1lib.account.v2proxy

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

A window allowing signing in to a v2 account.

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