From ff9ca6ea7962e8f5a58481308a723c2b4cd3d0d0 Mon Sep 17 00:00:00 2001 From: atavism Date: Fri, 29 Mar 2024 11:11:55 -0700 Subject: [PATCH] Handle language changes over websocket channel (#1032) * listen for language changes over websocket channel * only set user as pro if status is not null and active * use appLogger for error logging * Add wsMessageProp utility method * refresh pro user data after redeeming reseller code * re-enable checkout page on desktop * rename method and add comment * add comments --- desktop/app/app.go | 6 +++++ desktop/app/pro.go | 7 ++++- desktop/app/sysproxy.go | 2 +- desktop/lib.go | 2 +- desktop/ws/service.go | 11 ++++++++ lib/common/session_model.dart | 44 +++++++++++++++++++++----------- lib/ffi.dart | 2 ++ lib/plans/checkout.dart | 5 ++-- lib/plans/plan_details.dart | 11 +++----- lib/plans/reseller_checkout.dart | 14 ---------- 10 files changed, 63 insertions(+), 41 deletions(-) diff --git a/desktop/app/app.go b/desktop/app/app.go index cd9feaec0..d246816ef 100644 --- a/desktop/app/app.go +++ b/desktop/app/app.go @@ -407,6 +407,12 @@ func (app *App) GetLanguage() string { // SetLanguage sets the user language func (app *App) SetLanguage(lang string) { app.settings.SetLanguage(lang) + if app.ws != nil { + app.ws.SendMessage("pro", map[string]interface{}{ + "type": "pro", + "language": lang, + }) + } } // OnSettingChange sets a callback cb to get called when attr is changed from server. diff --git a/desktop/app/pro.go b/desktop/app/pro.go index 42d0bab96..021c709ac 100644 --- a/desktop/app/pro.go +++ b/desktop/app/pro.go @@ -1,6 +1,7 @@ package app import ( + "encoding/json" "time" "github.com/getlantern/errors" @@ -107,8 +108,12 @@ func (app *App) servePro(channel ws.UIChannel) error { } } service, err := channel.Register("pro", helloFn) + if err != nil { + return err + } pro.OnUserData(func(current *client.User, new *client.User) { - log.Debugf("Sending updated user data to all clients: %v", new) + b, _ := json.Marshal(new) + log.Debugf("Sending updated user data to all clients: %s", string(b)) service.Out <- new }) return err diff --git a/desktop/app/sysproxy.go b/desktop/app/sysproxy.go index b2c006782..d22b70b45 100644 --- a/desktop/app/sysproxy.go +++ b/desktop/app/sysproxy.go @@ -76,7 +76,7 @@ func (app *App) notifyConnectionStatus(isConnected bool) { } } -func (app *App) SetSysProxy(_sysproxyOff func() error) { +func (app *App) SetSysProxy(_sysproxyOff func() error) { app.mu.Lock() defer app.mu.Unlock() app._sysproxyOff = _sysproxyOff diff --git a/desktop/lib.go b/desktop/lib.go index eb8b1d91f..bb4f4abdb 100644 --- a/desktop/lib.go +++ b/desktop/lib.go @@ -310,7 +310,7 @@ func lang() *C.char { //export setSelectLang func setSelectLang(lang *C.char) { - a.Settings().SetLanguage(C.GoString(lang)) + a.SetLanguage(C.GoString(lang)) } //export country diff --git a/desktop/ws/service.go b/desktop/ws/service.go index 6642d8b20..d5f10df22 100644 --- a/desktop/ws/service.go +++ b/desktop/ws/service.go @@ -21,6 +21,8 @@ type UIChannel interface { // Register registers a service with an optional helloFn to send initial // message to connected clients. Register(t string, helloFn helloFnType) (*Service, error) + // SendMessage sends data over the given websocket channel + SendMessage(ch string, data any) error // RegisterWithMsgInitializer is similar to Register, but with an additional // newMsgFn to initialize the message data type to-be received from WebSocket // client, instead of letting JSON unmarshaler to guess the data type. @@ -118,6 +120,15 @@ func (c *uiChannel) Handler() http.Handler { return c.clients } +func (c *uiChannel) SendMessage(t string, data any) error { + service := c.services[t] + if service == nil { + return fmt.Errorf("No service registered %s", t) + } + service.writeMsg(data, c.clients.Out) + return nil +} + func (c *uiChannel) Register(t string, helloFn helloFnType) (*Service, error) { return c.RegisterWithMsgInitializer(t, helloFn, nil) } diff --git a/lib/common/session_model.dart b/lib/common/session_model.dart index 740a95922..d726c9a02 100644 --- a/lib/common/session_model.dart +++ b/lib/common/session_model.dart @@ -73,6 +73,13 @@ class SessionModel extends Model { late ValueNotifier proxyAvailable; late ValueNotifier country; + // wsMessageProp parses the given json, checks if it represents a pro user message and + // returns the value (if any) in the map for the given property. + String? wsMessageProp(Map json, String field) { + if (json["type"] != "pro") return null; + return json["message"][field]; + } + Widget proUser(ValueWidgetBuilder builder) { if (isMobile()) { return subscribedSingleValueBuilder('prouser', builder: builder); @@ -83,18 +90,12 @@ class SessionModel extends Model { defaultValue: false, onChanges: (setValue) { if (websocket == null) return; - - /// Listen for all incoming data websocket.messageStream.listen( (json) { - if (json["type"] == "pro") { - final userStatus = json["message"]["userStatus"]; - final isProUser = - userStatus != null && userStatus.toString() == "active"; - setValue(isProUser); - } + final userStatus = wsMessageProp(json, "userStatus"); + if (userStatus != null && userStatus.toString() == "active") setValue(true); }, - onError: (error) => print(error), + onError: (error) => appLogger.i("websocket error: ${error.description}"), ); }, ffiProUser, @@ -183,9 +184,20 @@ class SessionModel extends Model { if (isMobile()) { return subscribedSingleValueBuilder('lang', builder: builder); } + final websocket = WebsocketImpl.instance(); return ffiValueBuilder( 'lang', defaultValue: 'en', + onChanges: (setValue) { + if (websocket == null) return; + websocket.messageStream.listen( + (json) { + final language = wsMessageProp(json, "language"); + if (language != null && language != "") setValue(language); + }, + onError: (error) => appLogger.i("websocket error: ${error.description}"), + ); + }, ffiLang, builder: builder, ); @@ -256,7 +268,7 @@ class SessionModel extends Model { devices.add(Device.create()..mergeFromProto3Json(element)); } on Exception catch (e) { // Handle parsing errors as needed - print("Error parsing device data: $e"); + appLogger.i("Error parsing device data: $e"); } } return Devices.create()..devices.addAll(devices); @@ -608,11 +620,13 @@ class SessionModel extends Model { String url, String title, ) async { - return methodChannel.invokeMethod('trackUserAction', { - 'name': name, - 'url': url, - 'title': title, - }); + if (isMobile()) { + return methodChannel.invokeMethod('trackUserAction', { + 'name': name, + 'url': url, + 'title': title, + }); + } } Future requestLinkCode() { diff --git a/lib/ffi.dart b/lib/ffi.dart index 2c913bc34..a4012f3f3 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -62,6 +62,8 @@ Pointer ffiRedeemResellerCode(email, currency, deviceName, resellerCode) { final errorCode = result.r1.cast().toDartString(); throw PlatformException(code: errorCode, message: 'wrong_seller_code'.i18n); } + // if successful redeeming a reseller code, immediately refresh Pro user data + ffiProUser(); return result.r0.cast(); } diff --git a/lib/plans/checkout.dart b/lib/plans/checkout.dart index 04c968808..3b626acba 100644 --- a/lib/plans/checkout.dart +++ b/lib/plans/checkout.dart @@ -402,13 +402,14 @@ class _CheckoutState extends State ), ), // * Price summary, unused pro time disclaimer, Continue button - Center( child: Tooltip( message: AppKeys.continueCheckout, child: Button( text: 'continue'.i18n, - disabled: !showContinueButton, + // for Pro users renewing their accounts, we always have an e-mail address + // so it's unnecessary to disable the continue button + disabled: !widget.isPro ? !showContinueButton : false, onPressed: onContinueTapped, ), ), diff --git a/lib/plans/plan_details.dart b/lib/plans/plan_details.dart index 74a92eaac..2d92f3f6a 100644 --- a/lib/plans/plan_details.dart +++ b/lib/plans/plan_details.dart @@ -121,23 +121,20 @@ class PlanCard extends StatelessWidget { void onPlanTap(BuildContext context) { switch (Platform.operatingSystem) { - case 'android': - _androidCheckOut(context); - break; case 'ios': throw Exception("Not support at the moment"); break; default: - throw Exception("Not support at the moment"); - // * Fallback code + // proceed to the default checkout page on Android and desktop + _checkOut(context); break; } } - Future _androidCheckOut(BuildContext context) async { + Future _checkOut(BuildContext context) async { final isPlayVersion = sessionModel.isPlayVersion.value ?? false; final inRussia = sessionModel.country.value == 'RU'; - // * Play version + // * Play version (Android only) if (isPlayVersion && !inRussia) { await context.pushRoute( PlayCheckout( diff --git a/lib/plans/reseller_checkout.dart b/lib/plans/reseller_checkout.dart index a5e85f7a4..8155ead00 100644 --- a/lib/plans/reseller_checkout.dart +++ b/lib/plans/reseller_checkout.dart @@ -201,19 +201,5 @@ class _ResellerCodeCheckoutState extends State { .toString(), // This is coming localized ); } - - // .timeout( - // const Duration(seconds: 20), - // onTimeout: () => onAPIcallTimeout( - // code: 'redeemresellerCodeTimeout', - // message: 'reseller_timeout'.i18n, - // ), - // ) - // .then((value) { - // context.loaderOverlay.hide(); - // showSuccessDialog(context, widget.isPro, true); - // }).onError((error, stackTrace) { - // - // }); } }