diff --git a/.github/workflows/app-deploy.yaml b/.github/workflows/app-deploy.yaml index 9179b193a..a87da7b92 100644 --- a/.github/workflows/app-deploy.yaml +++ b/.github/workflows/app-deploy.yaml @@ -22,13 +22,13 @@ on: required: true default: release type: choice - options: [release, debug] + options: [ release, debug ] flavor: description: 'Target flavor:' required: true default: staging type: choice - options: [development, staging, production] + options: [ development, staging, production ] jobs: setup: @@ -155,6 +155,16 @@ jobs: echo "ios_target is set to = ${{ env.ios_target }}" echo "build_mode is set to = ${{ inputs.build_mode }}" + - name: Remove default anti_phishing.json + run: | + rm -f assets/config/anti_phishing.json + + - name: Download anti-phishing file + uses: actions/download-artifact@v4 + with: + name: anti-phishing-json + path: assets/config + - name: Setup Flutter uses: subosito/flutter-action@v2 with: @@ -281,6 +291,25 @@ jobs: - name: Display flavor target run: echo "Flavor target is = ${{ inputs.flavor }}" + - name: Fetch & merge anti-phishing lists + run: | + curl -s https://raw.githubusercontent.com/broxus/ever-wallet-anti-phishing/master/blacklist.json \ + -o /tmp/blacklist1.json + curl -s https://raw.githubusercontent.com/MetaMask/eth-phishing-detect/refs/heads/main/src/config.json \ + -o /tmp/config.json + mkdir -p assets/config + jq -s '(.[0] + .[1].blacklist) | unique | { blacklist: . }' \ + /tmp/blacklist1.json /tmp/config.json \ + > assets/config/anti_phishing.json + echo "→ New anti_phishing.json:" + head -n 5 assets/config/anti_phishing.json + + - name: Flutter clean + run: flutter clean + + - name: Get Flutter packages + run: dart pub get + - name: Build and deploy run: | bash scripts/build.sh \ diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml new file mode 100644 index 000000000..36e924ffb --- /dev/null +++ b/.github/workflows/create-release.yaml @@ -0,0 +1,142 @@ +name: Create release brunch + +on: + workflow_dispatch: + inputs: + version_type: + description: 'Version bump' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +jobs: + start-release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download and merge anti-phishing blacklists + run: | + mkdir -p temp + + echo "Downloading Broxus blacklist..." + curl -s https://raw.githubusercontent.com/broxus/ever-wallet-anti-phishing/master/blacklist.json -o temp/broxus_blacklist.json || exit 1 + + echo "Downloading MetaMask blacklist..." + curl -s https://raw.githubusercontent.com/MetaMask/eth-phishing-detect/refs/heads/main/src/config.json -o temp/metamask_config.json + + cat temp/broxus_blacklist.json | jq -r '.[]' > temp/broxus_entries.txt + + cat temp/metamask_config.json | jq -r '.blacklist[]' > temp/metamask_entries.txt + + broxus_count=$(wc -l < temp/broxus_entries.txt) + metamask_count=$(wc -l < temp/metamask_entries.txt) + + cat temp/broxus_entries.txt temp/metamask_entries.txt | sort -u > temp/merged_entries.txt + + final_count=$(wc -l < temp/merged_entries.txt) + + echo "[" > assets/configs/anti_phishing.json + + while IFS= read -r line; do + # Escape special characters for JSON + escaped_line=$(echo "$line" | sed 's/\\/\\\\/g; s/"/\\"/g') + echo " \"$escaped_line\"," >> assets/configs/anti_phishing.json + done < temp/merged_entries.txt + + sed -i '$s/,$//' assets/configs/anti_phishing.json + echo "]" >> assets/configs/anti_phishing.json + + echo "Merged $broxus_count Broxus entries and $metamask_count MetaMask entries into $final_count unique entries" + + rm -rf temp + + - name: Update version in pubspec.yaml + run: | + current_version=$(grep '^version:' pubspec.yaml | sed 's/version: //') + echo "Current version: $current_version" + + IFS='.' read -r major minor patch <<< "$current_version" + + case "${{ github.event.inputs.version_type }}" in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; + esac + + new_version="$major.$minor.$patch" + echo "New version: $new_version" + + sed -i "s/^version: .*/version: $new_version/" pubspec.yaml + + # Store version for next steps + echo "NEW_VERSION=$new_version" >> $GITHUB_ENV + + - name: Create release branch and commit + run: | + branch_name="release/v${{ env.NEW_VERSION }}" + git checkout -b "$branch_name" + + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + git add assets/configs/anti_phishing.json pubspec.yaml + + git commit -m "chore(release): start release v${{ env.NEW_VERSION }} + + - Update anti-phishing blacklist from upstream sources + - Bump version to ${{ env.NEW_VERSION }} (${{ github.event.inputs.version_type }}) + + Sources: + - Broxus ever-wallet-anti-phishing + - MetaMask eth-phishing-detect" + + git push origin "$branch_name" + + - name: Create Pull Request + run: | + gh pr create \ + --title "chore(release): start release v${{ env.NEW_VERSION }}" \ + --body "$(cat < + + + + + SparX Wallet Phishing Detection + + + +
+

SparX Wallet Phishing Detection

+

+ This domain is currently on the SparX Wallet domain warning list. This means + that based on information available to us, SparX Wallet believes this domain + could currently compromise your security and, as an added safety feature, + SparX Wallet has restricted access to the site. To override this, please read + the rest of this warning for instructions on how to continue at your own risk. +

+

+ There are many reasons sites can appear on our warning list, and our warning + list compiles from other widely used industry lists. Such reasons can include + known fraud or security risks, such as domains that test positive on the + SparX Wallet Phishing Detection. Domains on these warning lists may include + outright malicious websites and legitimate websites that have been compromised + by a malicious actor. +

+

+ To read more about this site please + + search for the domain on CryptoScamDB + . +

+

+ Note that this warning list is compiled on a voluntary basis. This list may be + inaccurate or incomplete. Just because a domain does not appear on this list is + not an implicit guarantee of that domain's safety. As always, your transactions + are your own responsibility. If you wish to interact with any domain on our + warning list, you can do so by + + continuing at your own risk + . +

+
+ + diff --git a/lib/app/service/storage_service/browser_history_storage_service.dart b/lib/app/service/storage_service/browser_history_storage_service.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/app/service/storage_service/browser_history_storage_service.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/app/service/storage_service/browser_permissions_storage_service.dart b/lib/app/service/storage_service/browser_permissions_storage_service.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/app/service/storage_service/browser_permissions_storage_service.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/app/service/storage_service/browser_tabs_storage_service.dart b/lib/app/service/storage_service/browser_tabs_storage_service.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/app/service/storage_service/browser_tabs_storage_service.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/di/di.config.dart b/lib/di/di.config.dart index 0ebfbb061..3350e425b 100644 --- a/lib/di/di.config.dart +++ b/lib/di/di.config.dart @@ -80,6 +80,8 @@ import '../feature/add_seed/enter_seed_phrase/route.dart' as _i741; import '../feature/add_seed/import_wallet/route.dart' as _i176; import '../feature/biometry/view/route.dart' as _i434; import '../feature/bootstrap_failed/route.dart' as _i501; +import '../feature/browser_v2/domain/delegates/browser_anti_phishing_delegate.dart' + as _i106; import '../feature/browser_v2/domain/delegates/browser_service_auth_delegate.dart' as _i931; import '../feature/browser_v2/domain/delegates/browser_service_bookmarks_delegate.dart' @@ -336,6 +338,8 @@ extension GetItInjectableX on _i174.GetIt { ); gh.lazySingleton<_i128.IIdentifyIconsService>( () => _i316.IdentifyIconsService(gh<_i128.AppStorageService>())); + gh.factory<_i106.BrowserAntiPhishingDelegate>( + () => _i106.BrowserAntiPhishingDelegate(gh<_i104.ResourcesService>())); gh.singleton<_i1020.BalanceStorageService>( () => _i1020.BalanceStorageService( gh<_i792.GetStorage>(instanceName: 'overallBalancesDomain'), @@ -703,6 +707,11 @@ extension GetItInjectableX on _i174.GetIt { ), instanceName: 'ImportWalletRoute', ); + gh.singleton<_i82.CompassBaseRoute>( + () => _i852.AddExistingWalletRoute( + gh<_i82.CompassBaseRoute>(instanceName: 'ImportWalletRoute')), + instanceName: 'AddExistingWalletRoute', + ); gh.singleton<_i470.BrowserService>( () => _i470.BrowserService( gh<_i850.AppLinksService>(), @@ -715,25 +724,10 @@ extension GetItInjectableX on _i174.GetIt { gh<_i617.BrowserServiceHistoryDelegate>(), gh<_i475.BrowserServicePermissionsDelegate>(), gh<_i77.BrowserServiceTabsDelegate>(), + gh<_i106.BrowserAntiPhishingDelegate>(), ), dispose: (i) => i.dispose(), ); - gh.singleton<_i533.RootTabService>(() => _i533.RootTabService( - gh<_i309.CompassRouter>(), - gh<_i470.BrowserService>(), - )); - gh.singleton<_i82.CompassBaseRoute>( - () => _i852.AddExistingWalletRoute( - gh<_i82.CompassBaseRoute>(instanceName: 'ImportWalletRoute')), - instanceName: 'AddExistingWalletRoute', - ); - gh.singleton<_i299.SessionService>(() => _i299.SessionService( - gh<_i771.NekotonRepository>(), - gh<_i725.StorageManagerService>(), - gh<_i679.SecureStorageService>(), - gh<_i958.IIdentifyIconsService>(), - gh<_i470.BrowserService>(), - )); gh.singleton<_i82.CompassBaseRoute>( () => _i45.ManageSeedsAccountsRoute( gh<_i82.CompassBaseRoute>(instanceName: 'SeedDetailRoute'), @@ -753,6 +747,10 @@ extension GetItInjectableX on _i174.GetIt { gh<_i470.BrowserService>(), gh<_i771.NekotonRepository>(), )); + gh.singleton<_i533.RootTabService>(() => _i533.RootTabService( + gh<_i309.CompassRouter>(), + gh<_i470.BrowserService>(), + )); gh.singleton<_i82.CompassBaseRoute>( () => _i1010.OnBoardingRoute( gh<_i82.CompassBaseRoute>(instanceName: 'ChooseNetworkRoute')), @@ -763,6 +761,13 @@ extension GetItInjectableX on _i174.GetIt { gh<_i82.CompassBaseRoute>(instanceName: 'ManageSeedsAccountsRoute')), instanceName: 'ProfileRoute', ); + gh.singleton<_i299.SessionService>(() => _i299.SessionService( + gh<_i771.NekotonRepository>(), + gh<_i725.StorageManagerService>(), + gh<_i679.SecureStorageService>(), + gh<_i958.IIdentifyIconsService>(), + gh<_i470.BrowserService>(), + )); gh.singleton<_i82.CompassBaseRoute>( () => _i786.RootRoute( gh<_i82.CompassBaseRoute>(instanceName: 'WalletRoute'), diff --git a/lib/feature/browser_v2/custom_web_controller.dart b/lib/feature/browser_v2/custom_web_controller.dart index a318f5908..de32c5e86 100644 --- a/lib/feature/browser_v2/custom_web_controller.dart +++ b/lib/feature/browser_v2/custom_web_controller.dart @@ -53,6 +53,26 @@ class CustomWebViewController { ); } + Future loadData({ + required String data, + String mimeType = 'text/html', + String encoding = 'utf8', + WebUri? baseUrl, + WebUri? historyUrl, + WebUri? allowingReadAccessTo, + }) { + return _safeCall( + () => _nativeController.loadData( + data: data, + mimeType: mimeType, + encoding: encoding, + baseUrl: baseUrl, + historyUrl: historyUrl, + allowingReadAccessTo: allowingReadAccessTo, + ), + ); + } + void loggedOut() { _safeCall(_nativeController.loggedOut); } diff --git a/lib/feature/browser_v2/domain/delegates/browser_anti_phishing_delegate.dart b/lib/feature/browser_v2/domain/delegates/browser_anti_phishing_delegate.dart new file mode 100644 index 000000000..1f7ab05b1 --- /dev/null +++ b/lib/feature/browser_v2/domain/delegates/browser_anti_phishing_delegate.dart @@ -0,0 +1,59 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:app/app/service/resources_service.dart'; +import 'package:app/utils/json/json.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:injectable/injectable.dart'; +import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; + +@injectable +class BrowserAntiPhishingDelegate { + BrowserAntiPhishingDelegate(this._resourcesService); + + final _blackListSubj = BehaviorSubject>.seeded(HashSet()); + + HashSet get blackList => _blackListSubj.value; + + final ResourcesService _resourcesService; + + final _log = Logger('BrowserAntiPhishingManager'); + + Future init() { + return loadLinksJson(); + } + + void dispose() { + _blackListSubj.close(); + } + + Future loadLinksJson() async { + try { + final json = await _resourcesService.loadString( + 'assets/configs/anti_phishing.json', + ); + + final map = await compute>(_parse, json); + + _blackListSubj.add( + HashSet.from( + castJsonList(map['blacklist']), + ), + ); + } catch (e, s) { + _log.severe('Load blacklist JSON error', e, s); + } + } + + Future getPhishingGuardHtml(String path) async { + final html = await rootBundle.loadString('assets/html/anti_phishing.html'); + html.replaceFirst('{PHISHING_ORIGINAL_SITE}', path); + return html; + } + + static Map _parse(String json) { + return jsonDecode(json) as Map; + } +} diff --git a/lib/feature/browser_v2/domain/delegates/browser_service_pages_controllers_delegate.dart b/lib/feature/browser_v2/domain/delegates/browser_service_pages_controllers_delegate.dart index c9d438dca..4661a85b5 100644 --- a/lib/feature/browser_v2/domain/delegates/browser_service_pages_controllers_delegate.dart +++ b/lib/feature/browser_v2/domain/delegates/browser_service_pages_controllers_delegate.dart @@ -37,6 +37,19 @@ class BrowserServicePagesControllersDelegate implements BrowserDelegate { ); } + Future loadData( + String tabId, + String html, { + WebUri? baseUrl, + WebUri? historyUrl, + }) async { + return _controllers[tabId]?.loadData( + data: html, + baseUrl: baseUrl, + historyUrl: historyUrl, + ); + } + Future goBack(String tabId) async => _controllers[tabId]?.goBack(); Future goForward(String tabId) async => diff --git a/lib/feature/browser_v2/domain/delegates/browser_service_tabs_delegate.dart b/lib/feature/browser_v2/domain/delegates/browser_service_tabs_delegate.dart index 380cf1a2b..580d44e5d 100644 --- a/lib/feature/browser_v2/domain/delegates/browser_service_tabs_delegate.dart +++ b/lib/feature/browser_v2/domain/delegates/browser_service_tabs_delegate.dart @@ -36,7 +36,7 @@ abstract interface class BrowserServiceTabs { NotNullListenableState> get allTabsIdsState; - ListenableState get activeTabUrlHostState; + ListenableState get activeTabUriState; NotNullListenableState? getTabListenableById(String id); @@ -139,7 +139,7 @@ class BrowserServiceTabsDelegate initValue: const ToolbarData(), ); - late final _activeTabUrlHostState = StateNotifier(); + late final _activeTabUriState = StateNotifier(); final _groupsReactiveStore = GroupsReactiveStore(); final _tabsReactiveStore = TabsReactiveStore(); @@ -161,7 +161,7 @@ class BrowserServiceTabsDelegate _tabsReactiveStore.entitiesIdsListState; @override - ListenableState get activeTabUrlHostState => _activeTabUrlHostState; + ListenableState get activeTabUriState => _activeTabUriState; @override ListenableState get screenshotsState => @@ -276,9 +276,7 @@ class BrowserServiceTabsDelegate @override void updateCachedUrl(String tabId, Uri uri) { - if (tabId == activeTabId) { - _activeTabUrlHostState.accept(uri.host); - } + _updateUriState(tabId, uri); _tabsReactiveStore.updateUrl(tabId: tabId, uri: uri); _browserTabsStorageService.saveBrowserTabs(_tabsReactiveStore.entities); _updateControlPanel(); @@ -304,6 +302,21 @@ class BrowserServiceTabsDelegate return requestUrl(id, uri); } + void loadData( + String tabId, + String html, { + WebUri? baseUrl, + WebUri? historyUrl, + }) { + _updateUriState(tabId, null); + _controllersDelegate.loadData( + tabId, + html, + baseUrl: baseUrl, + historyUrl: historyUrl, + ); + } + @override void updateTabTitle(String tabId, String title) { _tabsReactiveStore.updateTitle(id: tabId, title: title); @@ -590,6 +603,11 @@ class BrowserServiceTabsDelegate /// Put browser tabs to stream void _fetchDataFromCache() { final tabs = _browserTabsStorageService.getTabs() + ..forEach((tab) { + if (tab.url.hasEmptyPath || tab.url.path == 'blank') { + tab.url = Uri.parse(''); + } + }) ..sort( (a, b) => a.sortingOrder.compareTo(b.sortingOrder), ); @@ -639,8 +657,8 @@ class BrowserServiceTabsDelegate _updateControlPanel(); if (activeTabId != null) { - _activeTabUrlHostState.accept( - _tabsReactiveStore.getCachedUrl(activeTabId!)?.host, + _activeTabUriState.accept( + _tabsReactiveStore.getCachedUrl(activeTabId!), ); } } @@ -657,4 +675,10 @@ class BrowserServiceTabsDelegate ), ); } + + void _updateUriState(String tabId, Uri? uri) { + if (tabId == activeTabId && uri != _activeTabUriState.value) { + _activeTabUriState.accept(uri); + } + } } diff --git a/lib/feature/browser_v2/domain/service/browser_service.dart b/lib/feature/browser_v2/domain/service/browser_service.dart index 990a563b7..ffbbb9eb3 100644 --- a/lib/feature/browser_v2/domain/service/browser_service.dart +++ b/lib/feature/browser_v2/domain/service/browser_service.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:collection'; import 'package:app/app/router/router.dart'; import 'package:app/app/service/app_links/app_links.dart'; import 'package:app/app/service/ton_connect/ton_connect_service.dart'; import 'package:app/feature/browser_v2/data/history_type.dart'; +import 'package:app/feature/browser_v2/domain/delegates/browser_anti_phishing_delegate.dart'; import 'package:app/feature/browser_v2/domain/delegates/browser_service_auth_delegate.dart'; import 'package:app/feature/browser_v2/domain/delegates/browser_service_bookmarks_delegate.dart'; import 'package:app/feature/browser_v2/domain/delegates/browser_service_favicon_delegate.dart'; @@ -32,6 +34,7 @@ class BrowserService { this._historyDelegate, this._permissionsDelegate, this._tabsDelegate, + this._antiPhishingDelegate, ); final AppLinksService _appLinksService; @@ -45,9 +48,12 @@ class BrowserService { final BrowserServiceHistoryDelegate _historyDelegate; final BrowserServicePermissionsDelegate _permissionsDelegate; final BrowserServiceTabsDelegate _tabsDelegate; + final BrowserAntiPhishingDelegate _antiPhishingDelegate; final _isContentInteractedStream = BehaviorSubject.seeded(false); + final _phishingUrlCache = HashSet(); + StreamSubscription? _appLinksNavSubs; BrowserServiceAuth get auth => _authDelegate; @@ -64,11 +70,12 @@ class BrowserService { ValueStream get isContentInteractedStream => _isContentInteractedStream; - void init() { + Future init() async { _bookmarksDelegate.init(); _historyDelegate.init(); _permissionsDelegate.init(); _tabsDelegate.init(); + await _antiPhishingDelegate.init(); WidgetsBinding.instance.addPostFrameCallback((_) { _appLinksNavSubs = _appLinksService.browserLinksStream.listen(_listenAppLinks); @@ -86,6 +93,7 @@ class BrowserService { void dispose() { _tabsDelegate.dispose(); _appLinksNavSubs?.cancel(); + _antiPhishingDelegate.dispose(); } void openUrl(Uri uri) { @@ -152,6 +160,34 @@ class BrowserService { }); } + bool checkIsPhishingUri(Uri uri) { + final list = _antiPhishingDelegate.blackList; + + if (_phishingUrlCache.contains(uri.host)) { + return true; + } + + for (final link in list) { + if (uri.host == link || + uri.host.startsWith(link) || + link.startsWith('*') && uri.host.endsWith(link.substring(1))) { + _phishingUrlCache.add(uri.host); + return true; + } + } + + return false; + } + + Future loadPhishingGuard(String tabId, Uri uri) async { + final html = await _antiPhishingDelegate.getPhishingGuardHtml(uri.path); + + return _tabsDelegate.loadData( + tabId, + html, + ); + } + void _listenAppLinks(BrowserAppLinksData event) { _tabsDelegate.openUrl(event.url); } diff --git a/lib/app/service/storage_service/browser_bookmarks_storage_service.dart b/lib/feature/browser_v2/managers/tabs/tabs_manager.dart similarity index 100% rename from lib/app/service/storage_service/browser_bookmarks_storage_service.dart rename to lib/feature/browser_v2/managers/tabs/tabs_manager.dart diff --git a/lib/feature/browser_v2/screens/main/browser_main_screen.dart b/lib/feature/browser_v2/screens/main/browser_main_screen.dart index ada13dad4..6664a0a94 100644 --- a/lib/feature/browser_v2/screens/main/browser_main_screen.dart +++ b/lib/feature/browser_v2/screens/main/browser_main_screen.dart @@ -116,7 +116,7 @@ class BrowserMainScreen extends ElementaryWidget { offsetAnimation: wm.animations.urlMenuOffsetAnimation, opacityAnimation: wm.animations.urlMenuOpacityAnimation, child: HostPanel( - wm.tabs.hostState, + wm.tabs.activeTabUriState, key: wm.keys.urlKey, onPressed: wm.onPressedViewUrlPanel, ), diff --git a/lib/feature/browser_v2/screens/main/browser_main_screen_model.dart b/lib/feature/browser_v2/screens/main/browser_main_screen_model.dart index ded5c72ae..427e5554c 100644 --- a/lib/feature/browser_v2/screens/main/browser_main_screen_model.dart +++ b/lib/feature/browser_v2/screens/main/browser_main_screen_model.dart @@ -30,8 +30,8 @@ class BrowserMainScreenModel extends ElementaryModel { NotNullListenableState> get allTabsIdsState => _browserService.tab.allTabsIdsState; - ListenableState get activeTabUrlHostState => - _browserService.tab.activeTabUrlHostState; + ListenableState get activeTabUriState => + _browserService.tab.activeTabUriState; String? get activeTabId => activeTabIdState.value; @@ -79,10 +79,21 @@ class BrowserMainScreenModel extends ElementaryModel { final isUrl = UrlValidator.checkString(text); + final url = isUrl ? Uri.parse(text) : Uri.parse('$_searchEngineUri$text'); + + if (isUrl) { + final isPhishing = _browserService.checkIsPhishingUri(url); + + if (isPhishing) { + _browserService.loadPhishingGuard(tabId, url); + return; + } + } + unawaited( _browserService.tab.requestUrl( tabId, - isUrl ? Uri.parse(text) : Uri.parse('$_searchEngineUri$text'), + url, ), ); } diff --git a/lib/feature/browser_v2/screens/main/browser_main_screen_wm.dart b/lib/feature/browser_v2/screens/main/browser_main_screen_wm.dart index cd6af3c60..1d338cb1f 100644 --- a/lib/feature/browser_v2/screens/main/browser_main_screen_wm.dart +++ b/lib/feature/browser_v2/screens/main/browser_main_screen_wm.dart @@ -173,7 +173,7 @@ class BrowserMainScreenWidgetModel void initWidgetModel() { _menuState.addListener(_handleMenuState); _viewVisibleState.addListener(_updatePastGo); - model.activeTabUrlHostState.addListener(_updatePastGo); + model.activeTabUriState.addListener(_updatePastGo); WidgetsBinding.instance.addPostFrameCallback((_) { final groupId = model.activeGroupIdState.value; @@ -202,7 +202,7 @@ class BrowserMainScreenWidgetModel _renderManager.dispose(); _pastGoDelegate.dispose(); _pageSlideDelegate.dispose(); - model.activeTabUrlHostState.removeListener(_updatePastGo); + model.activeTabUriState.removeListener(_updatePastGo); super.dispose(); } @@ -347,7 +347,7 @@ class BrowserMainScreenWidgetModel Future _updatePastGo() async { _pastGoDelegate.updateVisible( (_viewVisibleState.value) && - (model.activeTabUrlHostState.value?.isEmpty ?? false) && + (model.activeTabUriState.value?.path.isEmpty ?? false) && await checkExistClipBoardData(), ); } diff --git a/lib/feature/browser_v2/screens/main/delegates/ui_past_go_delegate.dart b/lib/feature/browser_v2/screens/main/delegates/ui_past_go_delegate.dart index 2d25617c7..d4bbe7b0c 100644 --- a/lib/feature/browser_v2/screens/main/delegates/ui_past_go_delegate.dart +++ b/lib/feature/browser_v2/screens/main/delegates/ui_past_go_delegate.dart @@ -32,8 +32,6 @@ class BrowserPastGoUiDelegate implements BrowserPastGoUi { _model.requestUrl(_model.activeTabId, text); } - //activeTabUrlHostState - // ignore: avoid_positional_boolean_parameters void updateVisible(bool isView) { _showPastGoState.accept(isView); diff --git a/lib/feature/browser_v2/screens/main/delegates/ui_tabs_and_groups_delegate.dart b/lib/feature/browser_v2/screens/main/delegates/ui_tabs_and_groups_delegate.dart index 1d8d51306..09c32014d 100644 --- a/lib/feature/browser_v2/screens/main/delegates/ui_tabs_and_groups_delegate.dart +++ b/lib/feature/browser_v2/screens/main/delegates/ui_tabs_and_groups_delegate.dart @@ -15,7 +15,7 @@ import 'package:flutter/material.dart'; abstract interface class BrowserTabsAndGroupsUi { ListenableState>?> get viewTabsState; - ListenableState get hostState; + ListenableState get activeTabUriState; ListenableState get selectedGroupIdState; @@ -87,7 +87,7 @@ class BrowserTabsAndGroupsUiDelegate implements BrowserTabsAndGroupsUi { get viewTabsState => _viewTabsState; @override - ListenableState get hostState => model.activeTabUrlHostState; + ListenableState get activeTabUriState => model.activeTabUriState; @override ListenableState get tabAnimationTypeState => diff --git a/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/address_bar.dart b/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/address_bar.dart index 72a938afa..76831b7a8 100644 --- a/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/address_bar.dart +++ b/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/address_bar.dart @@ -53,9 +53,13 @@ class _BrowserAddressBarState extends State { void _handleTab() { final urlText = _url.toString(); - if (_controller.text != urlText) { - _controller.text = urlText; + if (_controller.text == urlText) { + return; + } else if (urlText == 'about:blank') { + _controller.text = ''; + return; } + _controller.text = urlText; } @override diff --git a/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/host_panel.dart b/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/host_panel.dart index 46a7d7c64..5d0a5c816 100644 --- a/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/host_panel.dart +++ b/lib/feature/browser_v2/screens/main/widgets/control_panels/navigation_panel/host_panel.dart @@ -4,14 +4,14 @@ import 'package:ui_components_lib/v2/ui_components_lib_v2.dart'; class HostPanel extends StatelessWidget { const HostPanel( - this.hostState, { + this.uriState, { required this.onPressed, super.key, }); static const height = DimensSizeV2.d62; - final ListenableState hostState; + final ListenableState uriState; final VoidCallback onPressed; @override @@ -31,11 +31,11 @@ class HostPanel extends StatelessWidget { alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.all(DimensSizeV2.d10), - child: StateNotifierBuilder( - listenableState: hostState, - builder: (_, String? host) { + child: StateNotifierBuilder( + listenableState: uriState, + builder: (_, Uri? uri) { return Text( - host ?? '', + uri?.host ?? '', style: theme.textStyles.labelMedium.copyWith( letterSpacing: -0.1, color: theme.colors.content3, diff --git a/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page.dart b/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page.dart index 39725fdce..0f7a0049f 100644 --- a/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page.dart +++ b/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page.dart @@ -17,7 +17,7 @@ class BrowserPage extends ElementaryWidget { required ValueChanged onLoadingProgressChanged, required this.width, required NotNullListenableState listenable, - Key? key, + super.key, WidgetModelFactory? wmFactory, }) : super( wmFactory ?? @@ -29,7 +29,6 @@ class BrowserPage extends ElementaryWidget { onDispose: onDispose, onLoadingProgressChanged: onLoadingProgressChanged, ), - key: key, ); final double width; diff --git a/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_model.dart b/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_model.dart index 6de4506eb..5c229cd6d 100644 --- a/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_model.dart +++ b/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:app/app/service/approvals_service.dart'; @@ -133,4 +134,23 @@ class BrowserPageModel extends ElementaryModel { bool checkIsActiveTab(String id) { return _activeTabId == null || id == _activeTabId; } + + Future initUri(Uri uri) async { + final isPhishing = checkIsPhishingUri(uri); + + if (isPhishing) { + unawaited(loadPhishingGuard(uri)); + return; + } + + return _browserService.tab.requestUrlActiveTab( + uri.host == '' && uri.path == 'blank' ? WebUri('') : WebUri.uri(uri), + ); + } + + bool checkIsPhishingUri(Uri uri) => _browserService.checkIsPhishingUri(uri); + + Future loadPhishingGuard(Uri uri) { + return _browserService.loadPhishingGuard(_tabId, uri); + } } diff --git a/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_wm.dart b/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_wm.dart index b94fbfb76..5a5e76590 100644 --- a/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_wm.dart +++ b/lib/feature/browser_v2/screens/main/widgets/pages/page/browser_page_wm.dart @@ -154,11 +154,7 @@ class BrowserPageWidgetModel await model.initEvents(customController); if (_url.isNotEmpty) { - await customController.loadUrl( - urlRequest: URLRequest( - url: WebUri.uri(_tab.url), - ), - ); + unawaited(model.initUri(_tab.url)); } } @@ -285,6 +281,19 @@ class BrowserPageWidgetModel return NavigationActionPolicy.ALLOW; } + final isGuardPhishing = model.checkIsPhishingUri(url); + + if (isGuardPhishing) { + unawaited( + model.loadPhishingGuard(url), + ); + return NavigationActionPolicy.CANCEL; + } + + if (isGuardPhishing) { + return NavigationActionPolicy.CANCEL; + } + final scheme = navigationAction.request.url?.scheme; if (!_allowSchemes.contains(scheme) || _checkIsCustomAppLink(url)) { diff --git a/lib/feature/splash/splash_screen_model.dart b/lib/feature/splash/splash_screen_model.dart index 8645f78bd..bfc89734e 100644 --- a/lib/feature/splash/splash_screen_model.dart +++ b/lib/feature/splash/splash_screen_model.dart @@ -25,7 +25,7 @@ class SplashScreenModel extends ElementaryModel { Future configure() async { final isInitSuccess = await _bootstrapService.init(currentAppBuildType); - _browserService.init(); + await _browserService.init(); return isInitSuccess; } diff --git a/lib/generated/assets.gen.dart b/lib/generated/assets.gen.dart index b944e3901..5cc378221 100644 --- a/lib/generated/assets.gen.dart +++ b/lib/generated/assets.gen.dart @@ -60,6 +60,9 @@ class $AssetsAnimationsGen { class $AssetsConfigsGen { const $AssetsConfigsGen(); + /// File path: assets/configs/anti_phishing.json + String get antiPhishing => 'assets/configs/anti_phishing.json'; + /// File path: assets/configs/connections.json String get connections => 'assets/configs/connections.json'; @@ -70,7 +73,18 @@ class $AssetsConfigsGen { String get updateRules => 'assets/configs/update_rules.json'; /// List of all assets - List get values => [connections, releaseNotes, updateRules]; + List get values => + [antiPhishing, connections, releaseNotes, updateRules]; +} + +class $AssetsHtmlGen { + const $AssetsHtmlGen(); + + /// File path: assets/html/anti_phishing.html + String get antiPhishing => 'assets/html/anti_phishing.html'; + + /// List of all assets + List get values => [antiPhishing]; } class $AssetsImagesGen { @@ -884,6 +898,7 @@ class Assets { static const $AssetsAbiGen abi = $AssetsAbiGen(); static const $AssetsAnimationsGen animations = $AssetsAnimationsGen(); static const $AssetsConfigsGen configs = $AssetsConfigsGen(); + static const $AssetsHtmlGen html = $AssetsHtmlGen(); static const $AssetsImagesGen images = $AssetsImagesGen(); static const $AssetsSplashGen splash = $AssetsSplashGen(); static const $AssetsTranslationsGen translations = $AssetsTranslationsGen(); diff --git a/pubspec.yaml b/pubspec.yaml index 53f57d41f..95bd0a6eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -149,3 +149,4 @@ flutter: - assets/images/nft_placeholder/ - assets/splash/ - assets/animations/ + - assets/html/