From b397e87176305d343035f9b0c217308781f991a8 Mon Sep 17 00:00:00 2001 From: jigar-f <132374182+jigar-f@users.noreply.github.com> Date: Mon, 10 Jun 2024 18:29:31 +0530 Subject: [PATCH] Start up sequence callback (#1070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * expose callback to internal sdk. * added callbacks. * Added callback for ui. * Implemented loading shimmer for VPN tap. * Implemented internet checker. * Disable VPN if internet connection not available. * added logs and use dynamic vaules. * Update GeneratedPluginRegistrant.swift * Show init message on startup for desktop * updated internal sdk to new flashlight * 🚀 Add startup sequence callback for Android * Remove InitCallback from app.go * Remove network warning from android and use flutter one. * Track timer while startup android. * change method name. * Change switch package. * Fix other small UI issue. * Fixed conflicts. * PR review udpates. * update go.opentelemetry.io/otel/sdk/metric * update how new instance of bandit is created in ios code * switch back to same flashlight version. * Update flashlight again * Update flashlight again * Update flashlight again --------- Co-authored-by: atavism --- .../mobilesdk/model/SessionManager.kt | 2 +- .../mobilesdk/util/DnsDetector.java | 2 - .../lantern/model/LanternSessionManager.kt | 22 +++ assets/images/cloud_off.svg | 10 ++ assets/locales/en-us.po | 25 +++ desktop/app/app.go | 74 ++++---- desktop/lib.go | 31 +++- go.mod | 6 +- go.sum | 16 +- internalsdk/android.go | 57 ++++-- internalsdk/android_test.go | 3 + internalsdk/ios/ios.go | 9 +- internalsdk/session_model.go | 20 +++ lib/account/developer_settings.dart | 22 ++- lib/account/settings.dart | 11 +- lib/app.dart | 163 ++++++++++-------- lib/common/common.dart | 4 +- lib/common/session_model.dart | 20 +-- lib/common/ui/colors.dart | 1 + lib/common/ui/custom/dialog.dart | 104 +++++++++++ lib/common/ui/custom/internet_checker.dart | 105 +++++++++++ lib/common/ui/image_paths.dart | 1 + lib/ffi.dart | 12 ++ lib/home.dart | 6 +- lib/vpn/vpn_notifier.dart | 110 ++++++++++++ lib/vpn/vpn_switch.dart | 46 +++-- lib/vpn/vpn_tab.dart | 156 +++++++++++++---- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 12 +- macos/Runner/Info.plist | 2 + pubspec.lock | 56 +++++- pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 34 files changed, 897 insertions(+), 221 deletions(-) create mode 100644 assets/images/cloud_off.svg create mode 100644 lib/common/ui/custom/internet_checker.dart create mode 100644 lib/vpn/vpn_notifier.dart diff --git a/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt b/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt index 350189d74..d422991c2 100644 --- a/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt +++ b/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt @@ -393,7 +393,7 @@ abstract class SessionManager(application: Application) : Session { adsBlocked: Long, hasSucceedingProxy: Boolean, ) { - Logger.debug("updateStats", "city $city, country $country, countryCode $countryCode") + Logger.debug("updateStats", "city $city, country $country, countryCode $countryCode hasSucceedingProxy $hasSucceedingProxy") if (hasUpdatedStats.compareAndSet(false, true)) { // The first time that we get the stats, hasSucceedingProxy is always false because we // haven't hit any proxies yet. So, we just ignore the stats. diff --git a/android/app/src/main/java/org/getlantern/mobilesdk/util/DnsDetector.java b/android/app/src/main/java/org/getlantern/mobilesdk/util/DnsDetector.java index a3ad28faa..3afa957df 100644 --- a/android/app/src/main/java/org/getlantern/mobilesdk/util/DnsDetector.java +++ b/android/app/src/main/java/org/getlantern/mobilesdk/util/DnsDetector.java @@ -61,14 +61,12 @@ public DnsDetector(Context context, String fakeDnsIP) { public void onAvailable(@NonNull Network network) { Logger.debug(TAG, "Adding available network"); allNetworks.put(network, ""); - EventBus.getDefault().postSticky(Event.NetworkAvailable); } @Override public void onLost(@NonNull Network network) { Logger.debug(TAG, "Removing lost network"); allNetworks.remove(network); - publishNetworkAvailability(); } } ); diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt index 08102c5ea..cd18fa7fe 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt @@ -263,6 +263,24 @@ class LanternSessionManager(application: Application) : SessionManager(applicati return prefs.getString(PROVIDER, "") } + override fun setHasConfigFetched(hasConfig: Boolean) { + db.mutate { + it.put(PATH_HAS_CONFIG, hasConfig) + } + } + + override fun setHasProxyFetched(hasProxy: Boolean) { + db.mutate { + it.put(PATH_HAS_PROXY, hasProxy) + } + } + + override fun setOnSuccess(hasConnection: Boolean) { + db.mutate { + it.put(PATH_HAS_ONSUCESS, hasConnection) + } + } + fun setReferral(referralCode: String?) { referral = referralCode } @@ -436,6 +454,10 @@ class LanternSessionManager(application: Application) : SessionManager(applicati private const val RENEWAL_LAST_SEEN = "renewalseen" private const val PROVIDER = "provider" private const val RESELLER_CODE = "resellercode" + private const val PATH_HAS_CONFIG = "hasConfigFetched" + private const val PATH_HAS_PROXY = "hasProxyFetched" + private const val PATH_HAS_ONSUCESS = "hasOnSuccess" + // other constants private const val DEFAULT_ONE_YEAR_COST: Long = 3200 diff --git a/assets/images/cloud_off.svg b/assets/images/cloud_off.svg new file mode 100644 index 000000000..24a5efe51 --- /dev/null +++ b/assets/images/cloud_off.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/locales/en-us.po b/assets/locales/en-us.po index 8561100fe..dc77f8a23 100644 --- a/assets/locales/en-us.po +++ b/assets/locales/en-us.po @@ -1527,6 +1527,31 @@ msgstr "Sorry, we are unable to load that page at the moment, please tap ‘Refr msgid "refresh" msgstr "Refresh" +msgid "check_your_internet_connection" +msgstr "Check your internet connection" + +msgid "please_try" +msgstr "Please try" + +msgid "turning_off_airplane_mode" +msgstr "Turning off airplane mode" + +msgid "turning_on_mobile_data_or_wifi" +msgstr "Turning on mobile data or wifi" + +msgid "check_the_signal_in_your_area" +msgstr "Check the signal in your area" + +msgid "got_it" +msgstr "Got it" + +msgid "fetching_configuration" +msgstr "Fetching Configuration" + +msgid "establish_connection_to_server" +msgstr "Establish connection to server" + + msgid "file_size_limit_title" msgstr "Limit Reached" diff --git a/desktop/app/app.go b/desktop/app/app.go index decd0103a..bec9b3ea4 100644 --- a/desktop/app/app.go +++ b/desktop/app/app.go @@ -55,6 +55,7 @@ var ( func init() { autoupdate.Version = common.ApplicationVersion autoupdate.PublicKey = []byte(packagePublicKey) + } // App is the core of the Lantern desktop application, in the form of a library. @@ -62,6 +63,7 @@ type App struct { hasExited int64 fetchedGlobalConfig int32 fetchedProxiesConfig int32 + hasSucceedingProxy int32 Flags flashlight.Flags configDir string @@ -187,15 +189,6 @@ func (app *App) Run(isMain bool) { }() } - if app.Flags.Initialize { - app.statsTracker.AddListener(func(newStats stats.Stats) { - if newStats.HasSucceedingProxy { - log.Debug("Finished initialization") - app.Exit(nil) - } - }) - } - cacheDir, err := os.UserCacheDir() if err != nil { cacheDir = os.TempDir() @@ -214,14 +207,22 @@ func (app *App) Run(isMain bool) { func() bool { return false }, // on desktop, we do not allow private hosts app.settings.IsAutoReport, app.Flags.AsMap(), - app.onConfigUpdate, - app.onProxiesUpdate, app.settings, app.statsTracker, app.IsPro, app.settings.GetLanguage, func(addr string) (string, error) { return addr, nil }, // no dnsgrab reverse lookups on desktop app.analyticsSession.EventWithLabel, + flashlight.WithOnConfig(app.onConfigUpdate), + flashlight.WithOnProxies(app.onProxiesUpdate), + flashlight.WithOnDialError(func(err error, hasSucceeding bool) { + if err != nil && !hasSucceeding { + app.onSucceedingProxy(hasSucceeding) + } + }), + flashlight.WithOnSucceedingProxy(func() { + app.onSucceedingProxy(true) + }), ) if err != nil { app.Exit(err) @@ -240,6 +241,7 @@ func (app *App) Run(isMain bool) { app.startFeaturesService(geolookup.OnRefresh(), chUserChanged, chProStatusChanged, app.chGlobalConfigChanged) notifyConfigSaveErrorOnce := new(sync.Once) + app.flashlight.SetErrorHandler(func(t flashlight.HandledErrorType, err error) { switch t { case flashlight.ErrorTypeProxySaveFailure, flashlight.ErrorTypeConfigSaveFailure: @@ -354,20 +356,6 @@ func (app *App) beforeStart(listenAddr string) { app.AddExitFunc("stopping notifier", notifier.NotificationsLoop(app.analyticsSession)) } -// Connect turns on proxying -func (app *App) Connect() { - app.analyticsSession.Event("systray-menu", "connect") - ops.Begin("connect").End() - app.settings.SetDisconnected(false) -} - -// Disconnect turns off proxying -func (app *App) Disconnect() { - app.analyticsSession.Event("systray-menu", "disconnect") - ops.Begin("disconnect").End() - app.settings.SetDisconnected(true) -} - // GetLanguage returns the user language func (app *App) GetLanguage() string { return app.settings.GetLanguage() @@ -429,9 +417,8 @@ func (app *App) afterStart(cl *flashlightClient.Client) { } func (app *App) onConfigUpdate(cfg *config.Global, src config.Source) { - if src == config.Fetched { - atomic.StoreInt32(&app.fetchedGlobalConfig, 1) - } + log.Debugf("[Startup Desktop] Got config update from %v", src) + atomic.StoreInt32(&app.fetchedGlobalConfig, 1) autoupdate.Configure(cfg.UpdateServerURL, cfg.AutoUpdateCA, func() string { return "/img/lantern_logo.png" }) @@ -447,9 +434,36 @@ func (app *App) onConfigUpdate(cfg *config.Global, src config.Source) { } func (app *App) onProxiesUpdate(proxies []bandit.Dialer, src config.Source) { - if src == config.Fetched { - atomic.StoreInt32(&app.fetchedProxiesConfig, 1) + log.Debugf("[Startup Desktop] Got proxies update from %v", src) + atomic.StoreInt32(&app.fetchedProxiesConfig, 1) +} + +func (app *App) onSucceedingProxy(succeeding bool) { + hasSucceedingProxy := int32(0) + if succeeding { + hasSucceedingProxy = 1 } + atomic.StoreInt32(&app.hasSucceedingProxy, hasSucceedingProxy) + log.Debugf("[Startup Desktop] onSucceedingProxy %v", succeeding) +} + +// HasSucceedingProxy returns whether or not the app is currently configured with any succeeding proxies +func (app *App) HasSucceedingProxy() bool { + return atomic.LoadInt32(&app.hasSucceedingProxy) == 1 +} + +func (app *App) GetHasConfigFetched() bool { + + log.Debugf("Global config fetched: %v, Proxies config fetched: %v") + return atomic.LoadInt32(&app.fetchedGlobalConfig) == 1 +} + +func (app *App) GetHasProxyFetched() bool { + return atomic.LoadInt32(&app.fetchedProxiesConfig) == 1 +} + +func (app *App) GetOnSuccess() bool { + return app.HasSucceedingProxy() } // AddExitFunc adds a function to be called before the application exits. diff --git a/desktop/lib.go b/desktop/lib.go index c8872b358..4dbb05a10 100644 --- a/desktop/lib.go +++ b/desktop/lib.go @@ -90,7 +90,6 @@ func start() { }) a = app.NewApp(flags, cdir, proClient, settings) - go func() { err := fetchOrCreate() if err != nil { @@ -139,6 +138,30 @@ func start() { }() } +//export hasProxyFected +func hasProxyFected() *C.char { + if a.GetHasProxyFetched() { + return C.CString(string("true")) + } + return C.CString(string("false")) +} + +//export hasConfigFected +func hasConfigFected() *C.char { + if a.GetHasConfigFetched() { + return C.CString(string("true")) + } + return C.CString(string("false")) +} + +//export onSuccess +func onSuccess() *C.char { + if a.GetOnSuccess() { + return C.CString(string("true")) + } + return C.CString(string("false")) +} + func fetchOrCreate() error { settings := a.Settings() userID := settings.GetUserID() @@ -485,7 +508,11 @@ func vpnStatus() *C.char { //export hasSucceedingProxy func hasSucceedingProxy() *C.char { - return C.CString("true") + hasSucceedingProxy := a.HasSucceedingProxy() + if hasSucceedingProxy { + return C.CString("true") + } + return C.CString("false") } //export onBoardingStatus diff --git a/go.mod b/go.mod index d3925eafa..9212bf722 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/getlantern/lantern-client go 1.22.0 -// replace github.com/getlantern/flashlight/v7 => ../flashlight +//replace github.com/getlantern/flashlight/v7 => ../flashlight // replace github.com/getlantern/fronted => ../fronted @@ -42,7 +42,7 @@ require ( github.com/getlantern/eventual v1.0.0 github.com/getlantern/eventual/v2 v2.0.2 github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c - github.com/getlantern/flashlight/v7 v7.6.80 + github.com/getlantern/flashlight/v7 v7.6.83 github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 github.com/getlantern/i18n v0.0.0-20181205222232-2afc4f49bb1c @@ -69,6 +69,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/jackpal/gateway v1.0.13 + github.com/joho/godotenv v1.5.1 github.com/leekchan/accounting v1.0.0 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.9.0 @@ -219,7 +220,6 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/jaffee/commandeer v0.6.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 // indirect github.com/kennygrant/sanitize v1.2.4 // indirect github.com/klauspost/compress v1.17.6 // indirect diff --git a/go.sum b/go.sum index 79b80e577..809e47c16 100644 --- a/go.sum +++ b/go.sum @@ -280,10 +280,16 @@ github.com/getlantern/fdcount v0.0.0-20210503151800-5decd65b3731/go.mod h1:XZwE+ github.com/getlantern/filepersist v0.0.0-20160317154340-c5f0cd24e799/go.mod h1:8DGAx0LNUfXNnEH+fXI0s3OCBA/351kZCiz/8YSK3i8= github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c h1:mcz27xtAkb1OuOLBct/uFfL1p3XxAIcFct82GbT+UZM= github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c/go.mod h1:8DGAx0LNUfXNnEH+fXI0s3OCBA/351kZCiz/8YSK3i8= -github.com/getlantern/flashlight/v7 v7.6.79 h1:GAvX9yuCJ3S8qB680gFYM6D/zwmAEk9FcC+LE8hgEGQ= -github.com/getlantern/flashlight/v7 v7.6.79/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= -github.com/getlantern/flashlight/v7 v7.6.80 h1:RZBLtFMUDy1CpORFaz9xMTE04ZBTf3yHhTCP1XbLkyQ= -github.com/getlantern/flashlight/v7 v7.6.80/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= +github.com/getlantern/flashlight/v7 v7.6.80-0.20240530173801-eaa86a1afe9f h1:Bp5g6x9RHyXSv/N2K0AgLIWY5/ym5qsITsjFWcH6SPY= +github.com/getlantern/flashlight/v7 v7.6.80-0.20240530173801-eaa86a1afe9f/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= +github.com/getlantern/flashlight/v7 v7.6.82-0.20240606214531-37c147431d2b h1:17x0t1q6LzA+qYG2EH51r+f8oALh+0kQCfqpEveu9LY= +github.com/getlantern/flashlight/v7 v7.6.82-0.20240606214531-37c147431d2b/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= +github.com/getlantern/flashlight/v7 v7.6.82-0.20240606214720-ea08484f5c06 h1:6QUqgrCiYvKSyS9CMag5C7A+3odKaXZ4D1obMPg9jnE= +github.com/getlantern/flashlight/v7 v7.6.82-0.20240606214720-ea08484f5c06/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= +github.com/getlantern/flashlight/v7 v7.6.82 h1:D+sUCKvg/iA77j/Y1yQ9NqSAUWNe8GQ4Dm2LefanRII= +github.com/getlantern/flashlight/v7 v7.6.82/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= +github.com/getlantern/flashlight/v7 v7.6.83 h1:623tGo7fhdMOD3O2b0PTQnREdEtfC+GiDjkxIhDEiks= +github.com/getlantern/flashlight/v7 v7.6.83/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= github.com/getlantern/framed v0.0.0-20190601192238-ceb6431eeede h1:yrU6Px3ZkvCsDLPryPGi6FN+2iqFPq+JeCb7EFoDBhw= github.com/getlantern/framed v0.0.0-20190601192238-ceb6431eeede/go.mod h1:nhnoiS6DE6zfe+BaCMU4YI01UpsuiXnDqM5S8jxHuuI= github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 h1:r/Z/SPPIfLXDI3QA7/tE6nOfPncrqeUPDjiFjnNGP50= @@ -987,8 +993,6 @@ go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0 go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k= -go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY= go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= diff --git a/internalsdk/android.go b/internalsdk/android.go index 8505aeb25..26e8dc4f9 100644 --- a/internalsdk/android.go +++ b/internalsdk/android.go @@ -18,6 +18,7 @@ import ( "github.com/getlantern/errors" "github.com/getlantern/eventual/v2" "github.com/getlantern/flashlight/v7" + "github.com/getlantern/flashlight/v7/bandit" "github.com/getlantern/flashlight/v7/bandwidth" "github.com/getlantern/flashlight/v7/client" "github.com/getlantern/flashlight/v7/config" @@ -89,12 +90,22 @@ type Session interface { SetChatEnabled(bool) SplitTunnelingEnabled() (bool, error) SetShowInterstitialAdsEnabled(bool) + SetHasConfigFetched(bool) + SetHasProxyFetched(bool) + SetOnSuccess(bool) // workaround for lack of any sequence types in gomobile bind... ;_; // used to implement GetInternalHeaders() map[string]string // Should return a JSON encoded map[string]string {"key":"val","key2":"val", ...} SerializedInternalHeaders() (string, error) } +// Callback that updates ui +type InitCallback struct { + hasConfigFected bool + hasProxyFected bool + onSuccess bool +} + // PanickingSession wraps the Session interface but panics instead of returning errors type PanickingSession interface { common.AuthConfig @@ -123,6 +134,9 @@ type PanickingSession interface { // used to implement GetInternalHeaders() map[string]string // Should return a JSON encoded map[string]string {"key":"val","key2":"val", ...} SerializedInternalHeaders() string + SetHasConfigFetched(bool) + SetHasProxyFetched(bool) + SetOnSuccess(bool) Wrapped() Session } @@ -278,6 +292,18 @@ func (s *panickingSessionImpl) SerializedInternalHeaders() string { return result } +func (s *panickingSessionImpl) SetHasConfigFetched(fetached bool) { + s.wrapped.SetHasConfigFetched(fetached) +} + +func (s *panickingSessionImpl) SetHasProxyFetched(fetached bool) { + s.wrapped.SetHasProxyFetched(fetached) +} + +func (s *panickingSessionImpl) SetOnSuccess(fetached bool) { + s.wrapped.SetOnSuccess(fetached) +} + type UserConfig struct { session PanickingSession } @@ -542,29 +568,28 @@ func run(configDir, locale string, settings Settings, session PanickingSession) func() bool { return false }, // always connected func() bool { return true }, func() bool { return false }, // do not proxy private hosts on Android - // TODO: allow configuring whether or not to enable reporting (just like we - // already have in desktop) - func() bool { return true }, // auto report + func() bool { return true }, // auto report flags, - func(cfg *config.Global, src config.Source) { - session.UpdateAdSettings(&adSettings{cfg.AdSettings}) - if session.IsStoreVersion() { - runner.EnableNamedDomainRules("google_play") // for google play build we want to make sure that Google Play domains are not being proxied - } - select { - case globalConfigChanged <- nil: - // okay - default: - // don't block - } - }, // onConfigUpdate - nil, // onProxiesUpdate userConfig, NewStatsTracker(session), session.IsProUser, func() string { return "" }, // only used for desktop ReverseDns(grabber), func(category, action, label string) {}, + flashlight.WithOnConfig(func(g *config.Global, s config.Source) { + session.SetHasConfigFetched(true) + }), + flashlight.WithOnProxies(func(d []bandit.Dialer, s config.Source) { + session.SetHasProxyFetched(true) + }), + flashlight.WithOnDialError(func(err error, hasSucceeding bool) { + if err != nil && !hasSucceeding { + session.SetOnSuccess(false) + } + }), + flashlight.WithOnSucceedingProxy(func() { + session.SetOnSuccess(true) + }), ) if err != nil { log.Fatalf("failed to start flashlight: %v", err) diff --git a/internalsdk/android_test.go b/internalsdk/android_test.go index 8dd92db26..d8f6f378e 100644 --- a/internalsdk/android_test.go +++ b/internalsdk/android_test.go @@ -73,6 +73,9 @@ func (c testSession) SetChatEnabled(enabled bool) {} func (c testSession) SetMatomoEnabled(bool) {} func (c testSession) IsPlayVersion() (bool, error) { return false, nil } func (c testSession) SetShowInterstitialAdsEnabled(enabled bool) {} +func (c testSession) SetHasConfigFetched(enabled bool) {} +func (c testSession) SetHasProxyFetched(enabled bool) {} +func (c testSession) SetOnSuccess(enabled bool) {} func (c testSession) SerializedInternalHeaders() (string, error) { return c.serializedInternalHeaders, nil diff --git a/internalsdk/ios/ios.go b/internalsdk/ios/ios.go index 925a8b7fe..6d4f23879 100644 --- a/internalsdk/ios/ios.go +++ b/internalsdk/ios/ios.go @@ -131,7 +131,9 @@ func (c *cw) Reconfigure() { panic(log.Errorf("Unable to load dialers on reconfigure: %v", err)) } - c.dialer, err = bandit.New(dialers) + c.dialer, err = bandit.New(bandit.Options{ + Dialers: dialers, + }) if err != nil { log.Errorf("Unable to create dialer on reconfigure: %v", err) } @@ -205,7 +207,10 @@ func (c *iosClient) start() (ClientWriter, error) { return nil, errors.New("No dialers found") } tracker := stats.NewTracker() - dialer, err := bandit.NewWithStats(dialers, tracker) + dialer, err := bandit.New(bandit.Options{ + Dialers: dialers, + StatsTracker: tracker, + }) if err != nil { return nil, err } diff --git a/internalsdk/session_model.go b/internalsdk/session_model.go index 822fccf39..c1c2e3e9a 100644 --- a/internalsdk/session_model.go +++ b/internalsdk/session_model.go @@ -63,6 +63,9 @@ const ( pathHasAllNetworkPermssion = "/hasAllNetworkPermssion" pathShouldShowGoogleAds = "shouldShowGoogleAds" currentTermsVersion = 1 + pathHasConfig = "hasConfigFetched" + pathHasProxy = "hasProxyFetched" + pathHasonSuccess = "hasOnSuccess" ) type SessionModelOpts struct { @@ -559,6 +562,23 @@ func (m *SessionModel) SerializedInternalHeaders() (string, error) { return "", nil } +func (m *SessionModel) SetHasConfigFetched(fetached bool) { + panicIfNecessary(pathdb.Mutate(m.db, func(tx pathdb.TX) error { + return pathdb.Put(tx, pathHasConfig, fetached, "") + })) +} + +func (m *SessionModel) SetHasProxyFetched(fetached bool) { + panicIfNecessary(pathdb.Mutate(m.db, func(tx pathdb.TX) error { + return pathdb.Put(tx, pathHasProxy, fetached, "") + })) +} +func (m *SessionModel) SetOnSuccess(fetached bool) { + panicIfNecessary(pathdb.Mutate(m.db, func(tx pathdb.TX) error { + return pathdb.Put(tx, pathHasonSuccess, fetached, "") + })) +} + func acceptTerms(m *baseModel) error { return pathdb.Mutate(m.db, func(tx pathdb.TX) error { return pathdb.Put(tx, pathAcceptedTermsVersion, currentTermsVersion, "") diff --git a/lib/account/developer_settings.dart b/lib/account/developer_settings.dart index 8157b91db..ade691059 100644 --- a/lib/account/developer_settings.dart +++ b/lib/account/developer_settings.dart @@ -1,3 +1,4 @@ +import 'package:flutter_advanced_switch/flutter_advanced_switch.dart'; import 'package:lantern/messaging/messaging.dart'; import 'package:lantern/replica/common.dart'; @@ -29,17 +30,14 @@ class DeveloperSettingsTab extends StatelessWidget { trailingArray: [ sessionModel.paymentTestMode( (BuildContext context, bool value, Widget? child) { - return FlutterSwitch( + return AdvancedSwitch( key: AppKeys.payment_mode_switch, width: 44.0, height: 24.0, - valueFontSize: 12.0, - padding: 2, - toggleSize: 18.0, - value: value, - onToggle: (bool newValue) { + onChanged: ( newValue) { sessionModel.setPaymentTestMode(newValue); }, + initialValue: value, ); }) ], @@ -49,14 +47,14 @@ class DeveloperSettingsTab extends StatelessWidget { trailingArray: [ sessionModel.playVersion( (BuildContext context, bool value, Widget? child) { - return FlutterSwitch( + return AdvancedSwitch( width: 44.0, height: 24.0, - valueFontSize: 12.0, - padding: 2, - toggleSize: 18.0, - value: value, - onToggle: (bool newValue) { + // valueFontSize: 12.0, + // padding: 2, + // toggleSize: 18.0, + initialValue: value, + onChanged: ( newValue) { sessionModel.setPlayVersion(newValue); }, ); diff --git a/lib/account/settings.dart b/lib/account/settings.dart index 1cdead378..95ef4b6cb 100644 --- a/lib/account/settings.dart +++ b/lib/account/settings.dart @@ -1,3 +1,5 @@ +import 'package:catcher_2/core/catcher_2.dart'; +import 'package:flutter_advanced_switch/flutter_advanced_switch.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:intl/intl.dart'; import 'package:lantern/common/app_methods.dart'; @@ -182,16 +184,13 @@ class Settings extends StatelessWidget { ), ), trailingArray: [ - FlutterSwitch( + AdvancedSwitch( width: 44.0, height: 24.0, - valueFontSize: 12.0, - padding: 2, - toggleSize: 18.0, - value: proxyAll, + initialValue: proxyAll, activeColor: indicatorGreen, inactiveColor: offSwitchColor, - onToggle: (bool newValue) { + onChanged: ( newValue) { sessionModel.setProxyAll(newValue); }, ), diff --git a/lib/app.dart b/lib/app.dart index 89b91cd77..4caf81384 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,9 +1,13 @@ import 'package:animated_loading_border/animated_loading_border.dart'; import 'package:app_links/app_links.dart'; import 'package:flutter/scheduler.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:lantern/core/router/router.dart'; import 'package:lantern/custom_bottom_bar.dart'; import 'package:lantern/messaging/messaging.dart'; +import 'package:lantern/vpn/vpn_notifier.dart'; + +import 'common/ui/custom/internet_checker.dart'; final navigatorKey = GlobalKey(); final globalRouter = AppRouter(); @@ -56,8 +60,6 @@ class _LanternAppState extends State { void _animateNetworkWarning() { if (isMobile()) { - sessionModel.networkAvailable - .addListener(toggleConnectivityWarningIfNecessary); sessionModel.proxyAvailable .addListener(toggleConnectivityWarningIfNecessary); networkWarningAnimationController = AnimationController( @@ -93,11 +95,17 @@ class _LanternAppState extends State { networkWarningBarHeightRatio.value = networkWarningAnimation.value; } - void toggleConnectivityWarningIfNecessary() { + Future toggleConnectivityWarningIfNecessary() async { + final hasConnection = await InternetConnection().hasInternetAccess; + //Check if the device has internet connection + //if not then proxy will not be available + //We already showing on internet connection error + if (!hasConnection) { + return; + } final shouldShowConnectivityWarning = - !sessionModel.networkAvailable.value || - (sessionModel.proxyAvailable.value != null && - sessionModel.proxyAvailable.value == false); + (sessionModel.proxyAvailable.value != null && + sessionModel.proxyAvailable.value == false); if (shouldShowConnectivityWarning != showConnectivityWarning) { showConnectivityWarning = shouldShowConnectivityWarning; if (showConnectivityWarning) { @@ -113,80 +121,85 @@ class _LanternAppState extends State { Widget build(BuildContext context) { final currentLocal = View.of(context).platformDispatcher.locale; print('selected local: ${currentLocal.languageCode}'); - return ChangeNotifierProvider( - create: (context) => BottomBarChangeNotifier(), - child: FutureBuilder( - future: translations, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return Container(); - } - return sessionModel.language( - (context, lang, child) { - Localization.locale = lang.startsWith('en')?'en_us':lang; - return GlobalLoaderOverlay( - useDefaultLoading: false, - overlayColor: Colors.black.withOpacity(0.5), - overlayWidget: Center( - child: AnimatedLoadingBorder( - borderWidth: 5, - borderColor: yellow3, - cornerRadius: 100, - child: SvgPicture.asset( - ImagePaths.lantern_logo, + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => BottomBarChangeNotifier()), + ChangeNotifierProvider(create: (context) => VPNChangeNotifier()), + ChangeNotifierProvider(create: (context) => InternetStatusProvider()) + ], + child: ChangeNotifierProvider( + create: (context) => BottomBarChangeNotifier(), + child: FutureBuilder( + future: translations, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (!snapshot.hasData) { + return Container(); + } + return sessionModel.language( + (context, lang, child) { + Localization.locale = lang; + return GlobalLoaderOverlay( + useDefaultLoading: false, + overlayColor: Colors.black.withOpacity(0.5), + overlayWidget: Center( + child: AnimatedLoadingBorder( + borderWidth: 5, + borderColor: yellow3, + cornerRadius: 100, + child: SvgPicture.asset( + ImagePaths.lantern_logo, + ), ), ), - ), - child: I18n( - initialLocale: currentLocale(lang), - child: MaterialApp.router( - locale: currentLocale(lang), - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: false, - fontFamily: _getLocaleBasedFont(currentLocal), - brightness: Brightness.light, - primarySwatch: Colors.grey, - appBarTheme: const AppBarTheme( - systemOverlayStyle: SystemUiOverlayStyle.dark, + child: I18n( + initialLocale: currentLocale(lang), + child: MaterialApp.router( + locale: currentLocale(lang), + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: false, + fontFamily: _getLocaleBasedFont(currentLocal), + brightness: Brightness.light, + primarySwatch: Colors.grey, + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle.dark, + ), + colorScheme: ColorScheme.fromSwatch() + .copyWith(secondary: Colors.black), ), - colorScheme: ColorScheme.fromSwatch() - .copyWith(secondary: Colors.black), - ), - title: 'app_name'.i18n, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - routerConfig: globalRouter.config( - deepLinkBuilder: navigateToDeepLink, + title: 'app_name'.i18n, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + routerConfig: globalRouter.config( + deepLinkBuilder: navigateToDeepLink, + ), + supportedLocales: const [ + Locale('ar', 'EG'), + Locale('fr', 'FR'), + Locale('en', 'US'), + Locale('fa', 'IR'), + Locale('th', 'TH'), + Locale('ms', 'MY'), + Locale('ru', 'RU'), + Locale('ur', 'IN'), + Locale('zh', 'CN'), + Locale('zh', 'HK'), + Locale('es', 'ES'), + Locale('es', 'CU'), + Locale('tr', 'TR'), + Locale('vi', 'VN'), + Locale('my', 'MM'), + ], ), - supportedLocales: const [ - Locale('ar', 'EG'), - Locale('fr', 'FR'), - Locale('en', 'US'), - Locale('fa', 'IR'), - Locale('th', 'TH'), - Locale('ms', 'MY'), - Locale('ru', 'RU'), - Locale('ur', 'IN'), - Locale('zh', 'CN'), - Locale('zh', 'HK'), - Locale('es', 'ES'), - Locale('es', 'CU'), - Locale('tr', 'TR'), - Locale('vi', 'VN'), - Locale('my', 'MM'), - Locale('hi', 'IN'), - Locale('bn', 'BD'), - ], ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ), ); } diff --git a/lib/common/common.dart b/lib/common/common.dart index 71883d4d7..f1864c086 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -18,7 +18,9 @@ export 'package:flutter/services.dart'; export 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; export 'package:flutter_localizations/flutter_localizations.dart'; export 'package:flutter_svg/flutter_svg.dart'; -export 'package:flutter_switch/flutter_switch.dart'; +export 'package:flutter_advanced_switch/flutter_advanced_switch.dart'; +// export 'package:i18n_extension/i18n_widget.dart'; +// export 'package:flutter_switch/flutter_switch.dart'; export 'package:i18n_extension/src/i18n_widget.dart'; export 'package:lantern/core/router/router.gr.dart'; export 'package:lantern/event_extension.dart'; diff --git a/lib/common/session_model.dart b/lib/common/session_model.dart index fec3ab30d..e873c4928 100644 --- a/lib/common/session_model.dart +++ b/lib/common/session_model.dart @@ -22,18 +22,6 @@ class SessionModel extends Model { SessionModel() : super('session') { if (isMobile()) { eventManager = EventManager('lantern_event_channel'); - eventManager.subscribe(Event.All, (eventType, map) { - switch (eventType) { - case Event.NoNetworkAvailable: - networkAvailable.value = false; - break; - case Event.NetworkAvailable: - networkAvailable.value = true; - break; - default: - break; - } - }); isStoreVersion = singleValueNotifier( 'storeVersion', @@ -75,12 +63,18 @@ class SessionModel extends Model { } } - ValueNotifier networkAvailable = ValueNotifier(true); + // ValueNotifier networkAvailable = ValueNotifier(true); late ValueNotifier isPlayVersion; late ValueNotifier isStoreVersion; late ValueNotifier proxyAvailable; late ValueNotifier country; + + + ValueNotifier pathValueNotifier(String path, T defaultValue){ + return singleValueNotifier(path, defaultValue); + } + // listenWebsocket listens for websocket messages from the server. If a message matches the given message type, // the onMessage callback is triggered with the given property value void listenWebsocket(WebsocketImpl? websocket, String messageType, diff --git a/lib/common/ui/colors.dart b/lib/common/ui/colors.dart index 28ce6c40a..c582f7bc1 100644 --- a/lib/common/ui/colors.dart +++ b/lib/common/ui/colors.dart @@ -11,6 +11,7 @@ Color blue5 = HexColor('#006163'); Color yellow3 = HexColor('#FFE600'); Color yellow4 = HexColor('#FFC107'); +Color yellow5 = HexColor('#D6A000'); Color yellow6 = HexColor('#957000'); Color pink1 = HexColor('#FFF4F8'); diff --git a/lib/common/ui/custom/dialog.dart b/lib/common/ui/custom/dialog.dart index c08af07bf..164d8ddbe 100644 --- a/lib/common/ui/custom/dialog.dart +++ b/lib/common/ui/custom/dialog.dart @@ -47,6 +47,110 @@ class CDialog extends StatefulWidget { ).show(context); } + static void showInternetUnavailableDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 20,vertical: 16), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Padding( + padding: EdgeInsetsDirectional.only(bottom: 16), + child: CAssetImage( + path: ImagePaths.cloudOff, color: Colors.black), + ), + ), + Center(child: CText('check_your_internet_connection'.i18n, style: tsSubtitle1)), + const SizedBox(height: 10), + CText('please_try'.i18n, style: tsSubtitle2), + RichText( + text: TextSpan( + children: [ + TextSpan(text: '1.', style: tsSubtitle2), + const WidgetSpan( + child: SizedBox( + width: 5, + )), + TextSpan( + text: 'turning_off_airplane_mode'.i18n, style: tsBody1) + ], + )), + RichText( + text: TextSpan( + children: [ + TextSpan(text: '2.', style: tsSubtitle2), + const WidgetSpan( + child: SizedBox( + width: 5, + )), + TextSpan( + text: 'turning_on_mobile_data_or_wifi'.i18n, + style: tsBody1) + ], + )), + RichText( + text: TextSpan( + children: [ + TextSpan(text: '3.', style: tsSubtitle2), + const WidgetSpan( + child: SizedBox( + width: 5, + )), + TextSpan( + text: 'check_the_signal_in_your_area'.i18n, + style: tsBody1) + ], + )), + const SizedBox( + height: 25, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: CText('got_it'.i18n.toUpperCase(), style: tsButtonPink), + ), + ], + ), + ], + ), + ); + }, + ); + + // CDialog( + // icon: const CAssetImage(path: ImagePaths.cloudOff, color: Colors.grey), + // title: 'Check your internet connection'.i18n, + // agreeText: 'Got it', + // // crossAxisAlignment: CrossAxisAlignment.start, + // description: Column( + // // mainAxisSize: MainAxisSize.max, + // // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Container( + // color: Colors.amber, + // child: CText('Please try', + // style: tsBody1, textAlign: TextAlign.start)), + // RichText( + // text: TextSpan(children: [ + // TextSpan(text: '1. ', style: tsBody1), + // ])) + // ], + // ), + // ).show(context); + } + CDialog({ this.iconPath, this.icon, diff --git a/lib/common/ui/custom/internet_checker.dart b/lib/common/ui/custom/internet_checker.dart new file mode 100644 index 000000000..dbc37afe9 --- /dev/null +++ b/lib/common/ui/custom/internet_checker.dart @@ -0,0 +1,105 @@ +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; + +import '../../common.dart'; + +class InternetChecker extends StatelessWidget { + const InternetChecker({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + CDialog.showInternetUnavailableDialog(context); + }, + child: Container( + padding: const EdgeInsets.only(top: 5,bottom: 8), + alignment: Alignment.center, + decoration: ShapeDecoration( + color: const Color(0xFFFFF9DB), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: yellow4), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(ImagePaths.cloudOff,height: 25), + const SizedBox(width: 10), + CText( + "No internet connection detected", + textAlign: TextAlign.center, + style: tsBody1.copiedWith(color: yellow5), + ) + ], + ), + ), + ); + } +} + +class InternetStatusProvider extends ChangeNotifier { + bool _isConnected = true; + late StreamSubscription _connectionSubscription; + bool _isDisconnected = false; + + /// Using debounce to avoid flickering when the connection is unstable + final _debounceDuration = const Duration(seconds: 2); + Timer? _debounceTimer; + + InternetStatusProvider() { + // Listen for connection status changes + _connectionSubscription = + InternetConnection().onStatusChange.listen((status) { + if (status == InternetStatus.connected) { + _handleConnected(); + } else { + _handleDisconnected(); + } + }); + } + + bool get isConnected => _isConnected; + + Future checkInternetConnection() async { + // Check the internet connection status + _isConnected = await InternetConnection().hasInternetAccess; + + // Notify listeners of the change + notifyListeners(); + } + + void _handleConnected() { + _cancelDebounceTimer(); + _isDisconnected = false; + _isConnected = true; + notifyListeners(); + } + + void _handleDisconnected() { + _isDisconnected = true; + _startDebounceTimer(); + } + + void _startDebounceTimer() { + _cancelDebounceTimer(); + _debounceTimer = Timer(_debounceDuration, () { + if (_isDisconnected) { + _isConnected = false; + notifyListeners(); + } + }); + } + + void _cancelDebounceTimer() { + _debounceTimer?.cancel(); + } + + @override + void dispose() { + _connectionSubscription.cancel(); + _cancelDebounceTimer(); + super.dispose(); + } +} diff --git a/lib/common/ui/image_paths.dart b/lib/common/ui/image_paths.dart index e040e2327..1d3724f7d 100644 --- a/lib/common/ui/image_paths.dart +++ b/lib/common/ui/image_paths.dart @@ -148,6 +148,7 @@ class ImagePaths { static const empty_search = 'assets/images/empty_search.svg'; static const lantern_logotype = 'assets/images/lantern_logotype.svg'; static const lantern_pro_logotype = 'assets/images/lantern_pro_logotype.svg'; + static const cloudOff = 'assets/images/cloud_off.svg'; static String countdownPath(int index) => 'assets/images/countdown_stopwatch/timer_$index.svg'; diff --git a/lib/ffi.dart b/lib/ffi.dart index db351743a..cf8e35b79 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -9,6 +9,10 @@ extension StringEx on String { Pointer toPointerChar() { return this.toNativeUtf8().cast(); } + + bool toBool() { + return this == 'true'; + } } void sysProxyOn() => _bindings.sysProxyOn(); @@ -50,6 +54,14 @@ Future ffiUserData() async { return User.create()..mergeFromProto3Json(jsonDecode(res)); } +(bool, bool, bool) startUpInitCallBacks() { + final proxy = _bindings.hasProxyFected().cast().toDartString(); + final config = _bindings.hasConfigFected().cast().toDartString(); + final success = _bindings.onSuccess().cast().toDartString(); + print("startup status proxy $proxy config $config success $success"); + return (proxy.toBool(), config.toBool(), success.toBool()); +} + // checkAPIError throws a PlatformException if the API response contains an error void checkAPIError(result, errorMessage) { if (result is String) { diff --git a/lib/home.dart b/lib/home.dart index b2956bcb7..a5fe753b3 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -1,3 +1,4 @@ +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:lantern/account/account_tab.dart'; import 'package:lantern/account/developer_settings.dart'; import 'package:lantern/account/privacy_disclosure.dart'; @@ -26,7 +27,6 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State with TrayListener, WindowListener { - Function()? _cancelEventSubscription; @override @@ -93,6 +93,8 @@ class _HomePageState extends State with TrayListener, WindowListener { }); } + + void _initWindowManager() async { windowManager.addListener(this); // Add this line to override the default close handler @@ -221,7 +223,7 @@ class _HomePageState extends State with TrayListener, WindowListener { ? Chats() : Welcome(); case TAB_VPN: - return VPNTab(); + return const VPNTab(); case TAB_REPLICA: return ReplicaTab(); case TAB_ACCOUNT: diff --git a/lib/vpn/vpn_notifier.dart b/lib/vpn/vpn_notifier.dart new file mode 100644 index 000000000..f3008b11f --- /dev/null +++ b/lib/vpn/vpn_notifier.dart @@ -0,0 +1,110 @@ +import '../common/common.dart'; +import '../ffi.dart'; + +class VPNChangeNotifier extends ChangeNotifier { + Timer? timer; + bool isFlashlightInitialized = false; + bool isFlashlightInitializedFailed = false; + String flashlightState = 'fetching_configuration'.i18n; + + VPNChangeNotifier() { + if (isMobile()) { + initCallbackForMobile(); + } else { + initCallbacks(); + } + } + + void initCallbacks() { + if (timer != null) { + return; + } + timer = Timer.periodic(const Duration(seconds: 1), (_) { + final result = startUpInitCallBacks(); + if (!result.$1 || !result.$2) { + flashlightState = 'fetching_configuration'.i18n; + } + if (result.$1 && result.$2 && !result.$3) { + flashlightState = 'establish_connection_to_server'.i18n; + } + notifyListeners(); + if (result.$1 && result.$2 && result.$3) { + // everything is initialized + isFlashlightInitialized = true; + isFlashlightInitializedFailed = false; + print("flashlight initialized"); + notifyListeners(); + timer?.cancel(); + } else if (timer!.tick >= 6) { + // Timer has reached 6 seconds + // Stop the timer and set isFlashlightInitialized to true + print("flashlight fail initialized"); + isFlashlightInitialized = true; + isFlashlightInitializedFailed = true; + notifyListeners(); + } + }); + } + + void initCallbackForMobile() { + if (timer != null) { + return; + } + timer = Timer.periodic(const Duration(seconds: 1), (_) { + if (timer!.tick >= 6) { + // Timer has reached 6 seconds + // Stop the timer and set isFlashlightInitialized to true + print("flashlight fail initialized"); + isFlashlightInitialized = true; + isFlashlightInitializedFailed = true; + notifyListeners(); + } + }); + final configNotifier = sessionModel.pathValueNotifier('hasConfigFetched', false); + final proxyNotifier = sessionModel.pathValueNotifier('hasProxyFetched', false); + final successNotifier = sessionModel.pathValueNotifier('hasOnSuccess', false); + + updateStatus(bool proxy, bool config, bool success) { + if (proxy || config) { + flashlightState = 'fetching_configuration'.i18n; + } + if (proxy && config && !success) { + flashlightState = 'establish_connection_to_server'.i18n; + } + notifyListeners(); + + if (proxy && proxy && success) { + // everything is initialized + isFlashlightInitialized = true; + isFlashlightInitializedFailed = false; + timer?.cancel(); + print("flashlight initialized"); + notifyListeners(); + } + } + + configNotifier.addListener(() { + updateStatus( + proxyNotifier.value!, configNotifier.value!, successNotifier.value!); + }); + proxyNotifier.addListener(() { + updateStatus( + proxyNotifier.value!, configNotifier.value!, successNotifier.value!); + }); + successNotifier.addListener(() { + print("successNotifier Notfier ${successNotifier.value}"); + updateStatus( + proxyNotifier.value!, configNotifier.value!, successNotifier.value!); + }); + } + + @override + void dispose() { + if (timer?.isActive ?? false) { + timer?.cancel(); + } + isFlashlightInitialized = false; + isFlashlightInitializedFailed = false; + super.dispose(); + } +} diff --git a/lib/vpn/vpn_switch.dart b/lib/vpn/vpn_switch.dart index 88728e3b9..8fea10c59 100644 --- a/lib/vpn/vpn_switch.dart +++ b/lib/vpn/vpn_switch.dart @@ -1,7 +1,11 @@ +import 'package:flutter_advanced_switch/flutter_advanced_switch.dart'; import 'package:lantern/ad_helper.dart'; import 'package:lantern/common/common.dart'; import 'package:lantern/common/common_desktop.dart'; import 'package:lantern/vpn/vpn.dart'; +import 'package:lantern/vpn/vpn_notifier.dart'; + +import '../common/ui/custom/internet_checker.dart'; class VPNSwitch extends StatefulWidget { const VPNSwitch({super.key}); @@ -10,6 +14,8 @@ class VPNSwitch extends StatefulWidget { State createState() => _VPNSwitchState(); } +//implement this switch with loading implementation +//https://pub.dev/packages/animated_toggle_switch class _VPNSwitchState extends State { final adHelper = AdHelper(); String vpnStatus = 'disconnected'; @@ -56,19 +62,30 @@ class _VPNSwitchState extends State { @override Widget build(BuildContext context) { + + final internetStatusProvider = context.watch(); + final vpnNotifier = context.watch(); if (isMobile()) { return sessionModel .shouldShowGoogleAds((context, isGoogleAdsEnable, child) { adHelper.loadAds(shouldShowGoogleAds: isGoogleAdsEnable); return Transform.scale( - scale: 2, + scale: 2.5, child: vpnModel.vpnStatus( (BuildContext context, String vpnStatus, Widget? child) { - return FlutterSwitch( - value: vpnStatus == 'connected' || vpnStatus == 'disconnecting', + return AdvancedSwitch( + width: 60, + disabledOpacity: 1, + enabled: (internetStatusProvider.isConnected && + !vpnNotifier.isFlashlightInitializedFailed), + initialValue: + vpnStatus == 'connected' || vpnStatus == 'disconnecting', activeColor: onSwitchColor, - inactiveColor: offSwitchColor, - onToggle: (bool newValue) => + inactiveColor: (internetStatusProvider.isConnected && + !vpnNotifier.isFlashlightInitializedFailed) + ?offSwitchColor + : grey3, + onChanged: (newValue) => vpnProcessForMobile(newValue, vpnStatus, isGoogleAdsEnable), ); })); @@ -76,17 +93,24 @@ class _VPNSwitchState extends State { } else { // This ui for desktop return Transform.scale( - scale: 2, + scale: 2.5, child: vpnModel .vpnStatus((BuildContext context, String vpnStatus, Widget? child) { this.vpnStatus = vpnStatus; - return FlutterSwitch( - value: this.vpnStatus == 'connected' || + return AdvancedSwitch( + width: 60, + disabledOpacity: 1, + enabled: (internetStatusProvider.isConnected && + !vpnNotifier.isFlashlightInitializedFailed), + initialValue: this.vpnStatus == 'connected' || this.vpnStatus == 'disconnecting', - //value: true, activeColor: onSwitchColor, - inactiveColor: offSwitchColor, - onToggle: (bool newValue) { + inactiveColor: (internetStatusProvider.isConnected && + !vpnNotifier.isFlashlightInitializedFailed) + ?offSwitchColor + : grey3, + + onChanged: (newValue) { vpnProcessForDesktop(); setState(() { this.vpnStatus = newValue ? 'connected' : 'disconnected'; diff --git a/lib/vpn/vpn_tab.dart b/lib/vpn/vpn_tab.dart index 435222e93..90fd79551 100644 --- a/lib/vpn/vpn_tab.dart +++ b/lib/vpn/vpn_tab.dart @@ -1,6 +1,10 @@ import 'package:lantern/account/split_tunneling.dart'; import 'package:lantern/messaging/messaging.dart'; import 'package:lantern/vpn/vpn.dart'; +import 'package:lantern/vpn/vpn_notifier.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../common/ui/custom/internet_checker.dart'; import 'vpn_bandwidth.dart'; import 'vpn_pro_banner.dart'; @@ -13,6 +17,7 @@ class VPNTab extends StatelessWidget { @override Widget build(BuildContext context) { + final vpnNotifier = context.watch(); return sessionModel .proUser((BuildContext context, bool proUser, Widget? child) { return BaseScreen( @@ -24,39 +29,132 @@ class VPNTab extends StatelessWidget { // make sure to disable the back arrow button on the home screen automaticallyImplyLeading: false, padVertical: true, - body: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (!proUser && !Platform.isIOS) const ProBanner() else const SizedBox(), - const VPNSwitch(), - Container( - padding: const EdgeInsetsDirectional.all(16), - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: 1, - ), - borderRadius: const BorderRadius.all( - Radius.circular(borderRadius), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, + body: !vpnNotifier.isFlashlightInitialized + ? VPNTapSkeleton( + isProUser: proUser, + ) + : Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ - VPNStatus(), - const CDivider(height: 32.0), - const ServerLocationWidget(), - if (Platform.isAndroid) ...{ - const CDivider(height: 32.0), - SplitTunnelingWidget(), - }, - if (!proUser&& Platform.isAndroid) const VPNBandwidth(), + if (!proUser && !Platform.isIOS) + const ProBanner() + else + const SizedBox(height: 50), + const SizedBox(height: 100), + const VPNSwitch(), + const SizedBox(height: 40), + if (vpnNotifier.isFlashlightInitializedFailed) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CText(vpnNotifier.flashlightState, style: tsSubtitle2), + const SizedBox(width: 10), + SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: grey5, + ), + ) + ], + ), + const SizedBox(height: 50), + Consumer( + builder: (context, provider, _) { + return provider.isConnected + ? const SizedBox() + : const InternetChecker(); + }, + ), + const Spacer(), + Container( + padding: const EdgeInsetsDirectional.all(16), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: 1, + ), + borderRadius: const BorderRadius.all( + Radius.circular(borderRadius), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + VPNStatus(), + const CDivider(height: 32.0), + ServerLocationWidget(), + if (Platform.isAndroid) ...{ + const CDivider(height: 32.0), + SplitTunnelingWidget(), + if (!proUser) const VPNBandwidth(), + } + ], + ), + ), ], ), - ), - ], - ), ); }); } } + +class VPNTapSkeleton extends StatelessWidget { + final bool isProUser; + + const VPNTapSkeleton({ + super.key, + required this.isProUser, + }); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade100, + highlightColor: Colors.grey.shade200, + enabled: true, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!isProUser) const ProBanner() else const SizedBox(height: 50), + const VPNSwitch(), + Container( + padding: const EdgeInsetsDirectional.all(16), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: 1, + ), + borderRadius: const BorderRadius.all( + Radius.circular(borderRadius), + ), + ), + child: Column( + children: [ + buildRow(), + const SizedBox(height: 20), + buildRow(), + const SizedBox(height: 20), + buildRow(), + if (Platform.isAndroid) ...{ + const SizedBox(height: 20), + buildRow(), + } + ], + ), + ), + ], + ), + ); + } + + Widget buildRow() { + return Container( + height: 30, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6b8606107..4df9fb849 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import audioplayers_darwin +import connectivity_plus import device_info_plus import emoji_picker_flutter import flutter_image_compress_macos @@ -28,6 +29,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d81940b25..0cdeb9e5f 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,9 @@ PODS: - FlutterMacOS - audioplayers_darwin (0.0.1): - FlutterMacOS + - connectivity_plus (0.0.1): + - FlutterMacOS + - ReachabilitySwift - device_info_plus (0.0.1): - FlutterMacOS - emoji_picker_flutter (0.0.1): @@ -21,6 +24,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - ReachabilitySwift (5.2.2) - screen_retriever (0.0.1): - FlutterMacOS - Sentry/HybridSDK (8.21.0): @@ -53,6 +57,7 @@ PODS: DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - emoji_picker_flutter (from `Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos`) - flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`) @@ -75,6 +80,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - OrderedSet + - ReachabilitySwift - Sentry - SentryPrivate @@ -83,6 +89,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos audioplayers_darwin: :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos emoji_picker_flutter: @@ -123,6 +131,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f emoji_picker_flutter: 533634326b1c5de9a181ba14b9758e6dfe967a20 flutter_image_compress_macos: c26c3c13ea0f28ae6dea4e139b3292e7729f99f1 @@ -132,6 +141,7 @@ SPEC CHECKSUMS: OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + ReachabilitySwift: 2128f3a8c9107e1ad33574c6e58e8285d460b149 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 Sentry: ebc12276bd17613a114ab359074096b6b3725203 sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e @@ -147,4 +157,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa6a..e27702397 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -28,5 +28,7 @@ MainMenu NSPrincipalClass NSApplication + com.apple.security.network.client + diff --git a/pubspec.lock b/pubspec.lock index 3ee66727c..9a4224930 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -305,6 +305,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + connectivity_plus: + dependency: transitive + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" convert: dependency: transitive description: @@ -510,6 +526,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_advanced_switch: + dependency: "direct main" + description: + name: flutter_advanced_switch + sha256: e1147161a3dd9b708a71c65e76174d4d1a0a5908a571b8b38b65c79b142c52a0 + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_cache_manager: dependency: "direct main" description: @@ -776,14 +800,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.10+1" - flutter_switch: - dependency: "direct main" - description: - name: flutter_switch - sha256: b91477f926bba135d2d203d7b24367492662d8d9c3aa6adb960b14c1087d3c41 - url: "https://pub.dev" - source: hosted - version: "0.3.2" flutter_test: dependency: "direct dev" description: flutter @@ -949,6 +965,14 @@ packages: description: flutter source: sdk version: "0.0.0" + internet_connection_checker_plus: + dependency: "direct main" + description: + name: internet_connection_checker_plus + sha256: "7daf4458c62923f4c7f3b54b1b3191bc3d8813e69a45722ef0ff5fc3ef2ef686" + url: "https://pub.dev" + source: hosted + version: "2.2.0" intl: dependency: "direct main" description: @@ -1109,6 +1133,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" node_preamble: dependency: transitive description: @@ -1525,6 +1557,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" shortid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5efb72e67..327c4ff67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: sliver_tools: ^0.2.12 # UI Enhancements & User Input - flutter_switch: ^0.3.2 + flutter_advanced_switch: ^3.1.0 flag: ^7.0.0 #Loogs and crash reporting @@ -67,6 +67,7 @@ dependencies: # Networking cached_network_image: ^3.3.1 dio: ^5.3.2 + internet_connection_checker_plus: ^2.2.0 # caching flutter_cache_manager: ^3.3.1 @@ -134,6 +135,7 @@ dependencies: app_links: ^3.5.1 #Loading animated_loading_border: ^0.0.2 + shimmer: ^3.0.0 # wakelock ^0.6.2 requires win32 ^2.0.0 or ^3.0.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 9582b198e..8385f684b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); EmojiPickerFlutterPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FlutterWindowsWebviewPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ad1e63721..8e1f005b3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links audioplayers_windows + connectivity_plus emoji_picker_flutter flutter_windows_webview permission_handler_windows