diff --git a/.github/workflows/build-ios-release-pullrequest.yml b/.github/workflows/build-ios-release-pullrequest.yml index 77a60b229..ebf654e13 100644 --- a/.github/workflows/build-ios-release-pullrequest.yml +++ b/.github/workflows/build-ios-release-pullrequest.yml @@ -22,12 +22,26 @@ jobs: branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }} env: APPLE_ID: ${{ secrets.APPLE_ID }} + MATCH_READONLY: "true" steps: - name: Checkout Project uses: actions/checkout@v4 with: fetch-depth: 0 # Ensures the full Git history is available + + - name: Setup Caching + uses: actions/cache@v3 + with: + path: | + ~/Library/Caches/CocoaPods + ios/Pods + ~/.npm + node_modules + vendor/bundle + key: ${{ runner.os }}-ios-${{ hashFiles('**/package-lock.json', '**/Podfile.lock', '**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-ios- - name: Clear All Caches if: github.ref == 'refs/heads/master' @@ -81,15 +95,32 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 + cache: 'npm' - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: 16.2.0 + xcode-version: latest + + - name: Install iOS Simulator Runtime + run: | + echo "Available iOS simulator runtimes:" + xcrun simctl list runtimes + + # Try to download the latest iOS 16.x simulator if not present + if (! xcrun simctl list runtimes | grep -q "iOS 16"); then + echo "Installing iOS 16.4 simulator..." + sudo xcode-select -s /Applications/Xcode.app + xcodebuild -downloadPlatform iOS + fi + + echo "Available iOS simulator runtimes after install:" + xcrun simctl list runtimes - name: Set Up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.1.6 + bundler-cache: true - name: Install Dependencies with Bundler run: | @@ -102,6 +133,7 @@ jobs: - name: Install CocoaPods Dependencies run: | bundle exec fastlane ios install_pods + echo "CocoaPods dependencies installed successfully" - name: Generate Build Number Based on Timestamp id: generate_build_number @@ -147,8 +179,26 @@ jobs: - name: Build App id: build_app run: | - bundle exec fastlane ios build_app_lane --verbose - echo "ipa_output_path=$IPA_OUTPUT_PATH" >> $GITHUB_OUTPUT # Set the IPA output path for future jobs + bundle exec fastlane ios build_app_lane + + # Ensure IPA path is set for subsequent steps + if [ -f "./ios/build/ipa_path.txt" ]; then + IPA_PATH=$(cat ./ios/build/ipa_path.txt) + echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV + echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT + echo "Found IPA at: $IPA_PATH" + else + echo "Warning: ipa_path.txt not found, trying to locate IPA file manually..." + IPA_PATH=$(find ./ios -name "*.ipa" | head -n 1) + if [ -n "$IPA_PATH" ]; then + echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV + echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT + echo "Found IPA at: $IPA_PATH" + else + echo "Error: No IPA file found" + exit 1 + fi + fi - name: Upload Bugsnag Sourcemaps if: success() @@ -156,8 +206,8 @@ jobs: env: BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }} BUGSNAG_RELEASE_STAGE: production - PROJECT_VERSION: ${{ needs.build.outputs.project_version }} - NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }} + PROJECT_VERSION: ${{ env.PROJECT_VERSION }} + NEW_BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }} - name: Upload Build Logs if: always() @@ -165,13 +215,32 @@ jobs: with: name: build_logs path: ./ios/build_logs/ + retention-days: 7 + - name: Verify IPA File Before Upload + run: | + echo "Checking IPA file at: $IPA_OUTPUT_PATH" + if [ -f "$IPA_OUTPUT_PATH" ]; then + echo "✅ IPA file exists" + ls -la "$IPA_OUTPUT_PATH" + else + echo "❌ IPA file not found at: $IPA_OUTPUT_PATH" + echo "Current directory contents:" + find ./ios -name "*.ipa" + exit 1 + fi + - name: Upload IPA as Artifact if: success() uses: actions/upload-artifact@v4 with: - name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa - path: ${{ env.IPA_OUTPUT_PATH }} # Directly from Fastfile `IPA_OUTPUT_PATH` + name: BlueWallet_IPA + path: ${{ env.IPA_OUTPUT_PATH }} + retention-days: 7 + + - name: Delete Temporary Keychain + if: always() + run: bundle exec fastlane ios delete_temp_keychain testflight-upload: needs: build @@ -191,6 +260,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.1.6 + bundler-cache: true - name: Install Dependencies with Bundler run: | @@ -200,18 +270,11 @@ jobs: - name: Download IPA from Artifact uses: actions/download-artifact@v4 with: - name: BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa + name: BlueWallet_IPA path: ./ - name: Create App Store Connect API Key JSON run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json - - - name: Verify IPA File Download - run: | - echo "Current directory:" - pwd - echo "Files in current directory:" - ls -la ./ - name: Set IPA Path Environment Variable run: echo "IPA_OUTPUT_PATH=$(pwd)/BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa" >> $GITHUB_ENV @@ -219,19 +282,23 @@ jobs: - name: Verify IPA Path Before Upload run: | if [ ! -f "$IPA_OUTPUT_PATH" ]; then - echo "IPA file not found at path: $IPA_OUTPUT_PATH" + echo "❌ IPA file not found at path: $IPA_OUTPUT_PATH" + ls -la $(pwd) exit 1 + else + echo "✅ Found IPA at: $IPA_OUTPUT_PATH" fi - name: Print Environment Variables for Debugging run: | echo "LATEST_COMMIT_MESSAGE: $LATEST_COMMIT_MESSAGE" echo "BRANCH_NAME: $BRANCH_NAME" + echo "PROJECT_VERSION: $PROJECT_VERSION" + echo "NEW_BUILD_NUMBER: $NEW_BUILD_NUMBER" + echo "IPA_OUTPUT_PATH: $IPA_OUTPUT_PATH" - name: Upload to TestFlight - run: | - ls -la $IPA_OUTPUT_PATH - bundle exec fastlane ios upload_to_testflight_lane + run: bundle exec fastlane ios upload_to_testflight_lane env: APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8 MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} @@ -242,18 +309,19 @@ jobs: APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - IPA_OUTPUT_PATH: ${{ env.IPA_OUTPUT_PATH }} - name: Post PR Comment if: success() && github.event_name == 'pull_request' uses: actions/github-script@v6 env: BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }} + PROJECT_VERSION: ${{ needs.build.outputs.project_version }} LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }} with: script: | const buildNumber = process.env.BUILD_NUMBER; - const message = `The build ${buildNumber} has been uploaded to TestFlight.`; + const version = process.env.PROJECT_VERSION; + const message = `✅ Build ${version} (${buildNumber}) has been uploaded to TestFlight and will be available for testing soon.`; const prNumber = context.payload.pull_request.number; const repo = context.repo; github.rest.issues.createComment({ diff --git a/Gemfile b/Gemfile index 40894e1eb..b0a72cff0 100644 --- a/Gemfile +++ b/Gemfile @@ -3,10 +3,14 @@ source "https://rubygems.org" # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version ruby "3.1.6" gem 'rubyzip', '2.4.1' -gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' +gem 'cocoapods', '~> 1.14.3' gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' -gem "fastlane", ">= 2.225.0" +gem "fastlane", "~> 2.226.0" gem 'xcodeproj', '< 1.26.0' gem 'concurrent-ruby', '< 1.3.4' + +# Required for App Store Connect API +gem "jwt" + plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 4bb4d2132..ace9d4579 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,18 +24,18 @@ GEM json (>= 1.5.1) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.1050.0) - aws-sdk-core (3.218.1) + aws-eventstream (1.3.1) + aws-partitions (1.1058.0) + aws-sdk-core (3.219.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.98.0) + aws-sdk-kms (1.99.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.180.0) + aws-sdk-s3 (1.182.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -46,10 +46,10 @@ GEM benchmark (0.4.0) bigdecimal (3.1.9) claide (1.1.0) - cocoapods (1.15.2) + cocoapods (1.14.3) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) + cocoapods-core (= 1.14.3) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -64,7 +64,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) + cocoapods-core (1.14.3) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -173,6 +173,9 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-browserstack (0.3.3) rest-client (~> 2.0, >= 2.0.2) + fastlane-plugin-bugsnag (2.3.1) + git + xml-simple fastlane-plugin-bugsnag_sourcemaps_upload (0.2.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) @@ -180,6 +183,11 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) + git (3.0.0) + activesupport (>= 5.0) + addressable (~> 2.8) + process_executer (~> 1.3) + rchardet (~> 1.9) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-core (0.11.3) @@ -220,24 +228,26 @@ GEM http-accept (1.7.0) http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.10.1) jwt (2.10.1) base64 - logger (1.6.5) + logger (1.6.6) mime-types (3.6.0) logger mime-types-data (~> 3.2015) - mime-types-data (3.2025.0204) + mime-types-data (3.2025.0220) mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.25.4) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) + mutex_m (0.3.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) @@ -246,8 +256,10 @@ GEM optparse (0.6.0) os (1.1.4) plist (3.7.2) + process_executer (1.3.0) public_suffix (4.0.7) rake (13.2.1) + rchardet (1.9.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -258,7 +270,7 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) - rexml (3.4.0) + rexml (3.4.1) rouge (3.28.0) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -300,17 +312,21 @@ GEM rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) + xml-simple (1.1.9) + rexml PLATFORMS ruby DEPENDENCIES activesupport (>= 6.1.7.5, != 7.1.0) - cocoapods (>= 1.13, != 1.15.1, != 1.15.0) + cocoapods (~> 1.14.3) concurrent-ruby (< 1.3.4) - fastlane (>= 2.225.0) + fastlane (~> 2.226.0) fastlane-plugin-browserstack + fastlane-plugin-bugsnag fastlane-plugin-bugsnag_sourcemaps_upload + jwt rubyzip (= 2.4.1) xcodeproj (< 1.26.0) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ba84a5d81..89bcb1bc2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -16,7 +16,6 @@ - = 1) { - Toast.makeText(context, "Only one widget instance is allowed.", Toast.LENGTH_SHORT).show() - Log.e(TAG, "Attempted to add multiple widget instances.") - return - } - sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount + 1).apply() Log.d(TAG, "onEnabled called") WidgetUpdateWorker.scheduleWork(context) } override fun onDisabled(context: Context) { super.onDisabled(context) - clearWidgetCount(context) Log.d(TAG, "onDisabled called") clearCache(context) WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME) @@ -47,19 +37,9 @@ class BitcoinPriceWidget : AppWidgetProvider() { override fun onDeleted(context: Context, appWidgetIds: IntArray) { super.onDeleted(context, appWidgetIds) - val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1) - val newCount = widgetCount - appWidgetIds.size - sharedPref.edit().putInt(WIDGET_COUNT_KEY, if (newCount >= 0) newCount else 0).apply() Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}") } - private fun clearWidgetCount(context: Context) { - val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - sharedPref.edit().putInt(WIDGET_COUNT_KEY, 0).apply() - Log.d(TAG, "Widget count reset to 0") - } - private fun clearCache(context: Context) { val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) sharedPref.edit().clear().apply() diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index addaad844..4d0a6519d 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo } from 'react'; -import { Platform, Pressable, TouchableOpacity } from 'react-native'; +import { Platform, TouchableOpacity } from 'react-native'; import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; -import { ContextMenuView, RenderItem, OnPressMenuItemEventObject, IconConfig, MenuElementConfig } from 'react-native-ios-context-menu'; import { ToolTipMenuProps, Action } from './types'; import { useSettings } from '../hooks/context/useSettings'; @@ -9,11 +8,8 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { const { title = '', isMenuPrimaryAction = false, - renderPreview, disabled = false, onPress, - onMenuWillShow, - onMenuWillHide, buttonStyle, onPressMenuItem, children, @@ -23,18 +19,6 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { const { language } = useSettings(); - // Map Menu Items for iOS Context Menu - const mapMenuItemForContextMenuView = useCallback((action: Action) => { - if (!action.id) return null; - return { - actionKey: action.id.toString(), - actionTitle: action.text, - icon: action.icon?.iconValue ? ({ iconType: 'SYSTEM', iconValue: action.icon.iconValue } as IconConfig) : undefined, - state: action.menuState ?? undefined, - attributes: action.disabled ? ['disabled'] : [], - }; - }, []); - // Map Menu Items for RN Menu (supports subactions and displayInline) const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => { if (!action.id) return null; @@ -88,11 +72,6 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { return menuItem; }, []); - const contextMenuItems = useMemo(() => { - const flattenedActions = props.actions.flat().filter(action => action.id); - return flattenedActions.map(mapMenuItemForContextMenuView).filter(item => item !== null) as MenuElementConfig[]; - }, [props.actions, mapMenuItemForContextMenuView]); - const menuViewItemsIOS = useMemo(() => { return props.actions .map(actionGroup => { @@ -119,13 +98,6 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { return mergedActions.map(mapMenuItemForMenuView).filter(item => item !== null) as MenuAction[]; }, [props.actions, mapMenuItemForMenuView]); - const handlePressMenuItemForContextMenuView = useCallback( - (event: OnPressMenuItemEventObject) => { - onPressMenuItem(event.nativeEvent.actionKey); - }, - [onPressMenuItem], - ); - const handlePressMenuItemForMenuView = useCallback( ({ nativeEvent }: NativeActionEvent) => { onPressMenuItem(nativeEvent.event); @@ -133,46 +105,6 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { [onPressMenuItem], ); - const renderContextMenuView = () => { - return ( - - {onPress ? ( - - {children} - - ) : ( - children - )} - - ); - }; - const renderMenuView = () => { return ( { ); }; - return props.actions.length > 0 ? (Platform.OS === 'ios' && renderPreview ? renderContextMenuView() : renderMenuView()) : null; + return props.actions.length > 0 ? renderMenuView() : null; }; export default ToolTipMenu; diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index 8a30d02d4..e2c4c3f63 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import Clipboard from '@react-native-clipboard/clipboard'; import { Linking, View, ViewStyle } from 'react-native'; @@ -36,7 +36,7 @@ interface TransactionListItemProps { type NavigationProps = NativeStackNavigationProp; -export const TransactionListItem: React.FC = React.memo( +export const TransactionListItem: React.FC = memo( ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => { const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); const { colors } = useTheme(); @@ -367,4 +367,12 @@ export const TransactionListItem: React.FC = React.mem ); }, + (prevProps, nextProps) => { + return ( + prevProps.item.hash === nextProps.item.hash && + prevProps.item.received === nextProps.item.received && + prevProps.itemPriceUnit === nextProps.itemPriceUnit && + prevProps.walletID === nextProps.walletID + ); + }, ); diff --git a/components/addresses/AddressItem.tsx b/components/addresses/AddressItem.tsx index de377f5ee..78254d040 100644 --- a/components/addresses/AddressItem.tsx +++ b/components/addresses/AddressItem.tsx @@ -149,6 +149,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad title={item.address} actions={menuActions} onPressMenuItem={onToolTipPress} + // Revisit once RNMenu has renderPreview prop renderPreview={renderPreview} onPress={navigateToReceive} isButton diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9f35ee1df..c770cdb17 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -192,6 +192,40 @@ end # =========================== platform :ios do + # Add helper methods for error handling and retries + def ensure_env_vars(vars) + vars.each do |var| + UI.user_error!("#{var} environment variable is missing") if ENV[var].nil? || ENV[var].empty? + end + end + + def log_success(message) + UI.success("✅ #{message}") + end + + def log_error(message) + UI.error("❌ #{message}") + end + + # Method to safely call actions with retry logic + def with_retry(max_attempts = 3, action_name = "") + attempts = 0 + begin + attempts += 1 + yield + rescue => e + if attempts < max_attempts + wait_time = 10 * attempts + log_error("Attempt #{attempts}/#{max_attempts} for #{action_name} failed: #{e.message}") + UI.message("Retrying in #{wait_time} seconds...") + sleep(wait_time) + retry + else + log_error("#{action_name} failed after #{max_attempts} attempts: #{e.message}") + raise e + end + end + end desc "Register new devices from a file" lane :register_devices_from_txt do @@ -238,26 +272,33 @@ platform :ios do desc "Synchronize certificates and provisioning profiles" lane :setup_provisioning_profiles do + required_vars = ["GIT_ACCESS_TOKEN", "GIT_URL", "ITC_TEAM_ID", "ITC_TEAM_NAME", "KEYCHAIN_PASSWORD"] + ensure_env_vars(required_vars) + UI.message("Setting up provisioning profiles...") - - platform = "ios" - + # Iterate over app identifiers to fetch provisioning profiles app_identifiers.each do |app_identifier| - match( - git_basic_authorization: ENV["GIT_ACCESS_TOKEN"], - git_url: ENV["GIT_URL"], - type: "appstore", - clone_branch_directly: true, # Skip if the branch already exists - platform: platform, - app_identifier: app_identifier, - team_id: ENV["ITC_TEAM_ID"], - team_name: ENV["ITC_TEAM_NAME"], - readonly: true, - keychain_name: "temp_keychain", - keychain_password: ENV["KEYCHAIN_PASSWORD"] - ) + with_retry(3, "Fetching provisioning profile for #{app_identifier}") do + UI.message("Fetching provisioning profile for #{app_identifier}...") + match( + git_basic_authorization: ENV["GIT_ACCESS_TOKEN"], + git_url: ENV["GIT_URL"], + type: "appstore", + clone_branch_directly: true, + platform: "ios", + app_identifier: app_identifier, + team_id: ENV["ITC_TEAM_ID"], + team_name: ENV["ITC_TEAM_NAME"], + readonly: true, + keychain_name: "temp_keychain", + keychain_password: ENV["KEYCHAIN_PASSWORD"] + ) + log_success("Successfully fetched provisioning profile for #{app_identifier}") + end end + + log_success("All provisioning profiles set up") end desc "Fetch development certificates and provisioning profiles for Mac Catalyst" @@ -402,48 +443,114 @@ lane :upload_bugsnag_sourcemaps do end desc "Build the iOS app" -lane :build_app_lane do - Dir.chdir(project_root) do - UI.message("Building the application from: #{Dir.pwd}") + lane :build_app_lane do + Dir.chdir(project_root) do + UI.message("Building the application from: #{Dir.pwd}") - workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace") - export_options_path = File.join(project_root, "ios", "export_options.plist") + workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace") + export_options_path = File.join(project_root, "ios", "export_options.plist") - clear_derived_data_lane - - begin - build_ios_app( - scheme: "BlueWallet", - workspace: workspace_path, - export_method: "app-store", - include_bitcode: false, - configuration: "Release", - skip_profile_detection: false, - include_symbols: true, - export_team_id: ENV["ITC_TEAM_ID"], - export_options: export_options_path, - output_directory: File.join(project_root, "ios", "build"), - output_name: "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa", - buildlog_path: File.join(project_root, "ios", "build_logs"), - silent: false, - clean: true - ) - rescue => e - UI.user_error!("build_ios_app failed: #{e.message}") - end + clear_derived_data_lane + + # Determine which iOS version to use + ios_version = determine_ios_version - # Use File.join to construct paths without extra slashes - ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] + UI.message("Using iOS version: #{ios_version}") + UI.message("Using export options from: #{export_options_path}") + + # Define the IPA output path before building + ipa_directory = File.join(project_root, "ios", "build") + ipa_name = "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa" + ipa_path = File.join(ipa_directory, ipa_name) + + begin + build_ios_app( + scheme: "BlueWallet", + workspace: workspace_path, + export_method: "app-store", + export_options: export_options_path, + output_directory: ipa_directory, + output_name: ipa_name, + buildlog_path: File.join(project_root, "ios", "build_logs"), + ) + rescue => e + UI.user_error!("build_ios_app failed: #{e.message}") + end - if ipa_path && File.exist?(ipa_path) - UI.message("IPA successfully found at: #{ipa_path}") + # Check for IPA path from both our defined path and fastlane's context + ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] || ipa_path + + # Ensure the directory exists + FileUtils.mkdir_p(File.dirname(ipa_path)) unless Dir.exist?(File.dirname(ipa_path)) + + if ipa_path && File.exist?(ipa_path) + UI.message("IPA successfully found at: #{ipa_path}") + else + # Try to find any IPA file as fallback + Dir.chdir(project_root) do + fallback_ipa = Dir.glob("**/*.ipa").first + if fallback_ipa + ipa_path = File.join(project_root, fallback_ipa) + UI.message("Found fallback IPA at: #{ipa_path}") + else + UI.user_error!("No IPA file found after build") + end + end + end + + # Set both environment variable and GitHub Actions output ENV['IPA_OUTPUT_PATH'] = ipa_path - sh("echo 'IPA_OUTPUT_PATH=#{ipa_path}' >> $GITHUB_ENV") # Export for GitHub Actions - else - UI.user_error!("IPA not found after build_ios_app.") + # Set both standard output format and the newer GITHUB_OUTPUT format + sh("echo 'ipa_output_path=#{ipa_path}' >> $GITHUB_OUTPUT") if ENV['GITHUB_OUTPUT'] + sh("echo ::set-output name=ipa_output_path::#{ipa_path}") + + # Also write path to a file that can be read by subsequent steps + ipa_path_file = "#{ipa_directory}/ipa_path.txt" + File.write(ipa_path_file, ipa_path) + UI.success("Saved IPA path to: #{ipa_path_file}") end end -end + + desc "Delete temporary keychain" + lane :delete_temp_keychain do + UI.message("Deleting temporary keychain...") + + delete_keychain( + name: "temp_keychain" + ) if File.exist?(File.expand_path("~/Library/Keychains/temp_keychain-db")) + + UI.message("Temporary keychain deleted successfully.") + end + + # Helper method to determine which iOS version to use + private_lane :determine_ios_version do + # Get available iOS simulator runtimes + runtimes_output = sh("xcrun simctl list runtimes 2>&1", log: false) rescue "" + + if runtimes_output.include?("iOS") + # Extract available iOS versions + ios_versions = runtimes_output.scan(/iOS ([0-9.]+)/) + .flatten + .map { |v| Gem::Version.new(v) } + .sort + .reverse + + if ios_versions.any? + latest_version = ios_versions.first.to_s + UI.success("Found iOS simulator version: #{latest_version}") + latest_version # Implicit return - last expression is returned + else + # Default to a reasonable iOS version if none found + UI.important("No iOS simulator versions found. Using default version.") + "17.6" # Implicit return + end + else + # Default to a reasonable iOS version if no iOS runtimes + UI.important("No iOS simulator runtimes found. Using default version.") + "17.6" # Implicit return + end + end + end # =========================== # Global Lanes diff --git a/fastlane/Matchfile b/fastlane/Matchfile index ba2f5652b..9772ca474 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -3,33 +3,39 @@ # URL of the Git repository to store the certificates git_url(ENV["GIT_URL"]) -# Define the type of match to run, could be one of 'appstore', 'adhoc', 'development', or 'enterprise'. -# For example, use 'appstore' for App Store builds, 'adhoc' for Ad Hoc distribution, -# 'development' for development builds, and 'enterprise' for In-House (enterprise) distribution. -type("appstore") +# Define the type of match to run +# Default to "appstore" but can be overridden +type(ENV["MATCH_TYPE"] || "appstore") -app_identifier(["io.bluewallet.bluewallet", "io.bluewallet.bluewallet.watch", "io.bluewallet.bluewallet.watch.extension", "io.bluewallet.bluewallet.Stickers", "io.bluewallet.bluewallet.MarketWidget"]) # Replace with your app identifiers +# App identifiers for all BlueWallet apps +app_identifier([ + "io.bluewallet.bluewallet", + "io.bluewallet.bluewallet.watch", + "io.bluewallet.bluewallet.watch.extension", + "io.bluewallet.bluewallet.Stickers", + "io.bluewallet.bluewallet.MarketWidget" +]) -# List of app identifiers to create provisioning profiles for. -# Replace with your app's bundle identifier(s). - -# Your Apple Developer account email address. +# Your Apple Developer account email address username(ENV["APPLE_ID"]) -# The ID of your Apple Developer team if you're part of multiple teams +# The ID of your Apple Developer team team_id(ENV["ITC_TEAM_ID"]) -# Set this to true if match should only read existing certificates and profiles -# and not create new ones. -readonly(true) +# Set readonly based on environment (default to true for safety) +# Set to false explicitly when new profiles need to be created +readonly(ENV["MATCH_READONLY"] == "false" ? false : true) -# Optional: The Git branch that is used for match. -# Default is 'master'. - -# Optional: Path to a specific SSH key to be used by match. -# Only needed if you're using a private repository and match needs to use SSH keys for authentication. -# ssh_key("/path/to/your/private/key") - -# Optional: Define the platform to use, can be 'ios', 'macos', or 'tvos'. -# For React Native projects, you'll typically use 'ios'. +# Define the platform to use platform("ios") + +# Git basic authentication through access token +# This is useful for CI/CD environments where SSH keys aren't available +git_basic_authorization(ENV["GIT_ACCESS_TOKEN"]) + +# Storage mode (git by default) +storage_mode("git") + +# Optional: The Git branch that is used for match +# Default is 'master' +# branch("main") diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 99f8763a1..29820960d 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -4,3 +4,4 @@ gem 'fastlane-plugin-browserstack' gem 'fastlane-plugin-bugsnag_sourcemaps_upload' +gem "fastlane-plugin-bugsnag" diff --git a/helpers/screenProtect.ts b/helpers/screenProtect.ts new file mode 100644 index 000000000..f32bb49a1 --- /dev/null +++ b/helpers/screenProtect.ts @@ -0,0 +1,23 @@ +// import { enableSecureView, disableSecureView, forbidAndroidShare, allowAndroidShare } from 'react-native-prevent-screenshot-ios-android'; +// import { Platform } from 'react-native'; +// import { isDesktop } from '../blue_modules/environment'; + +export const enableScreenProtect = () => { + // if (isDesktop) return; + // if (Platform.OS === 'ios') { + // enableSecureView(); + // } else if (Platform.OS === 'android') { + // forbidAndroidShare(); + // } +}; + +export const disableScreenProtect = () => { + // if (isDesktop) return; + // if (Platform.OS === 'ios') { + // disableSecureView(); + // } else if (Platform.OS === 'android') { + // allowAndroidShare(); + // } +}; + +// CURRENTLY UNUSED AS WE WAIT FOR NAV 7 SUPPORT diff --git a/ios/BlueWallet.xcodeproj/project.pbxproj b/ios/BlueWallet.xcodeproj/project.pbxproj index 4cdd85588..aeacd5939 100644 --- a/ios/BlueWallet.xcodeproj/project.pbxproj +++ b/ios/BlueWallet.xcodeproj/project.pbxproj @@ -164,7 +164,7 @@ B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; }; B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; }; B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; }; - C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -428,7 +428,7 @@ files = ( 782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */, 764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */, - C978A716948AB7DEC5B6F677 /* (null) in Frameworks */, + C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */, 17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1584,7 +1584,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Stickers/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIBRARY_SEARCH_PATHS = ( "$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift", @@ -1627,7 +1627,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Stickers/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIBRARY_SEARCH_PATHS = ( "$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 00efe5a69..4975e296c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1297,7 +1297,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-image-picker (7.2.3): + - react-native-image-picker (8.2.0): - DoubleConversion - glog - hermes-engine @@ -1318,10 +1318,29 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-ios-context-menu (1.15.3): - - React-Core - react-native-menu (1.2.1): - React + - react-native-prevent-screenshot-ios-android (1.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-randombytes (3.6.1): - React-Core - react-native-safe-area-context (5.2.0): @@ -1841,7 +1860,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNShare (11.1.0): + - RNShare (12.0.9): - DoubleConversion - glog - hermes-engine @@ -1939,8 +1958,8 @@ DEPENDENCIES: - react-native-bw-file-access (from `../blue_modules/react-native-bw-file-access`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) - - react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`) - "react-native-menu (from `../node_modules/@react-native-menu/menu`)" + - react-native-prevent-screenshot-ios-android (from `../node_modules/react-native-prevent-screenshot-ios-android`) - react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-screen-capture (from `../node_modules/react-native-screen-capture`) @@ -2094,10 +2113,10 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-document-picker" react-native-image-picker: :path: "../node_modules/react-native-image-picker" - react-native-ios-context-menu: - :path: "../node_modules/react-native-ios-context-menu" react-native-menu: :path: "../node_modules/@react-native-menu/menu" + react-native-prevent-screenshot-ios-android: + :path: "../node_modules/react-native-prevent-screenshot-ios-android" react-native-randombytes: :path: "../node_modules/react-native-randombytes" react-native-safe-area-context: @@ -2258,9 +2277,9 @@ SPEC CHECKSUMS: react-native-blue-crypto: de5babd59b17fbf3fc31d2e1e5d59ec859093fbc react-native-bw-file-access: fe925b77dbf48500df0b294c6851f8c84607a203 react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659 - react-native-image-picker: 130fad649d07e4eec8faaed361d3bba570e1e5ff - react-native-ios-context-menu: 986da6dcba70094bcc2a8049f68410fe7d25aff1 + react-native-image-picker: 86f8954a0b8c0f85d56fa1d85ae87936ae74e615 react-native-menu: 2cfe0a3b3c610ed331f00d9f0df300db14ba8692 + react-native-prevent-screenshot-ios-android: 490b2ae701658753e819ca215201f4aa8cab3d53 react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116 react-native-safe-area-context: 3e33e7c43c8b74dba436a5a32651cb8d7064c740 react-native-screen-capture: 7b6121f529681ed2fde36cdedadd0bb39e9a3796 @@ -2313,7 +2332,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: 00ba111b82aa266bb3ee1aa576831c2ea9a9dfad RNReanimated: 66cf0f600a26d2b5e74c6e0b1c77c1ab1f62fc05 RNScreens: b3975354ddafe0fb00112a9054898ccf0d92c78e - RNShare: 6204e6a1987ba3e7c47071ef703e5449a0e3548a + RNShare: 381ed02f6c0dc42b8c24bb5a1e5df0ee5fd91354 RNSVG: a07e14363aa208062c6483bad24a438d5986d490 RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28 RNWatch: 28fe1f5e0c6410d45fd20925f4796fce05522e3f @@ -2323,4 +2342,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: eb430c3fd96af23d4962c4c041798d65efc6843e -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/package-lock.json b/package-lock.json index bf3316759..d50f616b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,10 +26,10 @@ "@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#038a9c9", "@react-native/gradle-plugin": "0.76.7", "@react-native/metro-config": "0.76.7", - "@react-navigation/devtools": "7.0.15", - "@react-navigation/drawer": "7.1.1", - "@react-navigation/native": "7.0.14", - "@react-navigation/native-stack": "7.2.0", + "@react-navigation/devtools": "7.0.16", + "@react-navigation/drawer": "7.1.2", + "@react-navigation/native": "7.0.15", + "@react-navigation/native-stack": "7.2.1", "@rneui/base": "4.0.0-rc.8", "@rneui/themed": "4.0.0-rc.8", "@spsina/bip47": "github:BlueWallet/bip47#df82345", @@ -77,12 +77,12 @@ "react-native-gesture-handler": "2.23.1", "react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4", "react-native-haptic-feedback": "2.3.3", - "react-native-image-picker": "7.2.3", - "react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e", + "react-native-image-picker": "8.2.0", "react-native-keychain": "9.1.0", "react-native-linear-gradient": "2.8.3", "react-native-localize": "3.4.1", "react-native-permissions": "5.2.5", + "react-native-prevent-screenshot-ios-android": "github:BlueWallet/react-native-prevent-screenshot-ios-android#133004e", "react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376", "react-native-push-notification": "8.1.1", "react-native-qrcode-svg": "6.3.12", @@ -94,7 +94,7 @@ "react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f", "react-native-screens": "4.9.1", "react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc", - "react-native-share": "11.1.0", + "react-native-share": "12.0.9", "react-native-svg": "15.11.2", "react-native-tcp-socket": "6.2.0", "react-native-vector-icons": "10.2.0", @@ -2459,12 +2459,6 @@ "bugsnag-source-maps": "bin/cli" } }, - "node_modules/@dominicstop/ts-event-emitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@dominicstop/ts-event-emitter/-/ts-event-emitter-1.1.0.tgz", - "integrity": "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw==", - "license": "MIT" - }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -4701,7 +4695,7 @@ "node_modules/@lodev09/react-native-true-sheet": { "version": "2.0.0", "resolved": "git+ssh://git@github.com/BlueWallet/react-native-true-sheet.git#5945184a2fea9fe5ba8f5cfcdb20e3fc5eed6e37", - "integrity": "sha512-KmqW5bBFowvbZ0d3u960PUx+avPZ80IaRPHszvarI5leeYrkTSUGqEeVVAQnaBHMfl5u6rrOVA4NoJHPsqU0Vg==", + "integrity": "sha512-JoMgC3w8Xgzvb4zHRnBdycBDuhz1O8MuKh9LE3QJjCElcm8x0n3asHzrt+XaLs3XfVrvq/LNyIMQSSYtvvhFKA==", "license": "MIT", "workspaces": [ "example", @@ -6614,12 +6608,12 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.3.1.tgz", - "integrity": "sha512-S3KCGvNsoqVk8ErAtQI2EAhg9185lahF5OY01ofrrD4Ij/uk3QEHHjoGQhR5l5DXSCSKr1JbMQA7MEKMsBiWZA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.4.0.tgz", + "integrity": "sha512-URiDluFl7cLA7GtOAsEvRqPmEMlSSXadqQ3wi9+Dl43dNRMqnoF76WQ9BCXeUPLydJq4yVte9XeMPyD6a0lY1g==", "license": "MIT", "dependencies": { - "@react-navigation/routers": "^7.1.2", + "@react-navigation/routers": "^7.2.0", "escape-string-regexp": "^4.0.0", "nanoid": "3.3.8", "query-string": "^7.1.3", @@ -6638,9 +6632,9 @@ "license": "MIT" }, "node_modules/@react-navigation/devtools": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.15.tgz", - "integrity": "sha512-pxEBVtd6e5ocT7bs6k6ghOJNyb9Fzxm+EYHemHQ53GEin1sQKYpsSHWZEJdFj1cxYp+/+KCT+TueuNDFkJOr4Q==", + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.16.tgz", + "integrity": "sha512-3rWR5TmI+JhxFyJMQXcAEdPumBIThpI1pj0YInON6u8olpL7nD5QGdGXaF71hilPpjID43+tgtV98fcAvhzzOA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6652,18 +6646,18 @@ } }, "node_modules/@react-navigation/drawer": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.1.1.tgz", - "integrity": "sha512-34UqRS5OLFaNXPs5ocz3Du9c7em0P7fFMPYCZn/MxadDzQ4Mn/74pmJczmiyvyvz8vcWsNRbZ3Qswm0Dv6z60w==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.1.2.tgz", + "integrity": "sha512-EIlS5PPzVQ9WJ4xQZytAUTXiVbZcUYfPFlkaAWZHIl7O/suoJIpu52yXwjbjG0XhNu6Vup23W35HJWHxpgKQEQ==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.2.5", + "@react-navigation/elements": "^2.2.6", "color": "^4.2.3", "react-native-drawer-layout": "^4.1.1", "use-latest-callback": "^0.2.1" }, "peerDependencies": { - "@react-navigation/native": "^7.0.14", + "@react-navigation/native": "^7.0.15", "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", @@ -6673,16 +6667,16 @@ } }, "node_modules/@react-navigation/elements": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.2.5.tgz", - "integrity": "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.2.6.tgz", + "integrity": "sha512-UPeaCcEDSDRaxjG+qEHbur6jmNW/f9QNCyPsUt6NMgPEdbB5Z8K8oSx2swIaiCnvUbs/K5G3MuWkqQxGj8QXXA==", "license": "MIT", "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.0.14", + "@react-navigation/native": "^7.0.15", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -6694,12 +6688,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.0.14.tgz", - "integrity": "sha512-Gi6lLw4VOGSWAhmUdJOMauOKGK51/YA1CprjXm91sNfgERWvznqEMw8QmUQx9SEqYfi0LfZhbzpMst09SJ00lw==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.0.15.tgz", + "integrity": "sha512-72PabJJ8VY3GM8i/DCutFW+ATED96ZV6NH+bW+ry1EL0ZFGHoie96H+KzTqktsrUbBw1rd9KXbEQhBQgo0N7iQ==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.3.1", + "@react-navigation/core": "^7.4.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "3.3.8", @@ -6711,16 +6705,16 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.2.0.tgz", - "integrity": "sha512-mw7Nq9qQrGsmJmCTwIIWB7yY/3tWYXvQswx+HJScGAadIjemvytJXm1fcl3+YZ9T9Ym0aERcVe5kDs+ny3X4vA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.2.1.tgz", + "integrity": "sha512-zqC6DVpO4pFZrl+8JuIgV8qk+AGdTuv+hJ5EHePmzs9gYSUrDpw6LahFCiXshwBvi9LinIw9Do7mtnQK2Q8AGA==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.2.5", + "@react-navigation/elements": "^2.2.6", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.0.14", + "@react-navigation/native": "^7.0.15", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -6728,9 +6722,9 @@ } }, "node_modules/@react-navigation/routers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.1.2.tgz", - "integrity": "sha512-emdEjpVDK8zbiu2GChC8oYIAub9i/OpNuQJekVsbyFCBz4/TzaBzms38Q53YaNhdIFNmiYLfHv/Y1Ub7KYfm3w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.2.0.tgz", + "integrity": "sha512-lMyib39H4a//u+eiyt162U6TwCfI8zJbjl9ovjKtDddQ4/Vf7b8/OhyimnJH09N2CBfm4pv0gCV6Q0WnZcfaJg==", "license": "MIT", "dependencies": { "nanoid": "3.3.8" @@ -22257,28 +22251,15 @@ } }, "node_modules/react-native-image-picker": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.2.3.tgz", - "integrity": "sha512-zKIZUlQNU3EtqizsXSH92zPeve4vpUrsqHu2kkpCxWE9TZhJFZBb+irDsBOY8J21k0+Edgt06TMQGJ+iPUIXyA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-8.2.0.tgz", + "integrity": "sha512-jIGllQJuJIn0YKss/JEeb0Kos1HSsnIpU+i3bYxR27sOxSyDZQyP9dKR22olssQPlfH+rGNR/Jc6xKRkhm48vw==", "license": "MIT", "peerDependencies": { "react": "*", "react-native": "*" } }, - "node_modules/react-native-ios-context-menu": { - "version": "1.15.3", - "resolved": "git+ssh://git@github.com/BlueWallet/react-native-ios-context-menu.git#e5c1217cd220bfab6e6d9a7c65838545082e3f8e", - "integrity": "sha512-cQnRYOcP3zGQpCEm7w8oSAKDibX3Ncu8G4xof3mXCbXkqwM00Vdbref8ZJcK9omBY5vwEvyNjFLQ+C/NW47iyQ==", - "license": "MIT", - "dependencies": { - "@dominicstop/ts-event-emitter": "^1.1.0" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-keychain": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-9.1.0.tgz", @@ -22334,6 +22315,18 @@ } } }, + "node_modules/react-native-prevent-screenshot-ios-android": { + "version": "1.1.0", + "resolved": "git+ssh://git@github.com/BlueWallet/react-native-prevent-screenshot-ios-android.git#133004eff4b2e95176ad2a8cc1d6aa61ea43be98", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-prompt-android": { "version": "1.0.0", "resolved": "git+ssh://git@github.com/BlueWallet/react-native-prompt-android.git#ed168d66fed556bc2ed07cf498770f058b78a376", @@ -22482,9 +22475,9 @@ "license": "ISC" }, "node_modules/react-native-share": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-11.1.0.tgz", - "integrity": "sha512-kcpBR90d5//xc8H84HnX6YFeOk4A34mtHz4UEpb7Twbu049KafJwsp4KVVr/SrJwy8W0/Rbe880En9Hq0REamw==", + "version": "12.0.9", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-12.0.9.tgz", + "integrity": "sha512-vSuz/9aF+/AZS3I5NC19MrB56h1Yivk2Yz8lf2d8Szv3KuRw2BnDI/AfCTjMWByJLVYr6xgzfkTkAfvbDGzxLQ==", "license": "MIT", "engines": { "node": ">=16" diff --git a/package.json b/package.json index 1729dbf8e..88f5f18ad 100644 --- a/package.json +++ b/package.json @@ -94,9 +94,10 @@ "@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#038a9c9", "@react-native/gradle-plugin": "0.76.7", "@react-native/metro-config": "0.76.7", - "@react-navigation/drawer": "7.1.1", - "@react-navigation/native": "7.0.14", - "@react-navigation/native-stack": "7.2.0", + "@react-navigation/devtools": "7.0.16", + "@react-navigation/drawer": "7.1.2", + "@react-navigation/native": "7.0.15", + "@react-navigation/native-stack": "7.2.1", "@rneui/base": "4.0.0-rc.8", "@rneui/themed": "4.0.0-rc.8", "@spsina/bip47": "github:BlueWallet/bip47#df82345", @@ -144,12 +145,12 @@ "react-native-gesture-handler": "2.23.1", "react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4", "react-native-haptic-feedback": "2.3.3", - "react-native-image-picker": "7.2.3", - "react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e", + "react-native-image-picker": "8.2.0", "react-native-keychain": "9.1.0", "react-native-linear-gradient": "2.8.3", "react-native-localize": "3.4.1", "react-native-permissions": "5.2.5", + "react-native-prevent-screenshot-ios-android": "github:BlueWallet/react-native-prevent-screenshot-ios-android#133004e", "react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376", "react-native-push-notification": "8.1.1", "react-native-qrcode-svg": "6.3.12", @@ -157,12 +158,11 @@ "react-native-randombytes": "3.6.1", "react-native-rate": "1.2.12", "react-native-reanimated": "3.16.7", - "@react-navigation/devtools": "7.0.15", "react-native-safe-area-context": "5.2.0", "react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f", "react-native-screens": "4.9.1", "react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc", - "react-native-share": "11.1.0", + "react-native-share": "12.0.9", "react-native-svg": "15.11.2", "react-native-tcp-socket": "6.2.0", "react-native-vector-icons": "10.2.0", diff --git a/screen/send/create.js b/screen/send/create.js index d936be714..f3e22eb66 100644 --- a/screen/send/create.js +++ b/screen/send/create.js @@ -9,17 +9,16 @@ import { Icon } from '@rneui/themed'; import RNFS from 'react-native-fs'; import { PERMISSIONS, request, RESULTS } from 'react-native-permissions'; import Share from 'react-native-share'; - import { satoshiToBTC } from '../../blue_modules/currency'; import { isDesktop } from '../../blue_modules/environment'; import { BlueSpacing20, BlueText } from '../../BlueComponents'; import presentAlert from '../../components/Alert'; import { DynamicQRCode } from '../../components/DynamicQRCode'; import { useTheme } from '../../components/themes'; -import { disallowScreenshot } from 'react-native-screen-capture'; import loc from '../../loc'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import { useSettings } from '../../hooks/context/useSettings'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; const SendCreate = () => { const { fee, recipients, memo = '', satoshiPerByte, psbt, showAnimatedQr, tx } = useRoute().params; @@ -49,9 +48,11 @@ const SendCreate = () => { useEffect(() => { console.log('send/create - useEffect'); - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } return () => { - if (!isDesktop) disallowScreenshot(false); + disableScreenProtect(); }; }, [isPrivacyBlurEnabled]); diff --git a/screen/wallets/ExportMultisigCoordinationSetup.tsx b/screen/wallets/ExportMultisigCoordinationSetup.tsx index 8e3369821..a00f21c68 100644 --- a/screen/wallets/ExportMultisigCoordinationSetup.tsx +++ b/screen/wallets/ExportMultisigCoordinationSetup.tsx @@ -7,12 +7,11 @@ import { DynamicQRCode } from '../../components/DynamicQRCode'; import SaveFileButton from '../../components/SaveFileButton'; import { SquareButton } from '../../components/SquareButton'; import { useTheme } from '../../components/themes'; -import { disallowScreenshot } from 'react-native-screen-capture'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; import { ExportMultisigCoordinationSetupStackRootParamList } from '../../navigation/ExportMultisigCoordinationSetupStack'; import { useSettings } from '../../hooks/context/useSettings'; -import { isDesktop } from '../../blue_modules/environment'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; const enum ActionType { SET_LOADING = 'SET_LOADING', @@ -102,7 +101,6 @@ const ExportMultisigCoordinationSetup: React.FC = () => { dispatch({ type: ActionType.SET_LOADING, isLoading: true }); const task = InteractionManager.runAfterInteractions(() => { - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); if (wallet) { setTimeout(async () => { try { @@ -128,12 +126,22 @@ const ExportMultisigCoordinationSetup: React.FC = () => { return () => { task.cancel(); - if (!isDesktop) disallowScreenshot(false); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletID]), ); + useFocusEffect( + useCallback(() => { + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } + return () => { + disableScreenProtect(); + }; + }, [isPrivacyBlurEnabled]), + ); + useFocusEffect( useCallback(() => { if (closeButtonState) { diff --git a/screen/wallets/ImportWallet.tsx b/screen/wallets/ImportWallet.tsx index 19c2c1778..8450b1a1a 100644 --- a/screen/wallets/ImportWallet.tsx +++ b/screen/wallets/ImportWallet.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { RouteProp, useRoute } from '@react-navigation/native'; import Clipboard from '@react-native-clipboard/clipboard'; import { Keyboard, Platform, ScrollView, StyleSheet, TouchableWithoutFeedback, View, TouchableOpacity, Image } from 'react-native'; -import { disallowScreenshot } from 'react-native-screen-capture'; import { BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from '../../BlueComponents'; import Button from '../../components/Button'; import { @@ -18,8 +17,8 @@ import loc from '../../loc'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { isDesktop } from '../../blue_modules/environment'; import { AddressInputScanButton } from '../../components/AddressInputScanButton'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; type RouteProps = RouteProp; type NavigationProps = NativeStackNavigationProp; @@ -156,9 +155,11 @@ const ImportWallet = () => { ); useEffect(() => { - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } return () => { - if (!isDesktop) disallowScreenshot(false); + disableScreenProtect(); }; }, [isPrivacyBlurEnabled]); diff --git a/screen/wallets/ImportWalletDiscovery.tsx b/screen/wallets/ImportWalletDiscovery.tsx index 07ddc72bb..dbedcb0c3 100644 --- a/screen/wallets/ImportWalletDiscovery.tsx +++ b/screen/wallets/ImportWalletDiscovery.tsx @@ -16,10 +16,9 @@ import { useStorage } from '../../hooks/context/useStorage'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { THDWalletForWatchOnly, TWallet } from '../../class/wallets/types'; -import { keepAwake, disallowScreenshot } from 'react-native-screen-capture'; import { useSettings } from '../../hooks/context/useSettings'; -import { isDesktop } from '../../blue_modules/environment'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; type RouteProps = RouteProp; type NavigationProp = NativeStackNavigationProp; @@ -39,7 +38,7 @@ const ImportWalletDiscovery: React.FC = () => { const { colors } = useTheme(); const route = useRoute(); const { importText, askPassphrase, searchAccounts } = route.params; - const { isElectrumDisabled } = useSettings(); + const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings(); const task = useRef(null); const { addAndSaveWallet } = useStorage(); const [loading, setLoading] = useState(true); @@ -115,7 +114,6 @@ const ImportWalletDiscovery: React.FC = () => { } }; - if (!isDesktop) keepAwake(true); task.current = startImport(importText, askPassphrase, searchAccounts, isElectrumDisabled, onProgress, onWallet, onPassword); task.current.promise @@ -134,22 +132,24 @@ const ImportWalletDiscovery: React.FC = () => { .finally(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setLoading(false); - if (!isDesktop) keepAwake(false); }); return () => { - if (!isDesktop) keepAwake(false); task.current?.stop(); }; }, [askPassphrase, importText, isElectrumDisabled, navigation, saveWallet, searchAccounts]); + useEffect(() => { + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } + return () => { + disableScreenProtect(); + }; + }, [isPrivacyBlurEnabled]); + const handleCustomDerivation = () => { task.current?.stop(); - if (!isDesktop) { - keepAwake(false); - disallowScreenshot(false); - } - navigation.navigate('ImportCustomDerivationPath', { importText, password }); }; diff --git a/screen/wallets/PleaseBackup.tsx b/screen/wallets/PleaseBackup.tsx index 73a930a5d..0dbf74b55 100644 --- a/screen/wallets/PleaseBackup.tsx +++ b/screen/wallets/PleaseBackup.tsx @@ -1,17 +1,16 @@ -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { RouteProp, useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import React, { useCallback, useEffect } from 'react'; import { BackHandler, I18nManager, ScrollView, StyleSheet, Text, View } from 'react-native'; -import { disallowScreenshot } from 'react-native-screen-capture'; import Button from '../../components/Button'; import { useTheme } from '../../components/themes'; import { useSettings } from '../../hooks/context/useSettings'; import { useStorage } from '../../hooks/context/useStorage'; import loc from '../../loc'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; -import { isDesktop } from '../../blue_modules/environment'; import SeedWords from '../../components/SeedWords'; +import { disableScreenProtect, enableScreenProtect } from '../../helpers/screenProtect'; type RouteProps = RouteProp; type NavigationProp = NativeStackNavigationProp; @@ -34,21 +33,27 @@ const PleaseBackup: React.FC = () => { }); const handleBackButton = useCallback(() => { - // @ts-ignore: Ignore navigation.getParent()?.goBack(); return true; }, [navigation]); useEffect(() => { BackHandler.addEventListener('hardwareBackPress', handleBackButton); - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); return () => { BackHandler.removeEventListener('hardwareBackPress', handleBackButton); - if (!isDesktop) disallowScreenshot(false); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useFocusEffect( + useCallback(() => { + if (isPrivacyBlurEnabled) enableScreenProtect(); + return () => { + disableScreenProtect(); + }; + }, [isPrivacyBlurEnabled]), + ); + return ( { // useFocusEffect is called on willAppear (example: when camera dismisses). we want to avoid this. if (hasLoaded.current) return; setIsLoading(true); - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); + if (isPrivacyBlurEnabled) enableScreenProtect(); const task = InteractionManager.runAfterInteractions(async () => { if (!w.current) { @@ -207,7 +207,7 @@ const ViewEditMultisigCosigners: React.FC = () => { setIsLoading(false); }); return () => { - if (!isDesktop) disallowScreenshot(false); + disableScreenProtect(); task.cancel(); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/screen/wallets/WalletAddresses.tsx b/screen/wallets/WalletAddresses.tsx index 4a1850369..c43ec430e 100644 --- a/screen/wallets/WalletAddresses.tsx +++ b/screen/wallets/WalletAddresses.tsx @@ -1,10 +1,9 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useReducer, useMemo } from 'react'; -import { useFocusEffect, useRoute, RouteProp } from '@react-navigation/native'; +import { useRoute, RouteProp, useFocusEffect } from '@react-navigation/native'; import { ActivityIndicator, FlatList, StyleSheet, View, Platform, UIManager } from 'react-native'; import { WatchOnlyWallet } from '../../class'; import { AddressItem } from '../../components/addresses/AddressItem'; import { useTheme } from '../../components/themes'; -import { disallowScreenshot } from 'react-native-screen-capture'; import { useStorage } from '../../hooks/context/useStorage'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; @@ -12,7 +11,8 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import SegmentedControl from '../../components/SegmentControl'; import loc from '../../loc'; import { BitcoinUnit } from '../../models/bitcoinUnits'; -import { isDesktop } from '../../blue_modules/environment'; +import { useSettings } from '../../hooks/context/useSettings'; +import { disableScreenProtect, enableScreenProtect } from '../../helpers/screenProtect'; export const TABS = { EXTERNAL: 'receive', @@ -131,6 +131,7 @@ const WalletAddresses: React.FC = () => { const allowSignVerifyMessage = (wallet && 'allowSignVerifyMessage' in wallet && wallet.allowSignVerifyMessage()) ?? false; const { colors } = useTheme(); + const { isPrivacyBlurEnabled } = useSettings(); const { setOptions } = useExtendedNavigation(); const stylesHook = StyleSheet.create({ @@ -139,6 +140,15 @@ const WalletAddresses: React.FC = () => { }, }); + useFocusEffect( + useCallback(() => { + if (isPrivacyBlurEnabled) enableScreenProtect(); + return () => { + disableScreenProtect(); + }; + }, [isPrivacyBlurEnabled]), + ); + const getAddresses = useMemo(() => { if (!walletInstance) return []; const newAddresses: Address[] = []; @@ -177,15 +187,6 @@ const WalletAddresses: React.FC = () => { }); }, [setOptions]); - useFocusEffect( - useCallback(() => { - if (!isDesktop) disallowScreenshot(true); - return () => { - if (!isDesktop) disallowScreenshot(false); - }; - }, []), - ); - const data = search.length > 0 ? filteredAddresses.filter(item => item.address.toLowerCase().includes(search.toLowerCase())) : filteredAddresses; diff --git a/screen/wallets/WalletExport.tsx b/screen/wallets/WalletExport.tsx index 374ecc909..b6ff96eeb 100644 --- a/screen/wallets/WalletExport.tsx +++ b/screen/wallets/WalletExport.tsx @@ -1,10 +1,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Clipboard from '@react-native-clipboard/clipboard'; -import { RouteProp, useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { Icon } from '@rneui/themed'; -import { ActivityIndicator, InteractionManager, LayoutChangeEvent, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; -import { disallowScreenshot } from 'react-native-screen-capture'; - +import { LayoutChangeEvent, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; import { validateMnemonic } from '../../blue_modules/bip39'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { BlueText } from '../../BlueComponents'; @@ -15,7 +14,6 @@ import SeedWords from '../../components/SeedWords'; import { useTheme } from '../../components/themes'; import { HandOffActivityType } from '../../components/types'; import { useSettings } from '../../hooks/context/useSettings'; -import { isDesktop } from '../../blue_modules/environment'; import { useStorage } from '../../hooks/context/useStorage'; import useAppState from '../../hooks/useAppState'; import loc from '../../loc'; @@ -57,9 +55,8 @@ const DoNotDisclose: React.FC = () => { }; const WalletExport: React.FC = () => { - const { wallets, saveToDisk } = useStorage(); + const { wallets } = useStorage(); const { walletID } = useRoute().params; - const [isLoading, setIsLoading] = useState(true); const navigation = useNavigation(); const { isPrivacyBlurEnabled } = useSettings(); const { colors } = useTheme(); @@ -85,28 +82,20 @@ const WalletExport: React.FC = () => { }, [wallet]); useEffect(() => { - if (!isLoading && previousAppState === 'active' && currentAppState !== 'active') { + if (previousAppState === 'active' && currentAppState !== 'active') { const timer = setTimeout(() => navigation.goBack(), 500); return () => clearTimeout(timer); } - }, [currentAppState, previousAppState, navigation, isLoading]); + }, [currentAppState, previousAppState, navigation]); - useFocusEffect( - useCallback(() => { - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); - const task = InteractionManager.runAfterInteractions(async () => { - if (!wallet.getUserHasSavedExport()) { - wallet.setUserHasSavedExport(true); - saveToDisk(); - } - setIsLoading(false); - }); - return () => { - if (!isDesktop) disallowScreenshot(false); - task.cancel(); - }; - }, [isPrivacyBlurEnabled, wallet, saveToDisk]), - ); + useEffect(() => { + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } + return () => { + disableScreenProtect(); + }; + }, [isPrivacyBlurEnabled]); const onLayout = useCallback((e: LayoutChangeEvent) => { const { height, width } = e.nativeEvent.layout; @@ -128,22 +117,13 @@ const WalletExport: React.FC = () => { contentContainerStyle={styles.scrollViewContent} onLayout={onLayout} testID="WalletExportScroll" - centerContent={isLoading} > {children} ), - [isLoading, onLayout, stylesHook.root], + [onLayout, stylesHook.root], ); - if (isLoading) { - return ( - - - - ); - } - // for SLIP39 if (secrets.length !== 1) { return ( @@ -177,7 +157,6 @@ const WalletExport: React.FC = () => { contentContainerStyle={styles.scrollViewContent} onLayout={onLayout} testID="WalletExportScroll" - centerContent={isLoading} > {wallet.type !== WatchOnlyWallet.type && } diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index 9f2840443..d2f26d22b 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -290,10 +290,10 @@ const WalletTransactions: React.FC = ({ route }) => { const renderItem = useCallback( // eslint-disable-next-line react/no-unused-prop-types - ({ item }: { item: Transaction }) => { - return ; - }, - [wallet, walletID], + ({ item }: { item: Transaction }) => ( + + ), + [wallet?.preferredBalanceUnit, walletID], ); const choosePhoto = () => { @@ -310,7 +310,7 @@ const WalletTransactions: React.FC = ({ route }) => { }); }; - const _keyExtractor = (_item: any, index: number) => index.toString(); + const _keyExtractor = useCallback((_item: any, index: number) => index.toString(), []); const pasteFromClipboard = async () => { onBarCodeRead({ data: await getClipboardContent() }); @@ -391,6 +391,7 @@ const WalletTransactions: React.FC = ({ route }) => { }); return () => { task.cancel(); + console.debug('Next screen is focused, clearing reloadTransactionsMenuActionFunction'); setReloadTransactionsMenuActionFunction(() => {}); }; }, [setReloadTransactionsMenuActionFunction, refreshTransactions]), @@ -519,7 +520,7 @@ const WalletTransactions: React.FC = ({ route }) => { getItemLayout={getItemLayout} - updateCellsBatchingPeriod={30} + updateCellsBatchingPeriod={50} onEndReachedThreshold={0.3} onEndReached={loadMoreTransactions} ListFooterComponent={renderListFooterComponent} @@ -531,7 +532,7 @@ const WalletTransactions: React.FC = ({ route }) => { removeClippedSubviews contentContainerStyle={{ backgroundColor: colors.background }} contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }} - maxToRenderPerBatch={15} + maxToRenderPerBatch={10} onScroll={handleScroll} scrollEventThrottle={16} stickyHeaderHiddenOnScroll @@ -549,6 +550,10 @@ const WalletTransactions: React.FC = ({ route }) => { refreshTransactions(true)} tintColor={colors.msSuccessCheck} /> ) : undefined } + windowSize={15} + maintainVisibleContentPosition={{ + minIndexForVisible: 0, + }} /> {wallet?.allowReceive() && ( diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 69c1f795f..23b7c37c0 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -260,7 +260,7 @@ const WalletsList: React.FC = () => { const renderTransactionListsRow = useCallback( (item: ExtendedTransaction) => ( - + ), [], @@ -358,9 +358,9 @@ const WalletsList: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [scanImage, wallets.length]); - const sectionListKeyExtractor = (item: any, index: any) => { + const sectionListKeyExtractor = useCallback((item: any, index: any) => { return `${item}${index}}`; - }; + }, []); const onScanButtonPressed = useCallback(() => { navigation.navigate('ScanQRCode', { @@ -421,6 +421,15 @@ const WalletsList: React.FC = () => { { key: WalletsListSections.TRANSACTIONS, data: dataSource }, ]; + const getItemLayout = useCallback( + (data: any, index: number) => ({ + length: 80, // Approximate height of each item + offset: 80 * index, + index, + }), + [], + ); + return ( @@ -439,6 +448,7 @@ const WalletsList: React.FC = () => { windowSize={21} maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} + getItemLayout={getItemLayout} /> {renderScanButton()} diff --git a/screen/wallets/addMultisigStep2.js b/screen/wallets/addMultisigStep2.js index 698192fa1..d5453f435 100644 --- a/screen/wallets/addMultisigStep2.js +++ b/screen/wallets/addMultisigStep2.js @@ -26,14 +26,12 @@ import QRCodeComponent from '../../components/QRCodeComponent'; import { useTheme } from '../../components/themes'; import confirm from '../../helpers/confirm'; import prompt from '../../helpers/prompt'; -import { disallowScreenshot } from 'react-native-screen-capture'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import ToolTipMenu from '../../components/TooltipMenu'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { useSettings } from '../../hooks/context/useSettings'; -import { isDesktop } from '../../blue_modules/environment'; import { useKeyboard } from '../../hooks/useKeyboard'; import { DoneAndDismissKeyboardInputAccessory, @@ -45,6 +43,7 @@ import MultipleStepsListItem, { MultipleStepsListItemDashType, } from '../../components/MultipleStepsListItem'; import { AddressInputScanButton } from '../../components/AddressInputScanButton'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; const staticCache = {}; @@ -72,9 +71,11 @@ const WalletsAddMultisigStep2 = () => { useFocusEffect( useCallback(() => { - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } return () => { - if (!isDesktop) disallowScreenshot(false); + disableScreenProtect(); }; }, [isPrivacyBlurEnabled]), ); diff --git a/screen/wallets/pleaseBackupLNDHub.js b/screen/wallets/pleaseBackupLNDHub.js index 54c6b8bec..8f23e398b 100644 --- a/screen/wallets/pleaseBackupLNDHub.js +++ b/screen/wallets/pleaseBackupLNDHub.js @@ -7,11 +7,10 @@ import CopyTextToClipboard from '../../components/CopyTextToClipboard'; import QRCodeComponent from '../../components/QRCodeComponent'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; -import { disallowScreenshot } from 'react-native-screen-capture'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; import { useSettings } from '../../hooks/context/useSettings'; -import { isDesktop } from '../../blue_modules/environment'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; const PleaseBackupLNDHub = () => { const { wallets } = useStorage(); @@ -44,10 +43,12 @@ const PleaseBackupLNDHub = () => { }); useEffect(() => { - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); + if (isPrivacyBlurEnabled) { + enableScreenProtect(); + } BackHandler.addEventListener('hardwareBackPress', handleBackButton); return () => { - if (!isDesktop) disallowScreenshot(false); + disableScreenProtect(); BackHandler.removeEventListener('hardwareBackPress', handleBackButton); }; }, [handleBackButton, isPrivacyBlurEnabled]); diff --git a/screen/wallets/xpub.tsx b/screen/wallets/xpub.tsx index eac6a1f55..004211d0d 100644 --- a/screen/wallets/xpub.tsx +++ b/screen/wallets/xpub.tsx @@ -8,13 +8,12 @@ import CopyTextToClipboard from '../../components/CopyTextToClipboard'; import HandOffComponent from '../../components/HandOffComponent'; import QRCodeComponent from '../../components/QRCodeComponent'; import SafeArea from '../../components/SafeArea'; -import { disallowScreenshot } from 'react-native-screen-capture'; +import { enableScreenProtect, disableScreenProtect } from '../../helpers/screenProtect'; import loc from '../../loc'; import { styles, useDynamicStyles } from './xpub.styles'; import { useStorage } from '../../hooks/context/useStorage'; import { HandOffActivityType } from '../../components/types'; import { useSettings } from '../../hooks/context/useSettings'; -import { isDesktop } from '../../blue_modules/environment'; type WalletXpubRouteProp = RouteProp<{ params: { walletID: string; xpub: string } }, 'params'>; export type RootStackParamList = { @@ -39,7 +38,7 @@ const WalletXpub: React.FC = () => { useFocusEffect( useCallback(() => { - if (!isDesktop) disallowScreenshot(isPrivacyBlurEnabled); + if (isPrivacyBlurEnabled) enableScreenProtect(); // Skip execution if walletID hasn't changed if (lastWalletIdRef.current === walletID) { return; @@ -58,7 +57,7 @@ const WalletXpub: React.FC = () => { }); lastWalletIdRef.current = walletID; return () => { - if (!isDesktop) disallowScreenshot(false); + disableScreenProtect(); task.cancel(); }; }, [isPrivacyBlurEnabled, walletID, wallet, xpub, navigation]), diff --git a/tests/setup.js b/tests/setup.js index 16ba6217e..b41ce83a6 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -229,10 +229,6 @@ jest.mock('realm', () => { }; }); -jest.mock('react-native-ios-context-menu', () => { - return {}; -}); - jest.mock('rn-qr-generator', () => ({ detect: jest.fn(uri => { if (uri === 'invalid-image') {