diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1b93cd2be..b669771f22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: p12-file-base64: ${{ secrets.MACOS_BNS_CERT }} p12-password: ${{ secrets.MACOS_BNS_CERT_PASS }} - - name: Install the Apple certificate and provisioning profile + - name: Install the Apple certificate and provisioning profile IOS if: matrix.platform == 'ios' env: BUILD_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_P12_BASE64 }} @@ -146,7 +146,7 @@ jobs: echo -n "$BUILD_TUNNEL_PROVISION_PROFILE_BASE64" | base64 --decode -o $TPP_PATH cp $TPP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - # Create ExportOptions.plist + # Create ExportOptions.plist echo "$EXPORT_OPTIONS" | base64 --decode > "$EXPORT_OPTIONS_PATH" - name: Setup Sentry CLI on Android @@ -196,7 +196,6 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - - run: flutter --version - name: Setup JDK @@ -298,7 +297,11 @@ jobs: if: matrix.platform == 'android' && matrix.target == 'aab' run: make debug-symbols - - name: Build Flutter app + - name: Setup Sentry CLI on IOS + if: matrix.platform == 'ios' + uses: mathieu-bour/setup-sentry-cli@v2 + + - name: Build Flutter app (iOS, Linux, macOS) if: matrix.platform != 'windows' && matrix.platform != 'android' run: make ${{ matrix.platform }}-release env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9013cdeb4e..5d711299d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,23 +67,31 @@ jobs: vf = 'version-android-dev.txt' version = '9999.99.99-dev' else: - a = ref.strip().replace('refs/tags/lantern-', '') - parts = a.split('-',1) - suffix = parts[1] if len(parts)>1 else '' - beta = 'beta' in suffix - internal = 'internal' in suffix - if beta: - li = 'lantern-installer-preview' - vf = 'version-android-beta.txt' - version = parts[0] - elif internal: - li = 'lantern-installer-internal' - vf = 'version-android-internal.txt' - version = parts[0] + tag = ref.strip() + if tag.startswith('refs/tags/android-lantern-'): + a = tag.replace('refs/tags/android-lantern-', '') + elif tag.startswith('refs/tags/ios-lantern-'): + a = tag.replace('refs/tags/ios-lantern-', '') + elif tag.startswith('refs/tags/desktop-lantern-'): + a = tag.replace('refs/tags/desktop-lantern-', '') else: - li = 'lantern-installer' - vf = 'version-android.txt' - version = a + a = tag.replace('refs/tags/lantern-', '') + parts = a.split('-',1) + suffix = parts[1] if len(parts)>1 else '' + beta = 'beta' in suffix + internal = 'internal' in suffix + if beta: + li = 'lantern-installer-preview' + vf = 'version-android-beta.txt' + version = parts[0] + elif internal: + li = 'lantern-installer-internal' + vf = 'version-android-internal.txt' + version = parts[0] + else: + li = 'lantern-installer' + vf = 'version-android.txt' + version = a print('Setting version to ' + version) print('Setting prefix to ' + li) print('Setting version file to ' + vf) diff --git a/Makefile b/Makefile index 031b444e95..c53efdf46b 100644 --- a/Makefile +++ b/Makefile @@ -436,7 +436,6 @@ $(MOBILE_BUNDLE): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) require- android-debug: $(MOBILE_DEBUG_APK) - debug-symbols: @echo "Generating debug symbols for Android..." cd android && ./gradlew zipDebugSymbols @@ -449,14 +448,12 @@ set-version: NEXT_BUILD=$$(($$CURRENT_BUILD + 1)); \ /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $$NEXT_BUILD" $(INFO_PLIST) -ios: macos build-framework ffigen +#ios: macos build-framework ffigen -ios-release: set-version guard-SENTRY_AUTH_TOKEN guard-SENTRY_ORG guard-SENTRY_PROJECT_IOS pubget - @echo "Flutter Clean" +ios-release:require-version set-version guard-SENTRY_AUTH_TOKEN guard-SENTRY_ORG guard-SENTRY_PROJECT_IOS flutter clean - @echo "Flutter pub get" flutter pub get - @echo "Creating the Flutter iOS build..." + @echo "Creating the Flutter iOS build.set-version guard-SENTRY_AUTH_TOKEN guard-SENTRY_ORG guard-SENTRY_PROJECT_IOS.." flutter build ipa --flavor prod --release --export-options-plist ./ExportOptions.plist @echo "Uploading debug symbols to Sentry..." export SENTRY_LOG_LEVEL=info @@ -697,18 +694,19 @@ sourcedump: require-version ios: assert-go-version install-gomobile @echo "Nuking $(INTERNALSDK_FRAMEWORK_DIR) and $(MINISQL_FRAMEWORK_DIR)" - rm -Rf $(INTERNALSDK_FRAMEWORK_DIR) $(MINISQL_FRAMEWORK_DIR) - @echo "generating Ios.xcFramework" - go env -w 'GOPRIVATE=github.com/getlantern/*' && \ + @rm -Rf $(INTERNALSDK_FRAMEWORK_DIR) $(MINISQL_FRAMEWORK_DIR) + @echo "Generating Ios.xcFramework" + @go env -w 'GOPRIVATE=github.com/getlantern/*' && \ gomobile init && \ gomobile bind -target=ios,iossimulator \ -tags='headless lantern ios netgo' \ -ldflags="$(LDFLAGS)" \ $(GOMOBILE_EXTRA_BUILD_FLAGS) \ github.com/getlantern/lantern-client/internalsdk github.com/getlantern/pathdb/testsupport github.com/getlantern/pathdb/minisql github.com/getlantern/lantern-client/internalsdk/ios - @echo "moving framework" - mkdir -p $(INTERNALSDK_FRAMEWORK_DIR) - mv ./$(INTERNALSDK_FRAMEWORK_NAME) $(INTERNALSDK_FRAMEWORK_DIR)/$(INTERNALSDK_FRAMEWORK_NAME) + @echo "Moving framework" + @mkdir -p $(INTERNALSDK_FRAMEWORK_DIR) + @mv ./$(INTERNALSDK_FRAMEWORK_NAME) $(INTERNALSDK_FRAMEWORK_DIR)/$(INTERNALSDK_FRAMEWORK_NAME) + @echo "Framework generated" build-release-framework: assert-go-version install-gomobile @echo "Nuking $(INTERNALSDK_FRAMEWORK_DIR) and $(MINISQL_FRAMEWORK_DIR)" diff --git a/internalsdk/auth/auth.go b/internalsdk/auth/auth.go index 1f0f07ce66..56fdd95948 100644 --- a/internalsdk/auth/auth.go +++ b/internalsdk/auth/auth.go @@ -51,6 +51,22 @@ type AuthClient interface { // NewClient creates a new instance of AuthClient func NewClient(baseURL string, userConfig func() common.UserConfig) AuthClient { + var httpClient *http.Client + + if strings.HasSuffix(baseURL, common.APIBaseUrl) { + log.Debugf("Using Fronted proxy for auth client") + // iOS + // There is no use of chnained proxy in iOS since all requests will fail + httpClient = &http.Client{ + Transport: proxied.Fronted("authClient-ios"), + Timeout: 30 * time.Second, + } + } else { + httpClient = &http.Client{ + Transport: proxied.ChainedThenFronted(), + Timeout: 30 * time.Second, + } + } rc := webclient.NewRESTClient(&webclient.Opts{ BaseURL: baseURL, OnBeforeRequest: func(client *resty.Client, req *http.Request) error { @@ -59,10 +75,7 @@ func NewClient(baseURL string, userConfig func() common.UserConfig) AuthClient { }, // The Auth client uses an http.Client that first attempts to connect via chained proxies // and then falls back to using domain fronting with the custom op name above - HttpClient: &http.Client{ - Transport: proxied.ChainedThenFronted(), - Timeout: 30 * time.Second, - }, + HttpClient: httpClient, UserConfig: userConfig, }) return &authClient{rc} diff --git a/internalsdk/common/const.go b/internalsdk/common/const.go index 7bed11d4b3..f606639011 100644 --- a/internalsdk/common/const.go +++ b/internalsdk/common/const.go @@ -45,7 +45,7 @@ var ( DFBaseUrl = "df.iantem.io/api/v1" APIBaseUrl = "iantem.io/api/v1" - log = golog.LoggerFor("flashlight.common") + log = golog.LoggerFor("internalsdk.common") forceAds bool ) diff --git a/internalsdk/ios/config.go b/internalsdk/ios/config.go index 05e385b424..bd286af434 100644 --- a/internalsdk/ios/config.go +++ b/internalsdk/ios/config.go @@ -106,7 +106,7 @@ type UserConfig struct { func (cf *configurer) Configure(userID int, proToken string, refreshProxies bool) (*ConfigResult, error) { // Log the full method run time. defer func(start time.Time) { - log.Debugf("Configured completed in %v", time.Since(start)) + log.Debugf("Configured completed in %v seconds", time.Since(start).Seconds()) }(time.Now()) result := &ConfigResult{} if err := cf.writeUserConfig(); err != nil { @@ -128,12 +128,13 @@ func (cf *configurer) Configure(userID int, proToken string, refreshProxies bool var globalUpdated, proxiesUpdated bool setupFronting := func() error { + start := time.Now() log.Debug("Setting up fronting") - defer log.Debug("Set up fronting") - if frontingErr := cf.configureFronting(global, shortFrontedAvailableTimeout); frontingErr != nil { + defer log.Debugf("Setting up fronting completed in %v", time.Since(start).Seconds()) + if frontingErr := cf.configureFronting(global); frontingErr != nil { log.Errorf("Unable to configure fronting on first try, update global config directly from GitHub and try again: %v", frontingErr) global, globalUpdated = cf.updateGlobal(http.DefaultTransport, global, globalEtag, "https://raw.githubusercontent.com/getlantern/lantern-binaries/main/cloud.yaml.gz") - return cf.configureFronting(global, longFrontedAvailableTimeout) + return cf.configureFronting(global) } return nil } @@ -158,10 +159,11 @@ func (cf *configurer) Configure(userID int, proToken string, refreshProxies bool log.Errorf("Unable to save updated UserConfig with country and allow probes: %v", err) } }() - + globalStart := time.Now() log.Debug("Updating global config") global, globalUpdated = cf.updateGlobal(cf.rt, global, globalEtag, "https://globalconfig.flashlightproxy.com/global.yaml.gz") log.Debug("Updated global config") + log.Debugf("Global config update completed in %v seconds", time.Since(globalStart).Seconds()) if refreshProxies { log.Debug("Refreshing proxies") proxies, proxiesUpdated = cf.updateProxies(proxies, proxiesEtag) @@ -409,7 +411,7 @@ func (cf *configurer) doUpdateFromWeb(rt http.RoundTripper, name string, etag st return bytes, newEtag, nil } -func (cf *configurer) configureFronting(global *config.Global, timeout time.Duration) error { +func (cf *configurer) configureFronting(global *config.Global) error { log.Debug("Configuring fronting") certs, err := global.TrustedCACerts() if err != nil { diff --git a/internalsdk/ios/ios.go b/internalsdk/ios/ios.go index fe7f810375..bb20c6acd7 100644 --- a/internalsdk/ios/ios.go +++ b/internalsdk/ios/ios.go @@ -24,8 +24,6 @@ const ( maxDNSGrabAge = 1 * time.Hour // this doesn't need to be long because our fake DNS records have a TTL of only 1 second. We use a smaller value than on Android to be conservative with memory usag. quotaSaveInterval = 1 * time.Minute - shortFrontedAvailableTimeout = 30 * time.Second - longFrontedAvailableTimeout = 5 * time.Minute logMemoryInterval = 5 * time.Second forceGCInterval = 250 * time.Millisecond diff --git a/internalsdk/pro/pro.go b/internalsdk/pro/pro.go index de98aaee3c..1617575542 100644 --- a/internalsdk/pro/pro.go +++ b/internalsdk/pro/pro.go @@ -69,6 +69,19 @@ type ProClient interface { // NewClient creates a new instance of ProClient func NewClient(baseURL string, userConfig func() common.UserConfig) ProClient { + var httpClient *http.Client + if common.Platform == "ios" { + //For iOS use fronted proxy chined proxy does not work with iOS at the moment + httpClient = &http.Client{ + Transport: proxied.Fronted("proclient-ios"), + Timeout: 30 * time.Second, + } + } else { + httpClient = &http.Client{ + Transport: proxied.ParallelForIdempotent(), + Timeout: 30 * time.Second, + } + } return &proClient{ userConfig: userConfig, backoffRunner: &backoffRunner{}, @@ -76,10 +89,7 @@ func NewClient(baseURL string, userConfig func() common.UserConfig) ProClient { // The default http.RoundTripper used by the ProClient is ParallelForIdempotent which // attempts to send requests through both chained and direct fronted routes in parallel // for HEAD and GET requests and ChainedThenFronted for all others. - HttpClient: &http.Client{ - Transport: proxied.ParallelForIdempotent(), - Timeout: 30 * time.Second, - }, + HttpClient: httpClient, OnBeforeRequest: func(client *resty.Client, req *http.Request) error { prepareProRequest(req, common.ProAPIHost, userConfig()) return nil diff --git a/internalsdk/session_model.go b/internalsdk/session_model.go index 2e9beddd54..5dd29acaa4 100644 --- a/internalsdk/session_model.go +++ b/internalsdk/session_model.go @@ -208,7 +208,7 @@ func (m *SessionModel) setupIosConfigure(configPath string, userId int, token st return } m.iosConfigurer = global - log.Debugf("Found global config IOS configure done %v", global) + log.Debug("Found global config IOS configure done") return // Exit the loop after success } log.Debugf("global config not available, retrying...") @@ -506,7 +506,7 @@ func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (inter ttlSeconds := arguments.Get("ttlSeconds").Int() err := m.BandwidthUpdate(percent, mibUsed, mibAllowed, ttlSeconds) if err != nil { - return nil, err + return nil, log.Errorf("Error while updating bandwidth %v", err) } return true, nil case "isUserFirstTimeVisit": diff --git a/ios/Podfile b/ios/Podfile index 0f44475938..8c5fbdc33e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,8 +34,6 @@ target 'Runner' do use_frameworks! :linkage => :static use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -# pod 'SQLite.swift', '~> 0.12.2' - pod 'Toast-Swift', '~> 5.0.1' end # target 'LanternTests' do diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6f8e664806..f51915c6b7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -130,7 +130,6 @@ PODS: - FlutterMacOS - SwiftyGif (5.4.5) - Toast (4.1.1) - - Toast-Swift (5.0.1) - url_launcher_ios (0.0.1): - Flutter - video_player_avfoundation (0.0.1): @@ -169,7 +168,6 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - Toast-Swift (~> 5.0.1) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) @@ -191,7 +189,6 @@ SPEC REPOS: - Sentry - SwiftyGif - Toast - - Toast-Swift EXTERNAL SOURCES: app_links: @@ -293,12 +290,11 @@ SPEC CHECKSUMS: sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e - Toast-Swift: 9b6a70f28b3bf0b96c40d46c0c4b9d6639846711 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 -PODFILE CHECKSUM: edbeaea3b499feb7b2b276309b09c237de1a6cff +PODFILE CHECKSUM: 6238d8bf7027250fc945a92754c7db627d57de25 COCOAPODS: 1.16.2 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 020ee85737..727705d2b7 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,6 @@ import Flutter import Internalsdk import SQLite -import Toast_Swift import UIKit //know Issue @@ -21,6 +20,7 @@ import UIKit private var lanternModel: LanternModel! private var vpnModel: VpnModel! private var messagingModel: MessagingModel! + private var vpnHelper: VpnHelper! override func application( _ application: UIApplication, @@ -61,9 +61,17 @@ import UIKit } lanternModel = LanternModel(flutterBinary: self.flutterbinaryMessenger) sessionModel = try SessionModel(flutterBinary: self.flutterbinaryMessenger) + vpnHelper = VpnHelper( + constants: Constants(process: .app), + fileManager: .default, + userDefaults: Constants.appGroupDefaults, + notificationCenter: .default, + flashlightManager: FlashlightManager.appDefault, + vpnManager: (isSimulator() ? MockVPNManager() : VPNManager.appDefault)) + vpnModel = try VpnModel( flutterBinary: self.flutterbinaryMessenger, vpnBase: VPNManager.appDefault, - sessionModel: sessionModel) + sessionModel: sessionModel, vpnHelper: vpnHelper) messagingModel = try MessagingModel(flutterBinary: flutterbinaryMessenger) } diff --git a/ios/Runner/Lantern/Core/Vpn/VPNManager.swift b/ios/Runner/Lantern/Core/Vpn/VPNManager.swift index 7f4efdc6cf..5d163b8990 100644 --- a/ios/Runner/Lantern/Core/Vpn/VPNManager.swift +++ b/ios/Runner/Lantern/Core/Vpn/VPNManager.swift @@ -190,11 +190,11 @@ class VPNManager: VPNBase { _ completion: @escaping (Result) -> Void ) { provider.saveToPreferences { saveError in - if let _ = saveError { + if saveError != nil { completion(.failure(.savingProviderFailed)) } else { provider.loadFromPreferences { loadError in - if let _ = loadError { + if loadError != nil { completion(.failure(.loadingProviderFailed)) } else { completion(.success(())) diff --git a/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift b/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift index b1566a31db..b25bd49e19 100644 --- a/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift +++ b/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift @@ -10,13 +10,7 @@ import UIKit import UserNotifications class VpnHelper: NSObject { - static let shared = VpnHelper( - constants: Constants(process: .app), - fileManager: .default, - userDefaults: Constants.appGroupDefaults, - notificationCenter: .default, - flashlightManager: FlashlightManager.appDefault, - vpnManager: (isSimulator() ? MockVPNManager() : VPNManager.appDefault)) + // MARK: State static let didUpdateStateNotification = Notification.Name("Lantern.didUpdateState") enum VPNState: Equatable { diff --git a/ios/Runner/Lantern/Models/VpnModel.swift b/ios/Runner/Lantern/Models/VpnModel.swift index 5faf5ab97a..1aefd7d9d2 100644 --- a/ios/Runner/Lantern/Models/VpnModel.swift +++ b/ios/Runner/Lantern/Models/VpnModel.swift @@ -10,10 +10,13 @@ import Sentry class VpnModel: BaseModel, InternalsdkVPNManagerProtocol { let vpnManager: VPNBase - let vpnHelper = VpnHelper.shared + let vpnHelper: VpnHelper var sessionModel: SessionModel! - init(flutterBinary: FlutterBinaryMessenger, vpnBase: VPNBase, sessionModel: SessionModel) throws { + init( + flutterBinary: FlutterBinaryMessenger, vpnBase: VPNBase, sessionModel: SessionModel, + vpnHelper: VpnHelper + ) throws { logger.log("Initializing VPNModel") self.vpnManager = vpnBase var error: NSError? @@ -23,6 +26,7 @@ class VpnModel: BaseModel, InternalsdkVPNManagerProtocol { throw error! } self.sessionModel = sessionModel + self.vpnHelper = vpnHelper try super.init(flutterBinary, model) model.setManager(self) diff --git a/ios/Runner/Lantern/Utils/RunningEnv.swift b/ios/Runner/Lantern/Utils/RunningEnv.swift index e766bb2586..2c9a7e3829 100644 --- a/ios/Runner/Lantern/Utils/RunningEnv.swift +++ b/ios/Runner/Lantern/Utils/RunningEnv.swift @@ -30,7 +30,7 @@ func isRunningInTestFlightEnvironment() -> Bool { } private func hasEmbeddedMobileProvision() -> Bool { - if let _ = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") { + if Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") != nil { return true } return false diff --git a/lib/features/checkout/plan_details.dart b/lib/features/checkout/plan_details.dart index abd0068196..2cee87efac 100644 --- a/lib/features/checkout/plan_details.dart +++ b/lib/features/checkout/plan_details.dart @@ -9,8 +9,8 @@ class PlanCard extends StatefulWidget { const PlanCard({ required this.plan, required this.isPro, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _PlanCardState(); diff --git a/lib/features/checkout/plans.dart b/lib/features/checkout/plans.dart index 79832f7c15..d8f84fcaad 100644 --- a/lib/features/checkout/plans.dart +++ b/lib/features/checkout/plans.dart @@ -7,7 +7,6 @@ import 'package:lantern/core/utils/utils.dart'; @RoutePage(name: "PlansPage") class PlansPage extends StatelessWidget { const PlansPage({super.key}); - @override Widget build(BuildContext context) { return FullScreenDialog( diff --git a/lib/features/vpn/vpn_bandwidth.dart b/lib/features/vpn/vpn_bandwidth.dart index 2af4417c80..55d01889ad 100644 --- a/lib/features/vpn/vpn_bandwidth.dart +++ b/lib/features/vpn/vpn_bandwidth.dart @@ -2,69 +2,69 @@ import 'package:fixnum/src/int64.dart'; import 'package:lantern/features/vpn/vpn.dart'; class VPNBandwidth extends StatelessWidget { - const VPNBandwidth({super.key}); + final bool isProUser; + const VPNBandwidth({super.key, required this.isProUser}); @override Widget build(BuildContext context) { return sessionModel .bandwidth((BuildContext context, Bandwidth? bandwidth, Widget? child) { - return sessionModel.proUser((context, isProUser, child) { - if (bandwidth == null || isProUser) { - // Always disable the data cap if it's a pro user or we haven't - // received any bandwidth updates - return const SizedBox(); - } - return Column( - children: [ - Container( - margin: const EdgeInsetsDirectional.only( - top: 4.0, - bottom: 16.0, - ), - child: const CDivider(height: 10), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CText( - 'Daily Data Usage'.i18n, - style: tsSubtitle3.copiedWith( - color: unselectedTabIconColor, - ), - ), - Expanded( - child: CText( - '${bandwidth.mibUsed}/${bandwidth.mibAllowed} MB', - textAlign: TextAlign.end, - style: tsSubtitle4, - ), - ), - ], + if (bandwidth == null || isProUser) { + + // Always disable the data cap if it's a pro user or we haven't + // received any bandwidth updates + return const SizedBox(); + } + return Column( + children: [ + Container( + margin: const EdgeInsetsDirectional.only( + top: 4.0, + bottom: 16.0, ), - const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - color: unselectedTabColor, - border: Border.all( - color: borderColor, - width: 1, + child: const CDivider(height: 10), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CText( + 'Daily Data Usage'.i18n, + style: tsSubtitle3.copiedWith( + color: unselectedTabIconColor, ), - borderRadius: const BorderRadius.all( - Radius.circular(borderRadius), + ), + Expanded( + child: CText( + '${bandwidth.mibUsed}/${bandwidth.mibAllowed} MB', + textAlign: TextAlign.end, + style: tsSubtitle4, ), ), - child: LinearProgressIndicator( - value: (bandwidth.percent.toDouble() / 100).toDouble(), - minHeight: 12, - borderRadius: - const BorderRadius.all(Radius.circular(borderRadius)), - backgroundColor: unselectedTabColor, - valueColor: AlwaysStoppedAnimation(usedDataBarColor), + ], + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: unselectedTabColor, + border: Border.all( + color: borderColor, + width: 1, ), + borderRadius: const BorderRadius.all( + Radius.circular(borderRadius), + ), + ), + child: LinearProgressIndicator( + value: (bandwidth.percent.toDouble() / 100).toDouble(), + minHeight: 12, + borderRadius: + const BorderRadius.all(Radius.circular(borderRadius)), + backgroundColor: unselectedTabColor, + valueColor: AlwaysStoppedAnimation(usedDataBarColor), ), - ], - ); - }); + ), + ], + ); }); } } diff --git a/lib/features/vpn/vpn_tab.dart b/lib/features/vpn/vpn_tab.dart index 69e88a3941..e6c57e3583 100644 --- a/lib/features/vpn/vpn_tab.dart +++ b/lib/features/vpn/vpn_tab.dart @@ -1,9 +1,10 @@ -import 'package:lantern/features/account/split_tunneling.dart'; import 'package:lantern/common/ui/custom/internet_checker.dart'; +import 'package:lantern/features/account/split_tunneling.dart'; import 'package:lantern/features/messaging/messaging.dart'; import 'package:lantern/features/vpn/vpn.dart'; import 'package:lantern/features/vpn/vpn_notifier.dart'; import 'package:shimmer/shimmer.dart'; + import 'vpn_bandwidth.dart'; import 'vpn_pro_banner.dart'; import 'vpn_server_location.dart'; @@ -91,7 +92,7 @@ class VPNTab extends StatelessWidget { const CDivider(height: 32.0), SplitTunnelingWidget(), }, - if (!proUser) const VPNBandwidth(), + if (!proUser) VPNBandwidth(isProUser: proUser), ], ), ),