diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d762758518..733217ec1e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + result.success(LanternApp.session.hTTPAddr) + "isPlayServiceAvailable" -> { result.success(LanternApp.getInAppBilling().isPlayStoreAvailable()) } @@ -249,6 +251,7 @@ class SessionModel internal constructor( } else { startResult!!.httpAddr } + val sOCKS5Addr: String get() = if (startResult == null) { "" diff --git a/android/build.gradle b/android/build.gradle index f14804e0e2..c7f60b45e9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -51,6 +51,14 @@ allprojects { maven { url 'https://jitpack.io' } } + /// TODO: remove. hotfix for flutter_inappwebview (using androidx.webkit:webkit:1.8.0) + /// webview_flutter_android: ^3.16.2 androidx.webkit:webkit ^1.9.0 remove SUPPRESS_ERROR_PAGE + configurations.all { + resolutionStrategy { + force 'androidx.webkit:webkit:1.8.0' + } + } + // This code fixes a 'namespace not specified' error upgrading AGP to >= 8.x.x. subprojects { subproject -> subproject.tasks.whenTaskAdded { diff --git a/android/gradle.properties b/android/gradle.properties index 25b7c18522..06952d7610 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx4048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4048m android.useAndroidX=true android.enableJetifier=true org.gradle.caching=true @@ -9,5 +9,4 @@ kotlin.jvm.target.validation.mode = IGNORE # Additional Settings org.gradle.daemon=true -org.gradle.parallel=true org.gradle.configureondemand=true \ No newline at end of file diff --git a/desktop/app/config.go b/desktop/app/config.go index c5c61cf5b6..44a7097084 100644 --- a/desktop/app/config.go +++ b/desktop/app/config.go @@ -29,6 +29,7 @@ type ChatOptions struct { type ConfigOptions struct { DevelopmentMode bool `json:"developmentMode"` ReplicaAddr string `json:"replicaAddr"` + HttpProxyAddr string `json:"httpProxyAddr"` AuthEnabled bool `json:"authEnabled"` ChatEnabled bool `json:"chatEnabled"` SplitTunneling bool `json:"splitTunneling"` @@ -83,6 +84,7 @@ func (app *App) sendConfigOptions() { DevelopmentMode: common.IsDevEnvironment(), AppVersion: common.ApplicationVersion, ReplicaAddr: "", + HttpProxyAddr: app.settings.GetAddr(), AuthEnabled: authEnabled(app), ChatEnabled: false, SplitTunneling: false, diff --git a/lib/app.dart b/lib/app.dart index bd49b5c00e..7f42a927e9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -90,6 +90,7 @@ class _LanternAppState extends State if (!hasConnection) { return; } + final vpnConnected = await vpnModel.isVpnConnected(); /// If vpn is not connected then we should not show the connectivity warning diff --git a/lib/common/common.dart b/lib/common/common.dart index 619506ead4..1c50c86e6f 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -55,6 +55,7 @@ export 'once.dart'; export 'session_model.dart'; export 'single_value_subscriber.dart'; export 'ui/app_buttons.dart'; +export 'ui/app_webview.dart'; export 'ui/audio.dart'; export 'ui/base_screen.dart'; export 'ui/basic_memory_image.dart'; diff --git a/lib/common/config.dart b/lib/common/config.dart index 978219825e..4ae77ad228 100644 --- a/lib/common/config.dart +++ b/lib/common/config.dart @@ -39,6 +39,7 @@ class ConfigOptions { final String sdkVersion; final String deviceId; final String expirationDate; + final String httpProxyAddr; final Map? plans; Devices devices = Devices(); @@ -57,6 +58,7 @@ class ConfigOptions { this.fetchedProxiesConfig = false, this.expirationDate = '', this.sdkVersion = '', + this.httpProxyAddr = '', this.deviceId = '', this.plans = null, this.paymentMethods = null, @@ -82,6 +84,7 @@ class ConfigOptions { developmentMode: parsedJson['developmentMode'], authEnabled: parsedJson['authEnabled'], chatEnabled: parsedJson['chatEnabled'], + httpProxyAddr: parsedJson['httpProxyAddr'], splitTunneling: parsedJson['splitTunneling'], hasSucceedingProxy: parsedJson['hasSucceedingProxy'], fetchedGlobalConfig: parsedJson['fetchedGlobalConfig'], diff --git a/lib/common/session_model.dart b/lib/common/session_model.dart index 544aae4354..7b7f501e79 100644 --- a/lib/common/session_model.dart +++ b/lib/common/session_model.dart @@ -26,7 +26,7 @@ class SessionModel extends Model { late ValueNotifier country; late ValueNotifier referralNotifier; late ValueNotifier deviceIdNotifier; - ValueNotifier langNotifier=ValueNotifier('en_us'); + ValueNotifier langNotifier = ValueNotifier('en_us'); late ValueNotifier proxyAllNotifier; late ValueNotifier serverInfoNotifier; late ValueNotifier userEmail; @@ -431,6 +431,10 @@ class SessionModel extends Model { throw Exception("Not supported on mobile"); } + Future proxyAddr() async { + return await methodChannel.invokeMethod('proxyAddr', {}); + } + /// Auth API end Future getCountryCode() async { return await methodChannel @@ -864,12 +868,6 @@ class SessionModel extends Model { await compute(LanternFFI.emailExists, email); } - Future openWebview(String url) { - return methodChannel.invokeMethod('openWebview', { - 'url': url, - }); - } - Future refreshAppsList() async { await methodChannel.invokeMethod('refreshAppsList'); } diff --git a/lib/common/ui/app_webview.dart b/lib/common/ui/app_webview.dart index 7ff629a252..b047d4ed90 100644 --- a/lib/common/ui/app_webview.dart +++ b/lib/common/ui/app_webview.dart @@ -20,7 +20,7 @@ class AppWebView extends StatefulWidget { class _AppWebViewState extends State { final InAppWebViewSettings settings = InAppWebViewSettings( - isInspectable: false, + isInspectable: kDebugMode, javaScriptEnabled: true, mediaPlaybackRequiresUserGesture: false, allowsInlineMediaPlayback: false, @@ -47,15 +47,41 @@ class _AppWebViewState extends State { class AppBrowser extends InAppBrowser { final VoidCallback? onClose; - final InAppBrowserClassSettings settings = InAppBrowserClassSettings( - browserSettings: InAppBrowserSettings(hideUrlBar: true), - webViewSettings: InAppWebViewSettings( - javaScriptEnabled: true, isInspectable: kDebugMode)); + + static final InAppBrowserClassSettings settings = InAppBrowserClassSettings( + browserSettings: InAppBrowserSettings( + hideTitleBar: true, + hideToolbarBottom: true, + presentationStyle: ModalPresentationStyle.POPOVER, + ), + webViewSettings: InAppWebViewSettings( + sharedCookiesEnabled: true, + javaScriptEnabled: true, + useOnDownloadStart: true, + useShouldOverrideUrlLoading: true, + isInspectable: kDebugMode, + ), + ); AppBrowser({ - required this.onClose, + this.onClose, }); + static Future setProxyAddr() async { + var proxyAvailable = + await WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE); + if (proxyAvailable) { + ProxyController proxyController = ProxyController.instance(); + final proxyAddr = await sessionModel.proxyAddr(); + await proxyController.clearProxyOverride(); + await proxyController.setProxyOverride( + settings: ProxySettings( + proxyRules: [ProxyRule(url: "http://$proxyAddr")], + bypassRules: [], + )); + } + } + @override Future onBrowserCreated() async { print("Browser created"); @@ -76,13 +102,24 @@ class AppBrowser extends InAppBrowser { print("Can't load ${request.url}.. Error: ${error.description}"); } + @override + Future shouldOverrideUrlLoading( + navigationAction) async { + final url = navigationAction.request.url!; + if (url.scheme.startsWith("alipay")) { + await launchUrl(url, mode: LaunchMode.platformDefault); + return NavigationActionPolicy.CANCEL; + } + return NavigationActionPolicy.ALLOW; + } + @override void onProgressChanged(progress) { print("Progress: $progress"); } @override - void onExit() { + Future onExit() async { print("Browser closed"); onClose?.call(); } @@ -115,7 +152,12 @@ class AppBrowser extends InAppBrowser { InAppBrowser.openWithSystemBrowser(url: WebUri(url)); break; default: - await launchUrl(Uri.parse(url), mode: LaunchMode.platformDefault); + await setProxyAddr(); + final instance = AppBrowser(); + await instance.openUrlRequest( + urlRequest: URLRequest(url: WebUri(url)), + settings: settings, + ); break; } } diff --git a/lib/plans/checkout.dart b/lib/plans/checkout.dart index 24c14478ec..a8921f55be 100644 --- a/lib/plans/checkout.dart +++ b/lib/plans/checkout.dart @@ -187,12 +187,13 @@ class _CheckoutState extends State Iterable> paymentMethods, ) { var widgets = []; - for (final paymentMethod in paymentMethods) { + for (final pathAndValue in sortProviders(paymentMethods)) { + final paymentMethod = pathAndValue.value; if (widgets.length == 2) { - widgets.add(options()); + if (paymentMethods.length != 2) widgets.add(options()); if (!showMoreOptions) break; } - widgets.addAll(paymentProviders(paymentMethod.value)); + widgets.addAll(paymentProviders(paymentMethod)); } return widgets; } @@ -286,7 +287,7 @@ class _CheckoutState extends State context.loaderOverlay.hide(); final btcPayURL = value; - await sessionModel.openWebview(btcPayURL); + await AppBrowser.openWebview(btcPayURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); @@ -304,7 +305,7 @@ class _CheckoutState extends State context.loaderOverlay.hide(); final froPayURL = value; - await sessionModel.openWebview(froPayURL); + await AppBrowser.openWebview(froPayURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); @@ -339,14 +340,13 @@ class _CheckoutState extends State context.loaderOverlay.hide(); final shepherdURL = value; - await sessionModel.openWebview(shepherdURL); + await AppBrowser.openWebview(shepherdURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); } } - // This methods is responsible for polling for user data // so if user has done payment or renew plans and show void hasPlansUpdateOrBuy() { @@ -403,7 +403,7 @@ class _CheckoutState extends State context.loaderOverlay.hide(); final btcPayURL = value; - await sessionModel.openWebview(btcPayURL); + await AppBrowser.openWebview(btcPayURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); @@ -464,9 +464,10 @@ class _CheckoutState extends State assert(widget.authFlow != null, 'authFlow is null'); switch (widget.authFlow!) { case AuthFlow.createAccount: - /// There is edge case where user is signup with email and password but not pro - /// this happens when does restore purchase on other device so older device - /// does not have pro status but have email and password + + /// There is edge case where user is signup with email and password but not pro + /// this happens when does restore purchase on other device so older device + /// does not have pro status but have email and password if (sessionModel.hasUserSignedInNotifier.value ?? false) { showSuccessDialog(context, widget.isPro); return; @@ -493,7 +494,7 @@ class _CheckoutState extends State case AuthFlow.updateAccount: // TODO: Handle this case. case AuthFlow.restoreAccount: - // TODO: Handle this case. + // TODO: Handle this case. } } } diff --git a/lib/plans/checkout_legacy.dart b/lib/plans/checkout_legacy.dart index 423f499f1d..9192eaf0f6 100644 --- a/lib/plans/checkout_legacy.dart +++ b/lib/plans/checkout_legacy.dart @@ -23,7 +23,6 @@ class CheckoutLegacy extends StatefulWidget { class _CheckoutLegacyState extends State with SingleTickerProviderStateMixin { - bool showMoreOptions = false; bool showContinueButton = false; @@ -34,20 +33,20 @@ class _CheckoutLegacyState extends State validator: (value) => value!.isEmpty ? null : EmailValidator.validate(value ?? '') - ? null - : 'please_enter_a_valid_email_address'.i18n, + ? null + : 'please_enter_a_valid_email_address'.i18n, ); late final refCodeController = CustomTextEditingController( formKey: refCodeFieldKey, validator: (value) => - // only allow letters and numbers as well as 6 <= length <= 13 - value == null || - value.characters.isEmpty || - RegExp(r'^[a-zA-Z0-9]*$').hasMatch(value) && - (6 <= value.characters.length && - value.characters.length <= 13) - ? null - : 'invalid_or_incomplete_referral_code'.i18n, + // only allow letters and numbers as well as 6 <= length <= 13 + value == null || + value.characters.isEmpty || + RegExp(r'^[a-zA-Z0-9]*$').hasMatch(value) && + (6 <= value.characters.length && + value.characters.length <= 13) + ? null + : 'invalid_or_incomplete_referral_code'.i18n, ); var isRefCodeFieldShowing = false; @@ -84,13 +83,13 @@ class _CheckoutLegacyState extends State padVertical: true, body: sessionModel.paymentMethods( builder: ( - context, - Iterable> paymentMethods, - Widget? child, - ) { + context, + Iterable> paymentMethods, + Widget? child, + ) { defaultProviderIfNecessary(paymentMethods.toList()); - return sessionModel.emailAddress((context, emailAddress, child) => - Column( + return sessionModel.emailAddress( + (context, emailAddress, child) => Column( children: [ PlanStep( stepNum: '2', @@ -127,7 +126,7 @@ class _CheckoutLegacyState extends State ), Container( padding: - const EdgeInsetsDirectional.only(top: 16, bottom: 16), + const EdgeInsetsDirectional.only(top: 16, bottom: 16), width: MediaQuery.of(context).size.width, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -192,48 +191,50 @@ class _CheckoutLegacyState extends State } Widget options() => CInkWell( - onTap: () { - setState(() { - showMoreOptions = !showMoreOptions; - }); - }, - child: Container( - padding: const EdgeInsetsDirectional.only(bottom: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CText( - showMoreOptions ? 'fewer_options'.i18n : 'more_options'.i18n, - style: tsBody1, - ), - const Padding( - padding: EdgeInsetsDirectional.only(start: 8), - child: CAssetImage( - path: ImagePaths.down_arrow, - ), + onTap: () { + setState(() { + showMoreOptions = !showMoreOptions; + }); + }, + child: Container( + padding: const EdgeInsetsDirectional.only(bottom: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CText( + showMoreOptions ? 'fewer_options'.i18n : 'more_options'.i18n, + style: tsBody1, + ), + const Padding( + padding: EdgeInsetsDirectional.only(start: 8), + child: CAssetImage( + path: ImagePaths.down_arrow, + ), + ), + ], ), - ], - ), - ), - ); + ), + ); List paymentOptions( - Iterable> paymentMethods, - ) { + Iterable> paymentMethods, + ) { var widgets = []; - for (final paymentMethod in paymentMethods) { + for (final pathAndValue in sortProviders(paymentMethods)) { + final paymentMethod = pathAndValue.value; if (widgets.length == 2) { widgets.add(options()); if (!showMoreOptions) break; } - widgets.addAll(paymentProviders(paymentMethod.value)); + widgets.addAll(paymentProviders(paymentMethod)); } return widgets; } List paymentProviders(PaymentMethod paymentMethods) { var providers = []; + for (final provider in paymentMethods.providers) { providers.add( PaymentProvider( @@ -263,11 +264,10 @@ class _CheckoutLegacyState extends State //Class methods void selectPaymentProvider(Providers provider) { setState( - () => selectedPaymentProvider = provider, + () => selectedPaymentProvider = provider, ); } - Future onContinueTapped() async { var refCode = refCodeController.value; try { @@ -343,7 +343,7 @@ class _CheckoutLegacyState extends State context.loaderOverlay.hide(); final btcPayURL = value; - await sessionModel.openWebview(btcPayURL); + await AppBrowser.openWebview(btcPayURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); @@ -361,14 +361,13 @@ class _CheckoutLegacyState extends State context.loaderOverlay.hide(); final froPayURL = value; - await sessionModel.openWebview(froPayURL); + await AppBrowser.openWebview(froPayURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); } } - void _proceedWithShepherd() async { try { context.loaderOverlay.show(); @@ -380,7 +379,7 @@ class _CheckoutLegacyState extends State context.loaderOverlay.hide(); final shepherdURL = value; - await sessionModel.openWebview(shepherdURL); + await AppBrowser.openWebview(shepherdURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); @@ -393,7 +392,7 @@ class _CheckoutLegacyState extends State appLogger.i("calling hasPlansUpdateOrBuy to update plans or buy"); try { retry( - () async { + () async { /// Polling for userData that user has updates plans or buy final plansUpdated = await sessionModel.hasUpdatePlansOrBuy(); if (plansUpdated) { @@ -443,7 +442,7 @@ class _CheckoutLegacyState extends State context.loaderOverlay.hide(); final btcPayURL = value; - await sessionModel.openWebview(btcPayURL); + await AppBrowser.openWebview(btcPayURL); } catch (error, stackTrace) { context.loaderOverlay.hide(); showError(context, error: error, stackTrace: stackTrace); @@ -483,5 +482,4 @@ class _CheckoutLegacyState extends State //If needed to change default value changing to from server selectedPaymentProvider = paymentMethod.providers[0].name.toPaymentEnum(); } - } diff --git a/lib/plans/utils.dart b/lib/plans/utils.dart index ef68417178..b1b6d314dd 100644 --- a/lib/plans/utils.dart +++ b/lib/plans/utils.dart @@ -7,13 +7,13 @@ import 'package:lantern/common/ui/app_webview.dart'; const defaultTimeoutDuration = Duration(seconds: 10); bool isProdPlay() { - if (sessionModel.isStoreVersion.value ??false) { + if (sessionModel.isStoreVersion.value ?? false) { return true; } - if (sessionModel.isTestPlayVersion.value ??false) { + if (sessionModel.isTestPlayVersion.value ?? false) { return true; } - return false; + return false; } const lanternStarLogo = CAssetImage( @@ -160,8 +160,11 @@ Future openDesktopPaymentWebview( } } - - +List> sortProviders( + Iterable> paymentMethods, +) => + paymentMethods.toList() + ..sort((a, b) => a.value.method.compareTo(b.value.method)); Plan planFromJson(Map item) { print("called plans $item"); diff --git a/pubspec.lock b/pubspec.lock index 2c73d77a0f..3c260661af 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2126,10 +2126,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: transitive description: @@ -2190,10 +2190,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "6e64fcb1c19d92024da8f33503aaeeda35825d77142c01d0ea2aa32edc79fdc8" + sha256: dad3313c9ead95517bb1cae5e1c9d20ba83729d5a59e5e83c0a2d66203f27f91 url: "https://pub.dev" source: hosted - version: "3.16.7" + version: "3.16.1" webview_flutter_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index af6c3c02e5..064dcb5bb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -157,12 +157,6 @@ dev_dependencies: # following page: https://dart.dev/tools/pub/pubspec dependency_overrides: - # TODO: Recheck once flutter_inappwebview version >6.0.0 is released - flutter_inappwebview_android: - git: - url: https://github.com/holzgeist/flutter_inappwebview - path: flutter_inappwebview_android - ref: d89b1d32638b49dfc58c4b7c84153be0c269d057 flutter_inappwebview_macos: git: url: https://github.com/andychucs/flutter_inappwebview.git