Merge branch 'master' into clip

This commit is contained in:
Marcos Rodriguez Velez 2024-10-18 01:14:40 -04:00
commit 9890d5a7cf
47 changed files with 1937 additions and 1813 deletions

View file

@ -17,17 +17,17 @@ jobs:
outputs:
new_build_number: ${{ steps.generate_build_number.outputs.build_number }}
project_version: ${{ steps.determine_marketing_version.outputs.project_version }}
ipa_output_path: ${{ steps.build_app.outputs.ipa_output_path }}
latest_commit_message: ${{ steps.get_latest_commit_message.outputs.commit_message }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
steps:
- name: Checkout project
- name: Checkout Project
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetches all history
- name: Specify node version
- name: Specify Node.js Version
uses: actions/setup-node@v4
with:
node-version: 20
@ -36,46 +36,69 @@ jobs:
with:
xcode-version: 16.0
- name: Set up Ruby
- name: Set Up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Install dependencies with Bundler
- name: Install Dependencies with Bundler
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3 --quiet
- name: Install node_modules
- name: Install Node Modules
run: npm install --omit=dev --yes
- name: Install CocoaPods Dependencies
run: |
bundle exec fastlane ios install_pods
- name: Cache CocoaPods Pods
uses: actions/cache@v4
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- name: Display release-notes.txt
run: cat release-notes.txt
restore-keys: |
${{ runner.os }}-pods-
- name: Get Latest Commit Message
id: get_latest_commit_message
run: |
LATEST_COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s")
echo "LATEST_COMMIT_MESSAGE=${LATEST_COMMIT_MESSAGE}" >> $GITHUB_ENV
echo "::set-output name=commit_message::$LATEST_COMMIT_MESSAGE"
- name: Set up Git Authentication
echo "commit_message=$LATEST_COMMIT_MESSAGE" >> $GITHUB_OUTPUT
- name: Generate Build Number Based on Timestamp
id: generate_build_number
run: |
NEW_BUILD_NUMBER=$(date +%s)
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
echo "build_number=$NEW_BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: Set Build Number
run: bundle exec fastlane ios increment_build_number_lane
- name: Determine Marketing Version
id: determine_marketing_version
run: |
MARKETING_VERSION=$(grep MARKETING_VERSION BlueWallet.xcodeproj/project.pbxproj | awk -F '= ' '{print $2}' | tr -d ' ;' | head -1)
echo "PROJECT_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "project_version=$MARKETING_VERSION" >> $GITHUB_OUTPUT
working-directory: ios
- name: Set Up Git Authentication
env:
ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
run: |
git config --global credential.helper 'cache --timeout=3600'
git config --global http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${ACCESS_TOKEN} | base64)"
- name: Create Temporary Keychain
run: bundle exec fastlane ios create_temp_keychain
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Setup Provisioning Profiles
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
@ -86,14 +109,16 @@ jobs:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
bundle exec fastlane ios setup_provisioning_profiles
- name: Cache Provisioning Profiles
id: cache_provisioning_profiles
uses: actions/cache@v4
with:
path: ~/Library/MobileDevice/Provisioning Profiles
path: ~/Library/MobileDevice/Provisioning\ Profiles
key: ${{ runner.os }}-provisioning-profiles-${{ github.sha }}
restore-keys: |
${{ runner.os }}-provisioning-profiles-
- name: Check Cache Status for Provisioning Profiles
run: |
if [ -n "${{ steps.cache_provisioning_profiles.outputs.cache-hit }}" ]; then
@ -101,40 +126,35 @@ jobs:
else
echo "No cache found for provisioning profiles. A new cache will be created."
fi
- name: Verify Provisioning Profiles Exist
run: |
if [ -d "~/Library/MobileDevice/Provisioning Profiles" ]; then
echo "Provisioning profiles are available in the cache."
ls -la ~/Library/MobileDevice/Provisioning Profiles
ls -la ~/Library/MobileDevice/Provisioning\ Profiles
else
echo "Provisioning profiles directory does not exist."
fi
- name: Generate Build Number based on timestamp
id: generate_build_number
run: |
NEW_BUILD_NUMBER=$(date +%s)
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
echo "::set-output name=build_number::$NEW_BUILD_NUMBER"
- name: Set Build Number
run: bundle exec fastlane ios increment_build_number_lane
- name: Determine Marketing Version
id: determine_marketing_version
run: |
MARKETING_VERSION=$(grep MARKETING_VERSION BlueWallet.xcodeproj/project.pbxproj | awk -F '= ' '{print $2}' | tr -d ' ;' | head -1)
echo "PROJECT_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "::set-output name=project_version::$MARKETING_VERSION"
working-directory: ios
- name: Expected IPA file name
run: |
echo "IPA file name: BlueWallet_${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa"
- name: Build App
run: bundle exec fastlane ios build_app_lane
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
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: build_logs
path: ./ios/build_logs/
- name: Upload IPA as Artifact
if: success()
uses: actions/upload-artifact@v4
with:
name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
path: ./build/BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
path: ${{ env.IPA_OUTPUT_PATH }} # Directly from Fastfile `IPA_OUTPUT_PATH`
testflight-upload:
needs: build
@ -146,13 +166,15 @@ jobs:
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
steps:
- name: Checkout project
- name: Checkout Project
uses: actions/checkout@v4
- name: Set up Ruby
- name: Set Up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Cache Ruby Gems
uses: actions/cache@v4
with:
@ -160,18 +182,42 @@ jobs:
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Install dependencies with Bundler
- name: Install Dependencies with Bundler
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3 --quiet
- 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
path: ./
path: ./ # Download the IPA file to the current working directory
- 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
- name: Verify IPA Path Before Upload
run: |
if [ ! -f "$IPA_OUTPUT_PATH" ]; then
echo "IPA file not found at path: $IPA_OUTPUT_PATH"
exit 1
fi
- name: Upload to TestFlight
run: |
ls -la $IPA_OUTPUT_PATH
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 }}
@ -182,7 +228,8 @@ 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 }}
run: bundle exec fastlane ios upload_to_testflight_lane
IPA_OUTPUT_PATH: ${{ env.IPA_OUTPUT_PATH }}
- name: Post PR Comment
if: success() && github.event_name == 'pull_request'
uses: actions/github-script@v6

View file

@ -24,20 +24,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.973.0)
aws-sdk-core (3.204.0)
aws-partitions (1.989.0)
aws-sdk-core (3.209.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.90.0)
aws-sdk-core (~> 3, >= 3.203.0)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.161.0)
aws-sdk-core (~> 3, >= 3.203.0)
aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@ -96,8 +96,8 @@ GEM
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
excon (0.111.0)
faraday (1.10.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -123,10 +123,10 @@ GEM
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.222.0)
fastlane (2.224.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -214,16 +214,17 @@ GEM
http-cookie (1.0.7)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.2)
jwt (2.9.3)
base64
logger (1.6.1)
mime-types (3.5.2)
mime-types (3.6.0)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2024.0903)
mime-types-data (3.2024.1001)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.1)
@ -250,7 +251,7 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.3.7)
rexml (3.3.8)
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
@ -278,15 +279,15 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unicode-display_width (2.5.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.25.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.2, < 4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)

View file

@ -95,11 +95,23 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- Main launcher intent filter -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Intent filter for opening the app only when .psbt files are selected -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" android:mimeType="application/octet-stream" android:pathPattern=".*\\.psbt" />
</intent-filter>
<!-- Intent filter for other custom schemes (bitcoin, bluewallet, etc.) -->
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -111,7 +123,16 @@
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
<!-- Intent filter for importing other file types (but not launching the app) -->
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" android:mimeType="application/octet-stream" android:pathPattern=".*\\.*" />
</intent-filter>
<!-- Intent filter for handling PSBT files -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
@ -119,7 +140,7 @@
android:mimeType="application/octet-stream"
android:pathPattern=".*\\.psbt" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />

View file

@ -299,6 +299,7 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
return;
}
presentAlert({
allowRepeat: false,
title: loc.errors.network,
message: loc.formatString(
usingPeer ? loc.settings.electrum_unable_to_connect : loc.settings.electrum_error_connect,

View file

@ -147,7 +147,7 @@ function Notifications(props) {
ActionSheet.showActionSheetWithOptions(
{
title: loc.settings.notifications,
message: loc.notifications.would_you_like_to_receive_notifications,
message: `${loc.notifications.would_you_like_to_receive_notifications}\n${loc.settings.push_notifications_explanation}`,
options,
cancelButtonIndex: 0, // Assuming 'no and don't ask' is still treated as the cancel action
anchor: anchor ? findNodeHandle(anchor.current) : undefined,

View file

@ -1,4 +1,4 @@
import { Alert as RNAlert, Platform, ToastAndroid } from 'react-native';
import { Alert as RNAlert, Platform, ToastAndroid, AlertButton, AlertOptions } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import loc from '../loc';
@ -7,16 +7,6 @@ export enum AlertType {
Toast,
}
interface AlertButton {
text: string;
onPress?: () => void;
style?: 'default' | 'cancel' | 'destructive';
}
interface AlertOptions {
cancelable?: boolean;
}
const presentAlert = (() => {
let lastAlertParams: {
title?: string;
@ -31,6 +21,14 @@ const presentAlert = (() => {
lastAlertParams = null;
};
const showAlert = (title: string | undefined, message: string, buttons: AlertButton[], options: AlertOptions) => {
if (Platform.OS === 'ios') {
RNAlert.alert(title ?? message, title && message ? message : undefined, buttons, options);
} else {
RNAlert.alert(title ?? '', message, buttons, options);
}
};
return ({
title,
message,
@ -38,6 +36,7 @@ const presentAlert = (() => {
hapticFeedback,
buttons = [],
options = { cancelable: false },
allowRepeat = true,
}: {
title?: string;
message: string;
@ -45,45 +44,38 @@ const presentAlert = (() => {
hapticFeedback?: HapticFeedbackTypes;
buttons?: AlertButton[];
options?: AlertOptions;
allowRepeat?: boolean;
}) => {
if (
lastAlertParams &&
lastAlertParams.title === title &&
lastAlertParams.message === message &&
lastAlertParams.type === type &&
lastAlertParams.hapticFeedback === hapticFeedback &&
JSON.stringify(lastAlertParams.buttons) === JSON.stringify(buttons) &&
JSON.stringify(lastAlertParams.options) === JSON.stringify(options)
) {
return; // Skip showing the alert if the content is the same as the last one
const currentAlertParams = { title, message, type, hapticFeedback, buttons, options };
if (!allowRepeat && lastAlertParams && JSON.stringify(lastAlertParams) === JSON.stringify(currentAlertParams)) {
return;
}
lastAlertParams = { title, message, type, hapticFeedback, buttons, options };
if (JSON.stringify(lastAlertParams) !== JSON.stringify(currentAlertParams)) {
clearCache();
}
lastAlertParams = currentAlertParams;
if (hapticFeedback) {
triggerHapticFeedback(hapticFeedback);
}
// Ensure that there's at least one button (required for both iOS and Android)
const wrappedButtons =
buttons.length > 0
? buttons
: [
{
text: loc._.ok,
onPress: () => {},
},
];
const wrappedButtons: AlertButton[] = buttons.length > 0 ? buttons : [{ text: loc._.ok, onPress: () => {}, style: 'default' }];
switch (type) {
case AlertType.Toast:
if (Platform.OS === 'android') {
ToastAndroid.show(message, ToastAndroid.LONG);
clearCache();
} else {
// For iOS, treat Toast as a normal alert
showAlert(title, message, wrappedButtons, options);
}
break;
default:
RNAlert.alert(title ?? message, title && message ? message : undefined, wrappedButtons, options);
showAlert(title, message, wrappedButtons, options);
break;
}
};

View file

@ -253,7 +253,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]);
const handleOnViewOnBlockExplorer = useCallback(() => {
const url = `${selectedBlockExplorer}/tx/${item.hash}`;
const url = `${selectedBlockExplorer.url}/tx/${item.hash}`;
Linking.canOpenURL(url).then(supported => {
if (supported) {
Linking.openURL(url);
@ -261,7 +261,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
});
}, [item.hash, selectedBlockExplorer]);
const handleCopyOpenInBlockExplorerPress = useCallback(() => {
Clipboard.setString(`${selectedBlockExplorer}/tx/${item.hash}`);
Clipboard.setString(`${selectedBlockExplorer.url}/tx/${item.hash}`);
}, [item.hash, selectedBlockExplorer]);
const onToolTipPress = useCallback(

View file

@ -1,21 +1,30 @@
# Define app identifiers once for reuse across lanes
def app_identifiers
["io.bluewallet.bluewallet", "io.bluewallet.bluewallet.watch", "io.bluewallet.bluewallet.watch.extension", "io.bluewallet.bluewallet.Stickers", "io.bluewallet.bluewallet.MarketWidget"]
[
"io.bluewallet.bluewallet",
"io.bluewallet.bluewallet.watch",
"io.bluewallet.bluewallet.watch.extension",
"io.bluewallet.bluewallet.Stickers",
"io.bluewallet.bluewallet.MarketWidget"
]
end
default_platform(:android)
project_root = File.expand_path("..", __dir__)
# ===========================
# Android Lanes
# ===========================
platform :android do
desc "Prepare the keystore file"
lane :prepare_keystore do
Dir.chdir(project_root) do
keystore_file_hex = ENV['KEYSTORE_FILE_HEX']
UI.user_error!("KEYSTORE_FILE_HEX environment variable is missing") if keystore_file_hex.nil?
Dir.chdir("android") do
UI.message("Creating keystore hex file...")
UI.message("Creating keystore from HEX...")
File.write("bluewallet-release-key.keystore.hex", keystore_file_hex)
sh("xxd -plain -revert bluewallet-release-key.keystore.hex > bluewallet-release-key.keystore") do |status|
@ -26,50 +35,41 @@ platform :android do
File.delete("bluewallet-release-key.keystore.hex")
end
end
end
desc "Update version, build number, and sign APK"
lane :update_version_build_and_sign_apk do
Dir.chdir(project_root) do
build_number = ENV['BUILD_NUMBER']
UI.user_error!("BUILD_NUMBER environment variable is missing") if build_number.nil?
# Get the version name from build.gradle
# Extract versionName from build.gradle
version_name = sh("grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '\"'").strip
# Manually update the versionCode in build.gradle
# Update versionCode in build.gradle
UI.message("Updating versionCode in build.gradle to #{build_number}...")
build_gradle_path = "android/app/build.gradle"
build_gradle_contents = File.read(build_gradle_path)
new_build_gradle_contents = build_gradle_contents.gsub(/versionCode\s+\d+/, "versionCode #{build_number}")
File.write(build_gradle_path, new_build_gradle_contents)
# Get the branch name and default to 'master' if empty
# Determine branch name
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip.gsub(/[\/\\:?*"<>|]/, '_')
if branch_name.nil? || branch_name.empty?
branch_name = 'master'
end
branch_name = 'master' if branch_name.nil? || branch_name.empty?
# Append branch name only if it's not 'master'
if branch_name != 'master'
signed_apk_name = "BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk"
else
signed_apk_name = "BlueWallet-#{version_name}-#{build_number}.apk"
end
# Define APK name based on branch
signed_apk_name = branch_name != 'master' ? "BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" : "BlueWallet-#{version_name}-#{build_number}.apk"
# Continue with the build process
# Build APK
Dir.chdir("android") do
UI.message("Building APK...")
gradle(
task: "assembleRelease",
project_dir: "android"
)
gradle(task: "assembleRelease", project_dir: "android")
UI.message("APK build completed.")
# Define the output paths
# Define paths
unsigned_apk_path = "app/build/outputs/apk/release/app-release-unsigned.apk"
signed_apk_path = "app/build/outputs/apk/release/#{signed_apk_name}"
# Rename the unsigned APK to include the version and build number
# Rename APK
if File.exist?(unsigned_apk_path)
UI.message("Renaming APK to #{signed_apk_name}...")
FileUtils.mv(unsigned_apk_path, signed_apk_path)
@ -79,7 +79,7 @@ platform :android do
next
end
# Sign the APK using apksigner
# Sign APK
UI.message("Signing APK with apksigner...")
apksigner_path = "#{ENV['ANDROID_HOME']}/build-tools/34.0.0/apksigner"
sh("#{apksigner_path} sign --ks ./bluewallet-release-key.keystore --ks-pass=pass:#{ENV['KEYSTORE_PASSWORD']} #{signed_apk_path}")
@ -91,16 +91,15 @@ platform :android do
desc "Upload APK to BrowserStack and post result as PR comment"
lane :upload_to_browserstack_and_comment do
Dir.chdir(project_root) do
# Fetch the APK path from environment variables
# Determine APK path
apk_path = ENV['APK_PATH']
# Attempt to find the APK if not provided
if apk_path.nil? || apk_path.empty?
UI.message("No APK path provided, attempting to find the artifact...")
UI.message("No APK path provided, searching for APK...")
apk_path = `find ./ -name "*.apk"`.strip
UI.user_error!("No APK file found") if apk_path.nil? || apk_path.empty?
end
# Upload to BrowserStack
UI.message("Uploading APK to BrowserStack: #{apk_path}...")
upload_to_browserstack_app_live(
file_path: apk_path,
@ -108,13 +107,13 @@ platform :android do
browserstack_access_key: ENV['BROWSERSTACK_ACCESS_KEY']
)
# Extract the BrowserStack URL from the output
# Extract BrowserStack URL
app_url = ENV['BROWSERSTACK_LIVE_APP_ID']
UI.user_error!("BrowserStack upload failed, no app URL returned") if app_url.nil? || app_url.empty?
# Prepare necessary values for the PR comment
# Prepare PR comment
apk_filename = File.basename(apk_path)
apk_download_url = ENV['APK_OUTPUT_PATH'] # Assuming this path is accessible to the PR
apk_download_url = ENV['APK_OUTPUT_PATH'] # Ensure this path is accessible
browserstack_hashed_id = app_url.gsub('bs://', '')
pr_number = ENV['GITHUB_PR_NUMBER']
@ -137,6 +136,7 @@ platform :android do
**BrowserStack App URL**: #{app_url}
COMMENT
# Post PR comment if PR number is available
if pr_number
begin
sh("GH_TOKEN=#{ENV['GH_TOKEN']} gh pr comment #{pr_number} --body '#{comment}'")
@ -149,36 +149,34 @@ platform :android do
end
end
end
end
# ===========================
# iOS Lanes
# ===========================
platform :ios do
before_all do |lane, options|
UI.message("Setting up for all lanes...")
UI.message("Discarding all untracked changes before running any lane...")
sh("git clean -fd")
sh("git checkout -- .")
end
desc "Register new devices from a file"
lane :register_devices_from_txt do
UI.message("Registering new devices from file...")
csv_path = "../../devices.txt" # Update this with the actual path to your file
# Registering devices using the devices_file parameter
# Register devices using the devices_file parameter
register_devices(
devices_file: csv_path
)
UI.message("Devices registered successfully.")
# Update provisioning profiles for all app identifiers
app_identifiers.each do |app_identifier|
match(
type: "development",
app_identifier: app_identifier,
readonly: false, # This will regenerate the provisioning profile if needed
readonly: false, # Regenerate provisioning profile if needed
force_for_new_devices: true,
clone_branch_directly: true
)
@ -204,27 +202,18 @@ platform :ios do
end
desc "Synchronize certificates and provisioning profiles"
lane :setup_provisioning_profiles do |options|
lane :setup_provisioning_profiles do
UI.message("Setting up provisioning profiles...")
target_to_app_identifier = {
'BlueWallet' => 'io.bluewallet.bluewallet',
'BlueWalletWatch' => 'io.bluewallet.bluewallet.watch',
'BlueWalletWatchExtension' => 'io.bluewallet.bluewallet.watch.extension',
'Stickers' => 'io.bluewallet.bluewallet.Stickers',
'MarketWidget' => 'io.bluewallet.bluewallet.MarketWidget'
}
platform = options[:platform] || "ios" # Default to iOS if not specified
platform = "ios"
# Remove local master branch if it exists (Exit status: 128 - 'fatal: a branch named 'master' already exists')
sh("git branch -D master || true")
target_to_app_identifier.each do |target, app_identifier|
# 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 (Exit 128 error)
clone_branch_directly: true, # Skip if the branch already exists
platform: platform,
app_identifier: app_identifier,
team_id: ENV["ITC_TEAM_ID"],
@ -255,7 +244,6 @@ platform :ios do
app_identifier: app_identifiers,
readonly: true,
clone_branch_directly: true
)
end
@ -306,44 +294,17 @@ platform :ios do
cocoapods(podfile: "ios/Podfile")
end
desc "Build the application"
lane :build_app_lane do
UI.message("Building the application...")
build_app(
scheme: "BlueWallet",
workspace: "ios/BlueWallet.xcworkspace",
export_method: "app-store",
include_bitcode: false,
configuration: "Release",
skip_profile_detection: true,
include_symbols: true,
export_team_id: ENV["ITC_TEAM_ID"],
export_options: {
signingStyle: "manual",
provisioningProfiles: {
'io.bluewallet.bluewallet' => 'match AppStore io.bluewallet.bluewallet',
'io.bluewallet.bluewallet.watch' => 'match AppStore io.bluewallet.bluewallet.watch',
'io.bluewallet.bluewallet.watch.extension' => 'match AppStore io.bluewallet.bluewallet.watch.extension',
'io.bluewallet.bluewallet.Stickers' => 'match AppStore io.bluewallet.bluewallet.Stickers',
'io.bluewallet.bluewallet.MarketWidget' => 'match AppStore io.bluewallet.bluewallet.MarketWidget'
}
},
xcargs: "GCC_PREPROCESSOR_DEFINITIONS='$(inherited) VERBOSE_LOGGING=1'",
output_directory: "./build", # Directory where the IPA file will be stored
desc "Upload IPA to TestFlight"
lane :upload_to_testflight_lane do
ipa_path = ENV['IPA_OUTPUT_PATH']
changelog = ENV["LATEST_COMMIT_MESSAGE"]
output_name: "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa",
buildlog_path: "./build_logs"
)
# Check if IPA exists before proceeding
if ipa_path.nil? || ipa_path.empty? || !File.exist?(ipa_path)
UI.user_error!("IPA file not found at path: #{ipa_path}")
end
desc "Upload to TestFlight without Processing Wait"
lane :upload_to_testflight_lane do
attempts = 0
max_attempts = 3
begin
UI.message("Uploading to TestFlight without processing wait...")
changelog = ENV["LATEST_COMMIT_MESSAGE"]
ipa_path = "./BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa"
UI.message("Uploading IPA to TestFlight from path: #{ipa_path}")
upload_to_testflight(
api_key_path: "./appstore_api_key.json",
@ -351,37 +312,86 @@ platform :ios do
skip_waiting_for_build_processing: true, # Do not wait for processing
changelog: changelog
)
rescue => exception
attempts += 1
if attempts <= max_attempts
wait_time = 180 # 3 minutes in seconds
UI.message("Attempt ##{attempts} failed with error: #{exception.message}. Waiting #{wait_time} seconds before trying again...")
sleep(wait_time)
retry
UI.success("Successfully uploaded IPA to TestFlight!")
end
desc "Build the iOS app"
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")
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
# Use File.join to construct paths without extra slashes
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
if ipa_path && File.exist?(ipa_path)
UI.message("IPA successfully found at: #{ipa_path}")
ENV['IPA_OUTPUT_PATH'] = ipa_path
sh("echo 'IPA_OUTPUT_PATH=#{ipa_path}' >> $GITHUB_ENV") # Export for GitHub Actions
else
UI.error("Failed after #{max_attempts} attempts. Error: #{exception.message}")
raise exception
UI.user_error!("IPA not found after build_ios_app.")
end
end
end
# ===========================
# Global Lanes
# ===========================
desc "Deploy to TestFlight"
lane :deploy do |options|
UI.message("Starting build process...")
UI.message("Starting deployment process...")
# Update the WWDR certificate
# Update WWDR Certificate
update_wwdr_certificate
# Setup App Store Connect API Key
setup_app_store_connect_api_key
# Setup Provisioning Profiles
setup_provisioning_profiles
# Clear Derived Data
clear_derived_data_lane
# Increment Build Number
increment_build_number_lane
# Install CocoaPods if not already installed
unless File.directory?("Pods")
install_pods
end
# Build the iOS App
build_app_lane
# Upload IPA to TestFlight
upload_to_testflight_lane
# Clean up and delete the temporary keychain
@ -402,16 +412,13 @@ lane :update_release_notes do |options|
app = Spaceship::ConnectAPI::App.find(app_identifiers.first)
unless app
UI.user_error!("Could not find the app with identifier: #{app_identifiers.first}")
end
UI.user_error!("Could not find the app with identifier: #{app_identifiers.first}") unless app
# Retry logic for fetching or creating the edit version
retries = 5
begin
prepare_version = app.get_edit_app_store_version(platform: Spaceship::ConnectAPI::Platform::IOS)
# If no "Prepare for Submission" version is found, create a new one
if prepare_version.nil?
UI.message("No version in 'Prepare for Submission' found. Creating a new version...")
latest_version = app.get_latest_version(platform: Spaceship::ConnectAPI::Platform::IOS)
@ -436,12 +443,11 @@ lane :update_release_notes do |options|
# Extract existing metadata
localized_metadata = prepare_version.get_app_store_version_localizations
# Get all the enabled locales for the app version
# Get enabled locales
enabled_locales = localized_metadata.map(&:locale)
# Define valid language codes and filter them based on enabled locales
# Define release notes
release_notes_text = options[:release_notes]
if release_notes_text.nil? || release_notes_text.strip.empty?
release_notes_path = "../../release-notes.txt"
unless File.exist?(release_notes_path)
@ -451,6 +457,7 @@ lane :update_release_notes do |options|
release_notes_text = File.read(release_notes_path)
end
# Define localized release notes
localized_release_notes = {
'en-US' => release_notes_text, # English (U.S.) - Primary
'ar-SA' => release_notes_text, # Arabic
@ -479,7 +486,7 @@ lane :update_release_notes do |options|
'th' => release_notes_text, # Thai
}.select { |locale, _| enabled_locales.include?(locale) } # Only include enabled locales
# Review what's going to be updated
# Review release notes updates
UI.message("Review the following release notes updates:")
localized_release_notes.each do |locale, notes|
UI.message("Locale: #{locale} - Notes: #{notes}")
@ -487,15 +494,12 @@ lane :update_release_notes do |options|
unless options[:force_yes]
confirm = UI.confirm("Do you want to proceed with these release notes updates?")
unless confirm
UI.user_error!("User aborted the lane.")
end
UI.user_error!("User aborted the lane.") unless confirm
end
# Update release notes in App Store Connect and skip all other metadata
# Update release notes in App Store Connect
localized_release_notes.each do |locale, notes|
app_store_version_localization = localized_metadata.find { |loc| loc.locale == locale }
if app_store_version_localization
app_store_version_localization.update(attributes: { "whats_new" => notes })
else
@ -503,5 +507,4 @@ lane :update_release_notes do |options|
end
end
end
end

View file

@ -1,35 +0,0 @@
import { useEffect, useCallback } from 'react';
// @ts-ignore: react-native-obscure is not in the type definition
import Obscure from 'react-native-obscure';
import { useSettings } from './context/useSettings';
export const usePrivacy = () => {
const { isPrivacyBlurEnabled } = useSettings();
const enableBlur = useCallback(() => {
if (!isPrivacyBlurEnabled) return;
Obscure.activateObscure();
}, [isPrivacyBlurEnabled]);
const disableBlur = useCallback(() => {
Obscure.deactivateObscure();
}, []); // This doesn't depend on the isPrivacyBlurEnabled value
useEffect(() => {
// Automatically activate or deactivate on mount and when isPrivacyBlurEnabled changes
if (isPrivacyBlurEnabled) {
enableBlur();
} else {
disableBlur();
}
// Cleanup function to deactivate obscure when the component unmounts
return () => {
disableBlur();
};
}, [isPrivacyBlurEnabled, enableBlur, disableBlur]);
return { enableBlur, disableBlur };
};
export default usePrivacy;

View file

@ -1,35 +0,0 @@
import { useEffect, useCallback } from 'react';
// @ts-ignore: react-native-obscure is not in the type definition
import { enabled } from 'react-native-privacy-snapshot';
import { useSettings } from './context/useSettings';
export const usePrivacy = () => {
const { isPrivacyBlurEnabled } = useSettings();
const enableBlur = useCallback(() => {
if (!isPrivacyBlurEnabled) return;
enabled(true);
}, [isPrivacyBlurEnabled]);
const disableBlur = useCallback(() => {
enabled(false);
}, []); // This doesn't depend on the isPrivacyBlurEnabled value
useEffect(() => {
// Automatically activate or deactivate on mount and when isPrivacyBlurEnabled changes
if (isPrivacyBlurEnabled) {
enableBlur();
} else {
disableBlur();
}
// Cleanup function to deactivate obscure when the component unmounts
return () => {
disableBlur();
};
}, [isPrivacyBlurEnabled, enableBlur, disableBlur]);
return { enableBlur, disableBlur };
};
export default usePrivacy;

View file

@ -1,9 +0,0 @@
export const usePrivacy = () => {
const enableBlur = () => {};
const disableBlur = () => {};
return { enableBlur, disableBlur };
};
export default usePrivacy;

View file

@ -16,6 +16,7 @@
<string>BlueWallet</string>
<key>CFBundleDocumentTypes</key>
<array>
<!-- PSBT file type -->
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
@ -30,6 +31,7 @@
<string>io.bluewallet.psbt</string>
</array>
</dict>
<!-- TXN file type -->
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
@ -44,6 +46,7 @@
<string>io.bluewallet.psbt.txn</string>
</array>
</dict>
<!-- Electrum Backup file type -->
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
@ -58,6 +61,7 @@
<string>io.bluewallet.backup</string>
</array>
</dict>
<!-- BW COSIGNER file type -->
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
@ -205,8 +209,11 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<!-- Define exported types (UTIs) for file types -->
<key>UTExportedTypeDeclarations</key>
<array>
<!-- PSBT -->
<dict>
<key>UTTypeConformsTo</key>
<array>
@ -214,8 +221,6 @@
</array>
<key>UTTypeDescription</key>
<string>Partially Signed Bitcoin Transaction</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt</string>
<key>UTTypeTagSpecification</key>
@ -226,11 +231,10 @@
</array>
</dict>
</dict>
<!-- BW Cosigner -->
<dict>
<key>UTTypeDescription</key>
<string>BW COSIGNER</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.bwcosigner</string>
<key>UTTypeTagSpecification</key>
@ -241,6 +245,7 @@
</array>
</dict>
</dict>
<!-- TXN -->
<dict>
<key>UTTypeConformsTo</key>
<array>
@ -248,8 +253,6 @@
</array>
<key>UTTypeDescription</key>
<string>Bitcoin Transaction</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt.txn</string>
<key>UTTypeTagSpecification</key>
@ -260,7 +263,27 @@
</array>
</dict>
</dict>
<!-- Electrum Backup -->
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Electrum Backup</string>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.backup</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>backup</string>
</array>
</dict>
</dict>
</array>
<!-- Define imported types for other files -->
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
@ -270,8 +293,8 @@
</array>
<key>UTTypeDescription</key>
<string>JSON File</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>public.json</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
@ -284,84 +307,14 @@
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Partially Signed Bitcoin Transaction</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>psbt</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Bitcoin Transaction</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.psbt.txn</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>txn</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Electrum Backup</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.backup</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>backup</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeDescription</key>
<string>BW COSIGNER</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.bwcosigner</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>bwcosigner</string>
</array>
</dict>
</dict>
</array>
<key>bugsnag</key>
<dict>
<key>apiKey</key>
<string>17ba9059f676f1cc4f45d98182388b01</string>
</dict>
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
<false/>
<key>FIREBASE_MESSAGING_AUTO_INIT_ENABLED</key>

View file

@ -1292,8 +1292,6 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-idle-timer (2.2.2):
- React-Core
- react-native-image-picker (7.1.2):
- DoubleConversion
- glog
@ -1317,7 +1315,7 @@ PODS:
- Yoga
- react-native-ios-context-menu (1.15.3):
- React-Core
- react-native-menu (1.1.2):
- react-native-menu (1.1.3):
- React
- react-native-qrcode-local-image (1.0.4):
- React
@ -1325,6 +1323,8 @@ PODS:
- React-Core
- react-native-safe-area-context (4.11.0):
- React-Core
- react-native-screen-capture (0.2.3):
- React
- react-native-secure-key-store (2.0.10):
- React-Core
- react-native-tcp-socket (6.2.0):
@ -1635,8 +1635,6 @@ PODS:
- React-Core
- RNPermissions (4.1.5):
- React-Core
- RNPrivacySnapshot (1.0.0):
- React
- RNQuickAction (0.3.13):
- React
- RNRate (1.2.12):
@ -1662,7 +1660,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReanimated (3.15.4):
- RNReanimated (3.15.5):
- DoubleConversion
- glog
- hermes-engine
@ -1682,10 +1680,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated (= 3.15.4)
- RNReanimated/worklets (= 3.15.4)
- RNReanimated/reanimated (= 3.15.5)
- RNReanimated/worklets (= 3.15.5)
- Yoga
- RNReanimated/reanimated (3.15.4):
- RNReanimated/reanimated (3.15.5):
- DoubleConversion
- glog
- hermes-engine
@ -1706,7 +1704,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReanimated/worklets (3.15.4):
- RNReanimated/worklets (3.15.5):
- DoubleConversion
- glog
- hermes-engine
@ -1845,13 +1843,13 @@ DEPENDENCIES:
- react-native-blue-crypto (from `../node_modules/react-native-blue-crypto`)
- 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-idle-timer (from `../node_modules/react-native-idle-timer`)
- 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-qrcode-local-image (from `../node_modules/@remobile/react-native-qrcode-local-image`)"
- 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`)
- react-native-secure-key-store (from `../node_modules/react-native-secure-key-store`)
- react-native-tcp-socket (from `../node_modules/react-native-tcp-socket`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
@ -1893,7 +1891,6 @@ DEPENDENCIES:
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLocalize (from `../node_modules/react-native-localize`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNPrivacySnapshot (from `../node_modules/react-native-privacy-snapshot`)
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
- RNRate (from `../node_modules/react-native-rate`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
@ -1998,8 +1995,6 @@ EXTERNAL SOURCES:
:path: "../blue_modules/react-native-bw-file-access"
react-native-document-picker:
:path: "../node_modules/react-native-document-picker"
react-native-idle-timer:
:path: "../node_modules/react-native-idle-timer"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-ios-context-menu:
@ -2012,6 +2007,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-randombytes"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-screen-capture:
:path: "../node_modules/react-native-screen-capture"
react-native-secure-key-store:
:path: "../node_modules/react-native-secure-key-store"
react-native-tcp-socket:
@ -2094,8 +2091,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-localize"
RNPermissions:
:path: "../node_modules/react-native-permissions"
RNPrivacySnapshot:
:path: "../node_modules/react-native-privacy-snapshot"
RNQuickAction:
:path: "../node_modules/react-native-quick-actions"
RNRate:
@ -2164,13 +2159,13 @@ SPEC CHECKSUMS:
react-native-blue-crypto: 23f1558ad3d38d7a2edb7e2f6ed1bc520ed93e56
react-native-bw-file-access: b232fd1d902521ca046f3fc5990ab1465e1878d7
react-native-document-picker: 7343222102ece8aec51390717f47ad7119c7921f
react-native-idle-timer: ee2053f2cd458f6fef1db7bebe5098ca281cce07
react-native-image-picker: 2fbbafdae7a7c6db9d25df2f2b1db4442d2ca2ad
react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5
react-native-menu: d32728a357dfb360cf01cd5979cf7713c5acbb95
react-native-menu: c30eb7a85d7b04d51945f61ea8a8986ed366ac5c
react-native-qrcode-local-image: 35ccb306e4265bc5545f813e54cc830b5d75bcfc
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9
react-native-screen-capture: 75db9b051c41fea47fa68665506e9257d4b1dadc
react-native-secure-key-store: 910e6df6bc33cb790aba6ee24bc7818df1fe5898
react-native-tcp-socket: 8c3e8bef909ab06c557eeb95363fe029391ff09d
React-nativeconfig: 8c83d992b9cc7d75b5abe262069eaeea4349f794
@ -2212,11 +2207,10 @@ SPEC CHECKSUMS:
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
RNLocalize: 4f22418187ecd5ca693231093ff1d912d1b3c9bc
RNPermissions: 9fa74223844f437bc309e112994859dc47194829
RNPrivacySnapshot: 71919dde3c6a29dd332115409c2aec564afee8f4
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNRate: ef3bcff84f39bb1d1e41c5593d3eea4aab2bd73a
RNReactNativeHapticFeedback: 0d591ea1e150f36cb96d868d4e8d77272243d78a
RNReanimated: 6e79f3e3b37a88cddfb38525e9652aabd7c4c750
RNReanimated: 625f9e7f53cba61d7b3436e8e6e209d1dd4e6e9b
RNScreens: 19719a9c326e925498ac3b2d35c4e50fe87afc06
RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c
RNSVG: 4590aa95758149fa27c5c83e54a6a466349a1688

33
ios/export_options.plist Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>manual</string>
<key>teamID</key>
<string>A7W54YZ4WU</string>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
<key>thinning</key>
<string>none</string>
<key>destination</key>
<string>export</string>
<key>provisioningProfiles</key>
<dict>
<key>io.bluewallet.bluewallet</key>
<string>match AppStore io.bluewallet.bluewallet</string>
<key>io.bluewallet.bluewallet.watch</key>
<string>match AppStore io.bluewallet.bluewallet.watch</string>
<key>io.bluewallet.bluewallet.watch.extension</key>
<string>match AppStore io.bluewallet.bluewallet.watch.extension</string>
<key>io.bluewallet.bluewallet.Stickers</key>
<string>match AppStore io.bluewallet.bluewallet.Stickers</string>
<key>io.bluewallet.bluewallet.MarketWidget</key>
<string>match AppStore io.bluewallet.bluewallet.MarketWidget</string>
</dict>
</dict>
</plist>

View file

@ -10,6 +10,7 @@
"never": "nie",
"of": "{number} von {total}",
"ok": "OK",
"enter_url": "URL eingeben",
"storage_is_encrypted": "Zum Entschlüsseln des Speichers das Passwort eingeben.",
"yes": "Ja",
"no": "Nein",
@ -25,7 +26,8 @@
"pick_file": "Datei auswählen",
"enter_amount": "Betrag eingeben",
"qr_custom_input_button": "10x antippen für individuelle Eingabe",
"unlock": "Entsperren"
"unlock": "Entsperren",
"suggested": "Vorgeschlagen"
},
"azteco": {
"codeIs": "Dein Gutscheincode lautet",
@ -206,8 +208,10 @@
"performance_score": "Leistungskennzahl: {num}",
"run_performance_test": "Leistung testen",
"about_selftest": "Selbsttest ausführen",
"block_explorer_invalid_custom_url": "Ungültige URL. Geben Sie eine gültige URL ein, die mit http:// oder https:// beginnt.",
"about_selftest_electrum_disabled": "Deaktiviere den Electrum Offline-Modus, um den Selbsttest durchführen zu können.",
"about_selftest_ok": "Alle internen Tests verliefen erfolgreich. Das Wallet funktioniert gut.",
"about_sm_github": "GitHub",
"about_sm_discord": "Discord Server",
"about_sm_telegram": "Telegram-Channel",
@ -259,6 +263,9 @@
"encrypt_storage_explanation_description_line1": "Die Aktivierung der Speicherverschlüsselung fügt Ihrer App ein zusätzlicher Schutz hinzu. Die Art und Weise, wie die Daten auf Ihrem Gerät gespeichert werden, macht es anderen damit schwieriger, ohne Erlaubnis darauf zuzugreifen.",
"encrypt_storage_explanation_description_line2": "Diese Verschlüsselung betrifft den Zugriff auf die im Gerät gespeicherten Schlüssel, schützt also die Wallets. Die Wallet selbst werden dabei nicht mit einem Passwort oder einem anderen zusätzlichen Schutz versehen.",
"i_understand": "Ich habe verstanden",
"block_explorer": "Block-Explorer",
"block_explorer_preferred": "Bevorzugten Block-Explorer verwenden",
"block_explorer_error_saving_custom": "Fehler beim Speichern des bevorzugten Block-Explorers.",
"encrypt_title": "Sicherheit",
"encrypt_tstorage": "Speicher",
"encrypt_use": "Benutze {type}",

View file

@ -130,6 +130,9 @@
"details_insert_contact": "Insert Contact",
"details_add_rec_add": "Add Recipient",
"details_add_rec_rem": "Remove Recipient",
"details_add_recc_rem_all_alert_description": "Are you sure you want to remove all recipients?",
"details_add_rec_rem_all": "Remove All Recipients",
"details_recipients_title": "Recipients",
"details_address": "Address",
"details_address_field_is_not_valid": "The address is not valid.",
"details_adv_fee_bump": "Allow Fee Bump",
@ -302,8 +305,8 @@
"privacy_clipboard_explanation": "Provide shortcuts if an address or invoice is found in your clipboard.",
"privacy_do_not_track": "Disable Analytics",
"privacy_do_not_track_explanation": "Performance and reliability information will not be submitted for analysis.",
"push_notifications": "Push Notifications",
"rate": "Rate",
"push_notifications_explanation": "By enabling notifications, your device token will be sent to the server, along with wallet addresses and transaction IDs for all wallets and transactions made after enabling notifications. The device token is used to send notifications, and the wallet information allows us to notify you about incoming Bitcoin or transaction confirmations.\n\nOnly information from after you enable notifications is transmitted—nothing from before is collected.\n\nDisabling notifications will remove all of this information from the server. Additionally, deleting a wallet from the app will also remove its associated information from the server.",
"selfTest": "Self-Test",
"save": "Save",
"saved": "Saved",
@ -315,6 +318,7 @@
},
"notifications": {
"would_you_like_to_receive_notifications": "Would you like to receive notifications when you get incoming payments?",
"notifications_subtitle": "Incoming payments and transaction confirmations",
"no_and_dont_ask": "No, and do not ask me again.",
"ask_me_later": "Ask me later."
},
@ -341,7 +345,6 @@
"details_outputs": "Outputs",
"date": "Date",
"details_received": "Received",
"transaction_saved": "Saved",
"details_show_in_block_explorer": "View in Block Explorer",
"details_title": "Transaction",
"incoming_transaction": "Incoming Transaction",
@ -417,7 +420,6 @@
"details_master_fingerprint": "Master Fingerprint",
"details_multisig_type": "multisig",
"details_no_cancel": "No, cancel",
"details_save": "Save",
"details_show_xpub": "Show Wallet XPUB",
"details_show_addresses": "Show addresses",
"details_title": "Wallet",

View file

@ -130,6 +130,9 @@
"details_insert_contact": "Insertar contacto",
"details_add_rec_add": "Agregar destinatario",
"details_add_rec_rem": "Eliminar destinatario",
"details_add_recc_rem_all_alert_description": "¿Estás seguro de que quieres eliminar todos los destinatarios?",
"details_add_rec_rem_all": "Eliminar todos los destinatarios",
"details_recipients_title": "Destinatarios",
"details_address": "Dirección",
"details_address_field_is_not_valid": "La dirección no es válida.",
"details_adv_fee_bump": "Permitir aumento de tarifas",
@ -302,8 +305,8 @@
"privacy_clipboard_explanation": "Proporciona atajos si encuentras una dirección o factura en tu portapapeles.",
"privacy_do_not_track": "Desactivar análisis",
"privacy_do_not_track_explanation": "La información de rendimiento y confiabilidad no se enviará para su análisis.",
"push_notifications": "Notificaciones Push",
"rate": "Tasa",
"push_notifications_explanation": "Al habilitar las notificaciones, el token de tu dispositivo se enviará al servidor, junto con las direcciones de la billetera y los identificadores de transacciones de todas las billeteras y transacciones realizadas después de habilitar las notificaciones. El token del dispositivo se utiliza para enviar notificaciones, y la información de la billetera nos permite notificarte sobre la llegada de Bitcoin o las confirmaciones de transacciones.\n\nSolo se transmite la información que se recibe después de habilitar las notificaciones; no se recopila nada anterior.\n\nSi deshabilitas las notificaciones, se eliminará toda esta información del servidor. Además, si eliminas una billetera de la aplicación, también se eliminará la información asociada a ella del servidor.",
"selfTest": "Auto-Test",
"save": "Guardar",
"saved": "Guardado",
@ -315,6 +318,7 @@
},
"notifications": {
"would_you_like_to_receive_notifications": "¿Te gustaría recibir notificaciones cuando recibas pagos entrantes?",
"notifications_subtitle": "Pagos entrantes y confirmaciones de transacciones",
"no_and_dont_ask": "No, y no me vuelvas a preguntar.",
"ask_me_later": "Pregúntame Luego."
},
@ -341,7 +345,6 @@
"details_outputs": "Salidas",
"date": "Fecha",
"details_received": "Recibido",
"transaction_saved": "Guardado",
"details_show_in_block_explorer": "Ver en el Explorador de Bloques",
"details_title": "Transacción",
"incoming_transaction": "Transacción entrante",
@ -416,7 +419,6 @@
"details_master_fingerprint": "Huella Digital Maestra",
"details_multisig_type": "multifirma",
"details_no_cancel": "No, cancelar",
"details_save": "Guardar",
"details_show_xpub": "Mostrar el XPUB de la Billetera",
"details_show_addresses": "Mostrar direcciones",
"details_title": "Billetera",

View file

@ -14,7 +14,7 @@ import LnurlAuth from '../screen/lnd/lnurlAuth';
import LnurlPay from '../screen/lnd/lnurlPay';
import LnurlPaySuccess from '../screen/lnd/lnurlPaySuccess';
import Broadcast from '../screen/send/Broadcast';
import IsItMyAddress from '../screen/send/isItMyAddress';
import IsItMyAddress from '../screen/settings/IsItMyAddress';
import Success from '../screen/send/success';
import CPFP from '../screen/transactions/CPFP';
import TransactionDetails from '../screen/transactions/TransactionDetails';
@ -133,7 +133,6 @@ const DetailViewStackScreensStack = () => {
backgroundColor: theme.colors.customHeader,
},
headerTitle: loc.transactions.details_title,
headerRight: () => DetailButton,
})(theme)}
/>
<DetailViewStack.Screen
@ -200,6 +199,7 @@ const DetailViewStackScreensStack = () => {
<DetailViewStack.Screen
name="IsItMyAddress"
component={IsItMyAddress}
initialParams={{ address: undefined }}
options={navigationStyle({ title: loc.is_it_my_address.title })(theme)}
/>
<DetailViewStack.Screen

View file

@ -16,7 +16,7 @@ export type DetailViewStackParamList = {
LNDViewAdditionalInvoiceInformation: { invoiceId: string };
LNDViewAdditionalInvoicePreImage: { invoiceId: string };
Broadcast: { scannedData?: string };
IsItMyAddress: undefined;
IsItMyAddress: { address?: string };
GenerateWord: undefined;
LnurlPay: undefined;
LnurlPaySuccess: {
@ -77,7 +77,7 @@ export type DetailViewStackParamList = {
ReceiveDetailsRoot: {
screen: 'ReceiveDetails';
params: {
walletID: string;
walletID?: string;
address: string;
};
};

View file

@ -14,7 +14,7 @@ const DefaultView = lazy(() => import('../screen/settings/DefaultView'));
const ElectrumSettings = lazy(() => import('../screen/settings/electrumSettings'));
const EncryptStorage = lazy(() => import('../screen/settings/EncryptStorage'));
const LightningSettings = lazy(() => import('../screen/settings/LightningSettings'));
const NotificationSettings = lazy(() => import('../screen/settings/notificationSettings'));
const NotificationSettings = lazy(() => import('../screen/settings/NotificationSettings'));
const SelfTest = lazy(() => import('../screen/settings/SelfTest'));
const ReleaseNotes = lazy(() => import('../screen/settings/ReleaseNotes'));
const Tools = lazy(() => import('../screen/settings/tools'));

1106
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -74,7 +74,7 @@
"unit": "jest -b -w tests/unit/*"
},
"dependencies": {
"@babel/preset-env": "7.25.3",
"@babel/preset-env": "7.25.8",
"@bugsnag/react-native": "8.0.0",
"@bugsnag/source-maps": "2.3.3",
"@keystonehq/bc-ur-registry": "0.7.0",
@ -84,7 +84,7 @@
"@react-native-async-storage/async-storage": "1.24.0",
"@react-native-clipboard/clipboard": "1.14.2",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#8c6004b",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#a33379d",
"@react-native/gradle-plugin": "^0.75.4",
"@react-native/metro-config": "0.75.4",
"@react-navigation/drawer": "6.7.2",
@ -138,27 +138,25 @@
"react-native-gesture-handler": "2.20.0",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
"react-native-haptic-feedback": "2.3.3",
"react-native-idle-timer": "github:BlueWallet/react-native-idle-timer#v2.2.3",
"react-native-image-picker": "7.1.2",
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e",
"react-native-keychain": "8.2.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.2.1",
"react-native-obscure": "github:BlueWallet/react-native-obscure#f4b83b4a261e39b1f5ed4a45ac5bcabc8a59eadb",
"react-native-permissions": "4.1.5",
"react-native-privacy-snapshot": "github:BlueWallet/react-native-privacy-snapshot#529e4627d93f67752a27e82a040ff7b64dca0783",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.2",
"react-native-quick-actions": "0.3.13",
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.15.4",
"react-native-safe-area-context": "4.11.0",
"react-native-reanimated": "3.16.0",
"react-native-safe-area-context": "4.11.1",
"react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f",
"react-native-screens": "3.34.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
"react-native-share": "10.2.1",
"react-native-svg": "15.7.1",
"react-native-svg": "15.8.0",
"react-native-tcp-socket": "6.2.0",
"react-native-vector-icons": "10.2.0",
"react-native-watch-connectivity": "1.1.0",

View file

@ -57,6 +57,7 @@ import { useKeyboard } from '../../hooks/useKeyboard';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import ActionSheet from '../ActionSheet';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
interface IPaymentDestinations {
address: string; // btc address or payment code
@ -852,6 +853,24 @@ const SendDetails = () => {
}, 0);
};
const onRemoveAllRecipientsConfirmed = useCallback(() => {
setAddresses([{ address: '', key: String(Math.random()) } as IPaymentDestinations]);
}, []);
const handleRemoveAllRecipients = useCallback(() => {
Alert.alert(loc.send.details_recipients_title, loc.send.details_add_recc_rem_all_alert_description, [
{
text: loc._.cancel,
onPress: () => {},
style: 'cancel',
},
{
text: loc._.ok,
onPress: onRemoveAllRecipientsConfirmed,
},
]);
}, [onRemoveAllRecipientsConfirmed]);
const handleRemoveRecipient = () => {
if (addresses.length > 1) {
const newAddresses = [...addresses];
@ -933,9 +952,9 @@ const SendDetails = () => {
// Header Right Button
const headerRightOnPress = (id: string) => {
if (id === SendDetails.actionKeys.AddRecipient) {
if (id === CommonToolTipActions.AddRecipient.id) {
handleAddRecipient();
} else if (id === SendDetails.actionKeys.RemoveRecipient) {
} else if (id === CommonToolTipActions.RemoveRecipient.id) {
handleRemoveRecipient();
} else if (id === SendDetails.actionKeys.SignPSBT) {
handlePsbtSign();
@ -955,6 +974,8 @@ const SendDetails = () => {
handleCoinControl();
} else if (id === SendDetails.actionKeys.InsertContact) {
handleInsertContact();
} else if (id === CommonToolTipActions.RemoveAllRecipients.id) {
handleRemoveAllRecipients();
}
};
@ -1007,19 +1028,13 @@ const SendDetails = () => {
if ((wallet as MultisigHDWallet)?.allowCosignPsbt()) {
transactionActions.push({ id: SendDetails.actionKeys.SignPSBT, text: loc.send.psbt_sign, icon: SendDetails.actionIcons.SignPSBT });
}
actions.push(transactionActions, [
{
id: SendDetails.actionKeys.AddRecipient,
text: loc.send.details_add_rec_add,
icon: SendDetails.actionIcons.AddRecipient,
},
{
id: SendDetails.actionKeys.RemoveRecipient,
text: loc.send.details_add_rec_rem,
disabled: addresses.length < 2,
icon: SendDetails.actionIcons.RemoveRecipient,
},
]);
actions.push(transactionActions);
const recipientActions: Action[] = [CommonToolTipActions.AddRecipient, CommonToolTipActions.RemoveRecipient];
if (addresses.length > 1) {
recipientActions.push(CommonToolTipActions.RemoveAllRecipients);
}
actions.push(recipientActions);
}
actions.push({ id: SendDetails.actionKeys.CoinControl, text: loc.cc.header, icon: SendDetails.actionIcons.CoinControl });
@ -1355,8 +1370,6 @@ SendDetails.actionKeys = {
InsertContact: 'InsertContact',
SignPSBT: 'SignPSBT',
SendMax: 'SendMax',
AddRecipient: 'AddRecipient',
RemoveRecipient: 'RemoveRecipient',
AllowRBF: 'AllowRBF',
ImportTransaction: 'ImportTransaction',
ImportTransactionMultsig: 'ImportTransactionMultisig',
@ -1369,8 +1382,6 @@ SendDetails.actionIcons = {
InsertContact: { iconValue: 'at.badge.plus' },
SignPSBT: { iconValue: 'signature' },
SendMax: 'SendMax',
AddRecipient: { iconValue: 'person.badge.plus' },
RemoveRecipient: { iconValue: 'person.badge.minus' },
AllowRBF: 'AllowRBF',
ImportTransaction: { iconValue: 'square.and.arrow.down' },
ImportTransactionMultsig: { iconValue: 'square.and.arrow.down.on.square' },

View file

@ -16,7 +16,7 @@ import { BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import { DynamicQRCode } from '../../components/DynamicQRCode';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
@ -26,7 +26,6 @@ const SendCreate = () => {
const size = transaction.virtualSize();
const { colors } = useTheme();
const { setOptions } = useNavigation();
const { enableBlur, disableBlur } = usePrivacy();
const styleHooks = StyleSheet.create({
transactionDetailsTitle: {
@ -48,11 +47,11 @@ const SendCreate = () => {
useEffect(() => {
console.log('send/create - useEffect');
enableBlur();
disallowScreenshot(true);
return () => {
disableBlur();
disallowScreenshot(false);
};
}, [disableBlur, enableBlur]);
}, []);
const exportTXN = useCallback(async () => {
const fileName = `${Date.now()}.txn`;

View file

@ -1,151 +0,0 @@
import React, { useRef, useState } from 'react';
import { useRoute } from '@react-navigation/native';
import { Keyboard, StyleSheet, TextInput, View } from 'react-native';
import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const IsItMyAddress = () => {
/** @type {AbstractWallet[]} */
const { wallets } = useStorage();
const { navigate } = useExtendedNavigation();
const { name } = useRoute();
const { colors } = useTheme();
const scanButtonRef = useRef();
const [address, setAddress] = useState('');
const [result, setResult] = useState('');
const [resultCleanAddress, setResultCleanAddress] = useState();
const stylesHooks = StyleSheet.create({
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});
const handleUpdateAddress = nextValue => setAddress(nextValue.trim());
const checkAddress = () => {
Keyboard.dismiss();
const cleanAddress = address.replace('bitcoin:', '').replace('BITCOIN:', '').replace('bitcoin=', '').split('?')[0];
const _result = [];
for (const w of wallets) {
if (w.weOwnAddress(cleanAddress)) {
setResultCleanAddress(cleanAddress);
_result.push(loc.formatString(loc.is_it_my_address.owns, { label: w.getLabel(), address: cleanAddress }));
}
}
if (_result.length === 0) {
setResult(_result.push(loc.is_it_my_address.no_wallet_owns_address));
setResultCleanAddress();
}
setResult(_result.join('\n\n'));
};
const onBarScanned = value => {
setAddress(value);
setResultCleanAddress(value);
};
const importScan = () => {
scanQrHelper(name, true, onBarScanned);
};
const clearAddressInput = () => {
setAddress('');
setResult();
setResultCleanAddress();
};
const viewQRCode = () => {
navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
address: resultCleanAddress,
},
});
};
return (
<SafeArea style={styles.blueArea}>
<View style={styles.wrapper}>
<BlueCard style={styles.mainCard}>
<View style={[styles.input, stylesHooks.input]}>
<TextInput
style={styles.text}
maxHeight={100}
minHeight={100}
maxWidth="100%"
minWidth="100%"
multiline
editable
placeholder={loc.is_it_my_address.enter_address}
placeholderTextColor="#81868e"
value={address}
onChangeText={handleUpdateAddress}
testID="AddressInput"
/>
</View>
<BlueSpacing10 />
<BlueButtonLink ref={scanButtonRef} title={loc.wallets.import_scan_qr} onPress={importScan} />
<BlueSpacing10 />
<Button title={loc.send.input_clear} onPress={clearAddressInput} />
<BlueSpacing20 />
{resultCleanAddress && (
<>
<Button title={loc.is_it_my_address.view_qrcode} onPress={viewQRCode} />
<BlueSpacing20 />
</>
)}
<Button
disabled={address.trim().length === 0}
title={loc.is_it_my_address.check_address}
onPress={checkAddress}
testID="CheckAddress"
/>
<BlueSpacing20 />
<BlueText testID="Result">{result}</BlueText>
</BlueCard>
</View>
</SafeArea>
);
};
export default IsItMyAddress;
const styles = StyleSheet.create({
wrapper: {
marginTop: 16,
alignItems: 'center',
justifyContent: 'flex-start',
},
mainCard: {
padding: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
alignItems: 'center',
borderRadius: 4,
},
text: {
padding: 8,
minHeight: 33,
color: '#81868e',
},
});

View file

@ -54,7 +54,7 @@ const Success = () => {
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffActivityType.ViewInBlockExplorer}
url={`${selectedBlockExplorer}/tx/${txid}`}
url={`${selectedBlockExplorer.url}/tx/${txid}`}
/>
)}
</SafeArea>

View file

@ -1,7 +1,7 @@
import dayjs from 'dayjs';
import calendar from 'dayjs/plugin/calendar';
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { FlatList, NativeSyntheticEvent, StyleSheet, View, LayoutAnimation, UIManager, Platform } from 'react-native';
import { FlatList, NativeSyntheticEvent, StyleSheet, View, LayoutAnimation, UIManager, Platform, Keyboard } from 'react-native';
import {
CurrencyRate,
@ -87,6 +87,7 @@ const Currency: React.FC = () => {
isLoading={isSavingNewPreferredCurrency && selectedCurrency.endPointKey === item.endPointKey}
subtitle={item.country}
onPress={async () => {
Keyboard.dismiss();
setIsSavingNewPreferredCurrency(item);
try {
await getFiatRate(item.endPointKey);

View file

@ -0,0 +1,282 @@
import React, { useRef, useState, useEffect } from 'react';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { Keyboard, StyleSheet, TextInput, View, ScrollView, TouchableOpacity, Text } from 'react-native';
import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { TWallet } from '../../class/wallets/types';
import { WalletCarouselItem } from '../../components/WalletsCarousel';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Divider } from '@rneui/themed';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import presentAlert from '../../components/Alert';
type RouteProps = RouteProp<DetailViewStackParamList, 'IsItMyAddress'>;
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'IsItMyAddress'>;
const IsItMyAddress: React.FC = () => {
const { wallets } = useStorage();
const navigation = useNavigation<NavigationProp>();
const route = useRoute<RouteProps>();
const { colors } = useTheme();
const scanButtonRef = useRef<any>();
const scrollViewRef = useRef<ScrollView>(null);
const firstWalletRef = useRef<View>(null);
const [address, setAddress] = useState<string>('');
const [matchingWallets, setMatchingWallets] = useState<TWallet[] | undefined>();
const [resultCleanAddress, setResultCleanAddress] = useState<string | undefined>();
const stylesHooks = StyleSheet.create({
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});
useEffect(() => {
if (route.params?.address && route.params.address !== address) {
setAddress(route.params.address);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [route.params?.address]);
useEffect(() => {
const currentAddress = route.params?.address;
if (currentAddress !== address) {
navigation.setParams({ address });
}
}, [address, navigation, route.params?.address]);
const handleUpdateAddress = (nextValue: string) => setAddress(nextValue);
const clearAddressInput = () => {
setAddress('');
setResultCleanAddress(undefined);
setMatchingWallets(undefined);
};
const checkAddress = () => {
Keyboard.dismiss();
const cleanAddress = address.replace('bitcoin:', '').replace('BITCOIN:', '').replace('bitcoin=', '').split('?')[0];
const matching: TWallet[] = [];
for (const w of wallets) {
if (w.weOwnAddress(cleanAddress)) {
matching.push(w);
}
}
if (matching.length > 0) {
setMatchingWallets(matching);
setResultCleanAddress(cleanAddress);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({
message: loc.is_it_my_address.no_wallet_owns_address,
buttons: [
{
text: loc.receive.reset,
onPress: () => {
clearAddressInput();
},
style: 'destructive',
},
{
text: loc._.ok,
onPress: () => {},
style: 'cancel',
},
],
options: { cancelable: true },
});
setMatchingWallets([]);
setResultCleanAddress(undefined);
}
};
const onBarScanned = (value: string) => {
const cleanAddress = value.replace(/^bitcoin(:|=)/i, '').split('?')[0];
setAddress(value);
setResultCleanAddress(cleanAddress);
};
const importScan = async () => {
const data = await scanQrHelper(route.name, true, undefined, true);
if (data) {
onBarScanned(data);
}
};
const viewQRCode = () => {
if (!resultCleanAddress) return;
navigation.navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
address: resultCleanAddress,
},
});
};
const isCheckAddressDisabled = address.trim().length === 0;
useEffect(() => {
if (matchingWallets && matchingWallets.length > 0 && scrollViewRef.current && firstWalletRef.current) {
firstWalletRef.current.measureLayout(scrollViewRef.current.getInnerViewNode(), (x, y) => {
scrollViewRef.current?.scrollTo({ x: 0, y: y - 20, animated: true });
});
}
}, [matchingWallets]);
const renderFormattedText = (text: string, values: { [key: string]: string }) => {
const regex = /\{(\w+)\}/g;
const parts = [];
let lastIndex = 0;
let match;
let index = 0;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(<Text key={`text-${index++}`}>{text.substring(lastIndex, match.index)}</Text>);
}
const value = values[match[1]];
if (value) {
parts.push(
<Text key={`bold-${index++}`} style={styles.boldText} selectable>
{value}
</Text>,
);
}
lastIndex = regex.lastIndex;
}
if (lastIndex < text.length) {
parts.push(<Text key={`text-${index++}`}>{text.substring(lastIndex)}</Text>);
}
return parts;
};
return (
<ScrollView
ref={scrollViewRef}
contentContainerStyle={styles.wrapper}
automaticallyAdjustContentInsets
automaticallyAdjustKeyboardInsets
contentInsetAdjustmentBehavior="automatic"
>
<BlueCard style={styles.mainCard}>
<View style={[styles.input, stylesHooks.input]}>
<TextInput
style={styles.textInput}
multiline
editable
placeholder={loc.is_it_my_address.enter_address}
placeholderTextColor={colors.placeholderTextColor}
value={address}
onChangeText={handleUpdateAddress}
testID="AddressInput"
/>
{address.length > 0 && (
<TouchableOpacity onPress={clearAddressInput} style={styles.clearButton}>
<Icon name="close" size={20} color="#81868e" />
</TouchableOpacity>
)}
</View>
<BlueSpacing10 />
<BlueButtonLink ref={scanButtonRef} title={loc.wallets.import_scan_qr} onPress={importScan} />
<BlueSpacing20 />
{resultCleanAddress && (
<>
<Button title={loc.is_it_my_address.view_qrcode} onPress={viewQRCode} />
<BlueSpacing20 />
</>
)}
<Button disabled={isCheckAddressDisabled} title={loc.is_it_my_address.check_address} onPress={checkAddress} testID="CheckAddress" />
<BlueSpacing40 />
{matchingWallets !== undefined && matchingWallets.length > 0 && (
<>
<Divider />
<BlueSpacing40 />
</>
)}
{matchingWallets !== undefined &&
matchingWallets.length > 0 &&
matchingWallets.map((wallet, index) => (
<View key={wallet.getID()} ref={index === 0 ? firstWalletRef : undefined} style={styles.walletContainer}>
<BlueText selectable style={styles.resultText}>
{resultCleanAddress &&
renderFormattedText(loc.is_it_my_address.owns, {
label: wallet.getLabel(),
address: resultCleanAddress,
})}
</BlueText>
<BlueSpacing10 />
<WalletCarouselItem
item={wallet}
onPress={item => {
navigation.navigate('WalletTransactions', {
walletID: item.getID(),
walletType: item.type,
});
}}
/>
<BlueSpacing20 />
</View>
))}
</BlueCard>
</ScrollView>
);
};
export default IsItMyAddress;
const styles = StyleSheet.create({
wrapper: {
alignItems: 'center',
},
mainCard: {
padding: 0,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%',
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
alignItems: 'center',
borderRadius: 4,
width: '100%',
},
textInput: {
flex: 1,
padding: 8,
minHeight: 100,
color: '#81868e',
},
clearButton: {
padding: 8,
justifyContent: 'center',
alignItems: 'center',
},
boldText: {
fontWeight: 'bold',
},
resultText: {
marginVertical: 10,
textAlign: 'center',
},
walletContainer: {
width: '100%',
alignItems: 'center',
},
});

View file

@ -1,5 +1,5 @@
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { FlatList, NativeSyntheticEvent, StyleSheet } from 'react-native';
import { FlatList, Keyboard, NativeSyntheticEvent, StyleSheet } from 'react-native';
import presentAlert from '../../components/Alert';
import ListItem from '../../components/ListItem';
import { useTheme } from '../../components/themes';
@ -33,6 +33,7 @@ const Language = () => {
}, [language]);
const onLanguageSelect = (item: TLanguage) => {
Keyboard.dismiss();
const currentLanguage = AvailableLanguages.find(l => l.value === language);
setLanguageStorage(item.value).then(() => {
if (currentLanguage?.isRTL !== item.isRTL) {

View file

@ -0,0 +1,231 @@
import React, { useCallback, useEffect, useState } from 'react';
import { I18nManager, Linking, ScrollView, StyleSheet, TextInput, View, Pressable } from 'react-native';
import { Button as ButtonRNElements } from '@rneui/themed';
// @ts-ignore: no declaration file
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueSpacing20, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import { Button } from '../../components/Button';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
import ListItem, { PressableWrapper } from '../../components/ListItem';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { Divider } from '@rneui/base';
const NotificationSettings: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [isNotificationsEnabled, setNotificationsEnabled] = useState(false);
const [tokenInfo, setTokenInfo] = useState('<empty>');
const [URI, setURI] = useState<string | undefined>();
const [tapCount, setTapCount] = useState(0);
const { colors } = useTheme();
const stylesWithThemeHook = {
root: {
backgroundColor: colors.background,
},
scroll: {
backgroundColor: colors.background,
},
scrollBody: {
backgroundColor: colors.background,
},
uri: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
};
const handleTap = () => {
setTapCount(prevCount => prevCount + 1);
};
const onNotificationsSwitch = async (value: boolean) => {
try {
setNotificationsEnabled(value);
if (value) {
// User is enabling notifications
// @ts-ignore: refactor later
await Notifications.cleanUserOptOutFlag();
// @ts-ignore: refactor later
if (await Notifications.getPushToken()) {
// we already have a token, so we just need to reenable ALL level on groundcontrol:
// @ts-ignore: refactor later
await Notifications.setLevels(true);
} else {
// ok, we dont have a token. we need to try to obtain permissions, configure callbacks and save token locally:
// @ts-ignore: refactor later
await Notifications.tryToObtainPermissions();
}
} else {
// User is disabling notifications
// @ts-ignore: refactor later
await Notifications.setLevels(false);
}
// @ts-ignore: refactor later
setNotificationsEnabled(await Notifications.isNotificationsEnabled());
} catch (error) {
console.error(error);
presentAlert({ message: (error as Error).message });
}
};
useEffect(() => {
(async () => {
try {
// @ts-ignore: refactor later
setNotificationsEnabled(await Notifications.isNotificationsEnabled());
// @ts-ignore: refactor later
setURI(await Notifications.getSavedUri());
// @ts-ignore: refactor later
setTokenInfo(
'token: ' +
// @ts-ignore: refactor later
JSON.stringify(await Notifications.getPushToken()) +
' permissions: ' +
// @ts-ignore: refactor later
JSON.stringify(await Notifications.checkPermissions()) +
' stored notifications: ' +
// @ts-ignore: refactor later
JSON.stringify(await Notifications.getStoredNotifications()),
);
} catch (e) {
console.error(e);
presentAlert({ message: (e as Error).message });
} finally {
setIsLoading(false);
}
})();
}, []);
const save = useCallback(async () => {
setIsLoading(true);
try {
if (URI) {
// validating only if its not empty. empty means use default
// @ts-ignore: refactor later
if (await Notifications.isGroundControlUriValid(URI)) {
// @ts-ignore: refactor later
await Notifications.saveUri(URI);
presentAlert({ message: loc.settings.saved });
} else {
presentAlert({ message: loc.settings.not_a_valid_uri });
}
} else {
// @ts-ignore: refactor later
await Notifications.saveUri('');
presentAlert({ message: loc.settings.saved });
}
} catch (error) {
console.warn(error);
}
setIsLoading(false);
}, [URI]);
return (
<ScrollView style={stylesWithThemeHook.scroll} automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="automatic">
<ListItem
Component={PressableWrapper}
title={loc.settings.notifications}
subtitle={loc.notifications.notifications_subtitle}
disabled={isLoading}
switch={{ onValueChange: onNotificationsSwitch, value: isNotificationsEnabled, testID: 'NotificationsSwitch' }}
/>
<Pressable onPress={handleTap}>
<BlueCard>
<BlueText style={styles.multilineText}>{loc.settings.push_notifications_explanation}</BlueText>
</BlueCard>
</Pressable>
{tapCount >= 10 && (
<>
<Divider />
<BlueCard>
<BlueText>{loc.settings.groundcontrol_explanation}</BlueText>
</BlueCard>
<ButtonRNElements
icon={{
name: 'github',
type: 'font-awesome',
color: colors.foregroundColor,
}}
onPress={() => Linking.openURL('https://github.com/BlueWallet/GroundControl')}
titleStyle={{ color: colors.buttonAlternativeTextColor }}
title="github.com/BlueWallet/GroundControl"
color={colors.buttonTextColor}
buttonStyle={styles.buttonStyle}
/>
<BlueCard>
<View style={[styles.uri, stylesWithThemeHook.uri]}>
<TextInput
// @ts-ignore: refactor later
placeholder={Notifications.getDefaultUri()}
value={URI}
onChangeText={setURI}
numberOfLines={1}
style={styles.uriText}
placeholderTextColor="#81868e"
editable={!isLoading}
textContentType="URL"
autoCapitalize="none"
underlineColorAndroid="transparent"
/>
</View>
<BlueSpacing20 />
<BlueText style={styles.centered} onPress={() => setTapCount(tapCount + 1)}>
Ground Control to Major Tom
</BlueText>
<BlueText style={styles.centered} onPress={() => setTapCount(tapCount + 1)}>
Commencing countdown, engines on
</BlueText>
<View>
<CopyToClipboardButton stringToCopy={tokenInfo} displayText={tokenInfo} />
</View>
<BlueSpacing20 />
<Button onPress={save} title={loc.settings.save} />
</BlueCard>
</>
)}
</ScrollView>
);
};
const styles = StyleSheet.create({
uri: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
},
centered: {
textAlign: 'center',
},
uriText: {
flex: 1,
color: '#81868e',
marginHorizontal: 8,
minHeight: 36,
height: 36,
},
buttonStyle: {
backgroundColor: 'transparent',
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
},
multilineText: {
textAlign: 'left',
lineHeight: 20,
paddingBottom: 10,
},
});
export default NotificationSettings;

View file

@ -1,195 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { I18nManager, Linking, ScrollView, StyleSheet, TextInput, TouchableWithoutFeedback, View } from 'react-native';
import { Button as ButtonRNElements } from '@rneui/themed';
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import { Button } from '../../components/Button';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
import ListItem from '../../components/ListItem';
import { BlueCurrentTheme, useTheme } from '../../components/themes';
import loc from '../../loc';
const NotificationSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [isNotificationsEnabled, setNotificationsEnabled] = useState(false);
const [isShowTokenInfo, setShowTokenInfo] = useState(0);
const [tokenInfo, setTokenInfo] = useState('<empty>');
const [URI, setURI] = useState();
const { colors } = useTheme();
const onNotificationsSwitch = async value => {
setNotificationsEnabled(value); // so the slider is not 'jumpy'
if (value) {
// user is ENABLING notifications
await Notifications.cleanUserOptOutFlag();
if (await Notifications.getPushToken()) {
// we already have a token, so we just need to reenable ALL level on groundcontrol:
await Notifications.setLevels(true);
} else {
// ok, we dont have a token. we need to try to obtain permissions, configure callbacks and save token locally:
await Notifications.tryToObtainPermissions();
}
} else {
// user is DISABLING notifications
await Notifications.setLevels(false);
}
setNotificationsEnabled(await Notifications.isNotificationsEnabled());
};
useEffect(() => {
(async () => {
try {
setNotificationsEnabled(await Notifications.isNotificationsEnabled());
setURI(await Notifications.getSavedUri());
setTokenInfo(
'token: ' +
JSON.stringify(await Notifications.getPushToken()) +
' permissions: ' +
JSON.stringify(await Notifications.checkPermissions()) +
' stored notifications: ' +
JSON.stringify(await Notifications.getStoredNotifications()),
);
} catch (e) {
console.debug(e);
presentAlert({ message: e.message });
} finally {
setIsLoading(false);
}
})();
}, []);
const stylesWithThemeHook = {
root: {
...styles.root,
backgroundColor: colors.background,
},
scroll: {
...styles.scroll,
backgroundColor: colors.background,
},
scrollBody: {
...styles.scrollBody,
backgroundColor: colors.background,
},
};
const save = useCallback(async () => {
setIsLoading(true);
try {
if (URI) {
// validating only if its not empty. empty means use default
if (await Notifications.isGroundControlUriValid(URI)) {
await Notifications.saveUri(URI);
presentAlert({ message: loc.settings.saved });
} else {
presentAlert({ message: loc.settings.not_a_valid_uri });
}
} else {
await Notifications.saveUri('');
presentAlert({ message: loc.settings.saved });
}
} catch (error) {
console.warn(error);
}
setIsLoading(false);
}, [URI]);
return isLoading ? (
<BlueLoading />
) : (
<ScrollView style={stylesWithThemeHook.scroll} automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="automatic">
<ListItem
Component={TouchableWithoutFeedback}
title={loc.settings.push_notifications}
subtitle={loc.settings.groundcontrol_explanation}
switch={{ onValueChange: onNotificationsSwitch, value: isNotificationsEnabled, testID: 'NotificationsSwitch' }}
/>
<BlueSpacing20 />
<ButtonRNElements
icon={{
name: 'github',
type: 'font-awesome',
color: colors.foregroundColor,
}}
onPress={() => Linking.openURL('https://github.com/BlueWallet/GroundControl')}
titleStyle={{ color: colors.buttonAlternativeTextColor }}
title="github.com/BlueWallet/GroundControl"
color={colors.buttonTextColor}
buttonStyle={styles.buttonStyle}
/>
<BlueCard>
<View style={styles.uri}>
<TextInput
placeholder={Notifications.getDefaultUri()}
value={URI}
onChangeText={setURI}
numberOfLines={1}
style={styles.uriText}
placeholderTextColor="#81868e"
editable={!isLoading}
textContentType="URL"
autoCapitalize="none"
underlineColorAndroid="transparent"
/>
</View>
<BlueSpacing20 />
<BlueText style={styles.centered} onPress={() => setShowTokenInfo(isShowTokenInfo + 1)}>
Ground Control to Major Tom
</BlueText>
<BlueText style={styles.centered} onPress={() => setShowTokenInfo(isShowTokenInfo + 1)}>
Commencing countdown, engines on
</BlueText>
{isShowTokenInfo >= 9 && (
<View>
<CopyToClipboardButton stringToCopy={tokenInfo} displayText={tokenInfo} />
</View>
)}
<BlueSpacing20 />
<Button onPress={save} title={loc.settings.save} />
</BlueCard>
</ScrollView>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
uri: {
flexDirection: 'row',
borderColor: BlueCurrentTheme.colors.formBorder,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderWidth: 1,
borderBottomWidth: 0.5,
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
},
centered: {
textAlign: 'center',
},
uriText: {
flex: 1,
color: '#81868e',
marginHorizontal: 8,
minHeight: 36,
height: 36,
},
buttonStyle: {
backgroundColor: 'transparent',
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
},
});
export default NotificationSettings;

View file

@ -1,17 +1,16 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import Clipboard from '@react-native-clipboard/clipboard';
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import assert from 'assert';
import dayjs from 'dayjs';
import { InteractionManager, Keyboard, Linking, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { InteractionManager, Linking, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
import { Transaction, TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
import HandOffComponent from '../../components/HandOffComponent';
import HeaderRightButton from '../../components/HeaderRightButton';
import { useTheme } from '../../components/themes';
import ToolTipMenu from '../../components/TooltipMenu';
import loc from '../../loc';
@ -61,7 +60,7 @@ type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList, 'Tran
type RouteProps = RouteProp<DetailViewStackParamList, 'TransactionDetails'>;
const TransactionDetails = () => {
const { setOptions, navigate } = useExtendedNavigation<NavigationProps>();
const { addListener, navigate } = useExtendedNavigation<NavigationProps>();
const { hash, walletID } = useRoute<RouteProps>().params;
const { saveToDisk, txMetadata, counterpartyMetadata, wallets, getTransactions } = useStorage();
const { selectedBlockExplorer } = useSettings();
@ -88,29 +87,23 @@ const TransactionDetails = () => {
},
});
const handleOnSaveButtonTapped = useCallback(() => {
Keyboard.dismiss();
if (!tx) return;
const saveTransactionDetails = useCallback(() => {
if (tx) {
txMetadata[tx.hash] = { memo };
if (counterpartyLabel && paymentCode) {
counterpartyMetadata[paymentCode] = { label: counterpartyLabel };
}
saveToDisk().then(_success => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.transactions.transaction_saved });
});
saveToDisk();
}
}, [tx, txMetadata, memo, counterpartyLabel, paymentCode, saveToDisk, counterpartyMetadata]);
const HeaderRight = useMemo(
() => <HeaderRightButton onPress={handleOnSaveButtonTapped} testID="SaveButton" disabled={false} title={loc.wallets.details_save} />,
[handleOnSaveButtonTapped],
);
useEffect(() => {
// This effect only handles changes in `colors`
setOptions({ headerRight: () => HeaderRight });
}, [colors, HeaderRight, setOptions]);
const unsubscribe = addListener('beforeRemove', () => {
saveTransactionDetails();
});
return unsubscribe;
}, [addListener, saveTransactionDetails]);
useFocusEffect(
useCallback(() => {
@ -160,8 +153,12 @@ const TransactionDetails = () => {
}, [hash, wallets]),
);
const handleMemoBlur = useCallback(() => {
saveTransactionDetails();
}, [saveTransactionDetails]);
const handleOnOpenTransactionOnBlockExplorerTapped = () => {
const url = `${selectedBlockExplorer}/tx/${tx?.hash}`;
const url = `${selectedBlockExplorer.url}/tx/${tx?.hash}`;
Linking.canOpenURL(url)
.then(supported => {
if (supported) {
@ -186,7 +183,7 @@ const TransactionDetails = () => {
};
const handleCopyPress = (stringToCopy: string) => {
Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `${selectedBlockExplorer}/tx/${tx?.hash}`);
Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `${selectedBlockExplorer.url}/tx/${tx?.hash}`);
};
if (isLoading || !tx) {
@ -257,7 +254,7 @@ const TransactionDetails = () => {
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffActivityType.ViewInBlockExplorer}
url={`${selectedBlockExplorer}/tx/${tx.hash}`}
url={`${selectedBlockExplorer.url}/tx/${tx.hash}`}
/>
<BlueCard>
<View>
@ -268,6 +265,7 @@ const TransactionDetails = () => {
clearButtonMode="while-editing"
style={[styles.memoTextInput, stylesHooks.memoTextInput]}
onChangeText={setMemo}
onBlur={handleMemoBlur}
testID="TransactionDetailsMemoInput"
/>
{isCounterpartyLabelVisible ? (
@ -276,6 +274,7 @@ const TransactionDetails = () => {
<TextInput
placeholder={loc.send.counterparty_label_placeholder}
value={counterpartyLabel}
onBlur={handleMemoBlur}
placeholderTextColor="#81868e"
style={[styles.memoTextInput, stylesHooks.memoTextInput]}
onChangeText={setCounterpartyLabel}

View file

@ -483,7 +483,7 @@ const TransactionStatus = () => {
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffActivityType.ViewInBlockExplorer}
url={`${selectedBlockExplorer}/tx/${tx.hash}`}
url={`${selectedBlockExplorer.url}/tx/${tx.hash}`}
/>
<View style={styles.container}>

View file

@ -7,7 +7,7 @@ import { DynamicQRCode } from '../../components/DynamicQRCode';
import SaveFileButton from '../../components/SaveFileButton';
import { SquareButton } from '../../components/SquareButton';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { ExportMultisigCoordinationSetupStackRootParamList } from '../../navigation/ExportMultisigCoordinationSetupStack';
@ -75,7 +75,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
const wallet: TWallet | undefined = wallets.find(w => w.getID() === walletID);
const dynamicQRCode = useRef<any>();
const { colors } = useTheme();
const { enableBlur, disableBlur } = usePrivacy();
const navigation = useNavigation();
const stylesHook = StyleSheet.create({
scrollViewContent: {
@ -99,7 +99,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
dispatch({ type: ActionType.SET_LOADING, isLoading: true });
const task = InteractionManager.runAfterInteractions(() => {
enableBlur();
disallowScreenshot(true);
if (wallet) {
setTimeout(async () => {
try {
@ -125,7 +125,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
return () => {
task.cancel();
disableBlur();
disallowScreenshot(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]),

View file

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useRoute } from '@react-navigation/native';
import { ActivityIndicator, FlatList, LayoutAnimation, StyleSheet, View } from 'react-native';
import IdleTimerManager from 'react-native-idle-timer';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueButtonLink, BlueFormLabel, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
@ -19,6 +18,7 @@ import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { THDWalletForWatchOnly, TWallet } from '../../class/wallets/types';
import { navigate } from '../../NavigationService';
import { keepAwake, disallowScreenshot } from 'react-native-screen-capture';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
@ -115,8 +115,7 @@ const ImportWalletDiscovery: React.FC = () => {
}
};
IdleTimerManager.setIdleTimerDisabled(true);
keepAwake(true);
task.current = startImport(importText, askPassphrase, searchAccounts, onProgress, onWallet, onPassword);
task.current.promise
@ -134,7 +133,7 @@ const ImportWalletDiscovery: React.FC = () => {
.finally(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setLoading(false);
IdleTimerManager.setIdleTimerDisabled(false);
keepAwake(false);
});
return () => {
@ -145,7 +144,8 @@ const ImportWalletDiscovery: React.FC = () => {
const handleCustomDerivation = () => {
task.current?.stop();
keepAwake(false);
disallowScreenshot(false);
navigation.navigate('ImportCustomDerivationPath', { importText, password });
};

View file

@ -3,7 +3,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
import { BackHandler, I18nManager, ScrollView, StyleSheet, Text, View } from 'react-native';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
@ -13,7 +13,6 @@ const PleaseBackup: React.FC = () => {
const wallet = wallets.find(w => w.getID() === walletID);
const navigation = useNavigation();
const { colors } = useTheme();
const { enableBlur, disableBlur } = usePrivacy();
const stylesHook = StyleSheet.create({
flex: {
@ -38,10 +37,10 @@ const PleaseBackup: React.FC = () => {
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
enableBlur();
disallowScreenshot(true);
return () => {
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
disableBlur();
disallowScreenshot(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View file

@ -41,7 +41,7 @@ import prompt from '../../helpers/prompt';
import { scanQrHelper } from '../../helpers/scan-qr';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import ActionSheet from '../ActionSheet';
import { useStorage } from '../../hooks/context/useStorage';
@ -78,7 +78,6 @@ const ViewEditMultisigCosigners: React.FC = () => {
Why the container view ? It was the easiest to get the ref for. No other reason.
*/
const discardChangesRef = useRef<View>(null);
const { enableBlur, disableBlur } = usePrivacy();
const stylesHook = StyleSheet.create({
root: {
@ -190,7 +189,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
if (hasLoaded.current) return;
setIsLoading(true);
enableBlur();
disallowScreenshot(true);
const task = InteractionManager.runAfterInteractions(async () => {
if (!w.current) {
@ -206,7 +205,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
setIsLoading(false);
});
return () => {
disableBlur();
disallowScreenshot(false);
task.cancel();
};
// eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -4,7 +4,7 @@ import { ActivityIndicator, FlatList, StyleSheet, View, Platform, UIManager } fr
import { WatchOnlyWallet } from '../../class';
import { AddressItem } from '../../components/addresses/AddressItem';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
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';
@ -131,7 +131,6 @@ const WalletAddresses: React.FC = () => {
const { colors } = useTheme();
const { setOptions } = useExtendedNavigation<NavigationProps>();
const { enableBlur, disableBlur } = usePrivacy();
const stylesHook = StyleSheet.create({
root: {
@ -177,12 +176,12 @@ const WalletAddresses: React.FC = () => {
useFocusEffect(
useCallback(() => {
enableBlur();
disallowScreenshot(true);
getAddresses();
return () => {
disableBlur();
disallowScreenshot(false);
};
}, [enableBlur, disableBlur, getAddresses]),
}, [getAddresses]),
);
const data =

View file

@ -8,7 +8,7 @@ import HandOffComponent from '../../components/HandOffComponent';
import QRCodeComponent from '../../components/QRCodeComponent';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
@ -25,7 +25,6 @@ const WalletExport: React.FC = () => {
const wallet = wallets.find(w => w.getID() === walletID);
const [qrCodeSize, setQRCodeSize] = useState(90);
const appState = useRef(AppState.currentState);
const { enableBlur, disableBlur } = usePrivacy();
useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
@ -55,7 +54,7 @@ const WalletExport: React.FC = () => {
useFocusEffect(
useCallback(() => {
enableBlur();
disallowScreenshot(true);
const task = InteractionManager.runAfterInteractions(async () => {
if (wallet) {
if (!wallet.getUserHasSavedExport()) {
@ -67,7 +66,7 @@ const WalletExport: React.FC = () => {
});
return () => {
task.cancel();
disableBlur();
disallowScreenshot(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]),

View file

@ -29,7 +29,7 @@ import QRCodeComponent from '../../components/QRCodeComponent';
import { useTheme } from '../../components/themes';
import confirm from '../../helpers/confirm';
import prompt from '../../helpers/prompt';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { scanQrHelper } from '../../helpers/scan-qr';
@ -60,15 +60,14 @@ const WalletsAddMultisigStep2 = () => {
const [askPassphrase, setAskPassphrase] = useState(false);
const openScannerButton = useRef();
const data = useRef(new Array(n));
const { enableBlur, disableBlur } = usePrivacy();
useFocusEffect(
useCallback(() => {
enableBlur();
disallowScreenshot(true);
return () => {
disableBlur();
disallowScreenshot(false);
};
}, [disableBlur, enableBlur]),
}, []),
);
useEffect(() => {

View file

@ -5,7 +5,7 @@ import { BlueButtonLink, BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import {
DoneAndDismissKeyboardInputAccessory,
@ -30,7 +30,6 @@ const WalletsImport = () => {
const [searchAccountsMenuState, setSearchAccountsMenuState] = useState(false);
const [askPassphraseMenuState, setAskPassphraseMenuState] = useState(false);
const [clearClipboardMenuState, setClearClipboardMenuState] = useState(true);
const { enableBlur, disableBlur } = usePrivacy();
// Styles
const styles = StyleSheet.create({
@ -62,11 +61,11 @@ const WalletsImport = () => {
});
useEffect(() => {
enableBlur();
disallowScreenshot(true);
return () => {
disableBlur();
disallowScreenshot(false);
};
}, [disableBlur, enableBlur]);
}, []);
useEffect(() => {
if (triggerImport) importButtonPressed();

View file

@ -7,7 +7,7 @@ import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import QRCodeComponent from '../../components/QRCodeComponent';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
@ -18,7 +18,6 @@ const PleaseBackupLNDHub = () => {
const navigation = useNavigation();
const { colors } = useTheme();
const [qrCodeSize, setQRCodeSize] = useState(90);
const { enableBlur, disableBlur } = usePrivacy();
const handleBackButton = useCallback(() => {
navigation.getParent().pop();
@ -39,13 +38,13 @@ const PleaseBackupLNDHub = () => {
});
useEffect(() => {
enableBlur();
disallowScreenshot(true);
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
return () => {
disableBlur();
disallowScreenshot(false);
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
};
}, [disableBlur, enableBlur, handleBackButton]);
}, [handleBackButton]);
const pop = () => navigation.getParent().pop();

View file

@ -8,7 +8,7 @@ import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import HandOffComponent from '../../components/HandOffComponent';
import QRCodeComponent from '../../components/QRCodeComponent';
import SafeArea from '../../components/SafeArea';
import usePrivacy from '../../hooks/usePrivacy';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { styles, useDynamicStyles } from './xpub.styles';
import { useStorage } from '../../hooks/context/useStorage';
@ -33,15 +33,14 @@ const WalletXpub: React.FC = () => {
const stylesHook = useDynamicStyles(); // This now includes the theme implicitly
const [qrCodeSize, setQRCodeSize] = useState<number>(90);
const lastWalletIdRef = useRef<string | undefined>();
const { enableBlur, disableBlur } = usePrivacy();
useFocusEffect(
useCallback(() => {
disallowScreenshot(true);
// Skip execution if walletID hasn't changed
if (lastWalletIdRef.current === walletID) {
return;
}
enableBlur();
const task = InteractionManager.runAfterInteractions(async () => {
if (wallet) {
const walletXpub = wallet.getXpub();
@ -57,9 +56,9 @@ const WalletXpub: React.FC = () => {
lastWalletIdRef.current = walletID;
return () => {
task.cancel();
disableBlur();
disallowScreenshot(false);
};
}, [walletID, enableBlur, wallet, xpub, navigation, disableBlur]),
}, [walletID, wallet, xpub, navigation]),
);
useEffect(() => {

View file

@ -153,7 +153,8 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('IsItMyAddress')).tap();
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('CheckAddress')).tap();
await expect(element(by.id('Result'))).toHaveText('None of the available wallets own the provided address.');
await expect(element(by.text('None of the available wallets own the provided address.'))).toBeVisible();
await element(by.text('OK')).tap();
await device.pressBack();
await device.pressBack();

View file

@ -187,52 +187,70 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await device.launchApp({ newInstance: true });
// go inside the wallet
// Go inside the wallet
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
// lets create real transaction:
// Add a few recipients initially
await element(by.id('AddressInput')).replaceText('bc1qnapskphjnwzw2w3dk4anpxntunc77v6qrua0f7');
await element(by.id('BitcoinAmountInput')).replaceText('0.0001\n');
// setting fee rate:
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction1');
await element(by.id('AddressInput').withAncestor(by.id('Transaction1'))).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput').withAncestor(by.id('Transaction1'))).replaceText('0.0002\n');
// Now remove all recipients before proceeding
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Remove All Recipients')).tap();
await element(by.text('OK')).tap();
// Now, let's proceed with the batch send process again
// Let's create a real transaction again:
await element(by.id('AddressInput')).replaceText('bc1qnapskphjnwzw2w3dk4anpxntunc77v6qrua0f7');
await element(by.id('BitcoinAmountInput')).replaceText('0.0001\n');
// Setting fee rate:
const feeRate = 2;
await element(by.id('chooseFee')).tap();
await element(by.id('feeCustom')).tap();
await element(by.type('android.widget.EditText')).typeText(feeRate + '\n');
await element(by.text('OK')).tap();
// lest add another two outputs
// Let's add another two outputs
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction1'); // adding a recipient autoscrolls it to the last one
await yo('Transaction1'); // Adding a recipient autoscrolls it to the last one
await element(by.id('AddressInput').withAncestor(by.id('Transaction1'))).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput').withAncestor(by.id('Transaction1'))).replaceText('0.0002\n');
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction2'); // adding a recipient autoscrolls it to the last one
await yo('Transaction2'); // Adding a recipient autoscrolls it to the last one
await element(by.id('AddressInput').withAncestor(by.id('Transaction2'))).replaceText('bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7');
await element(by.id('BitcoinAmountInput').withAncestor(by.id('Transaction2'))).replaceText('0.0003\n');
// remove last output, check if second output is shown
// Remove last output, check if second output is shown
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Remove Recipient')).tap();
await yo('Transaction1');
// adding it again
// Add it again
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction2'); // adding a recipient autoscrolls it to the last one
await yo('Transaction2'); // Adding a recipient autoscrolls it to the last one
await element(by.id('AddressInput').withAncestor(by.id('Transaction2'))).replaceText('bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7');
await element(by.id('BitcoinAmountInput').withAncestor(by.id('Transaction2'))).replaceText('0.0003\n');
// remove second output
// Remove second output
await element(by.id('Transaction2')).swipe('right', 'fast', NaN, 0.2);
await sleep(5000);
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Remove Recipient')).tap();
// creating and verifying. tx should have 3 outputs
// Creating and verifying. tx should have 3 outputs
if (process.env.TRAVIS) await sleep(5000);
try {
await element(by.id('CreateTransactionButton')).tap();
@ -599,8 +617,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('Details')).tap();
await expect(element(by.text('8b0ab2c7196312e021e0d3dc73f801693826428782970763df6134457bd2ec20'))).toBeVisible();
await element(by.type('android.widget.EditText')).typeText('test1');
await element(by.text('Save')).tap();
await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).tapReturnKey();
// Terminate and reopen the app to confirm the note is persisted
await device.launchApp({ newInstance: true });

View file

@ -159,12 +159,6 @@ jest.mock('realm', () => {
};
});
jest.mock('react-native-idle-timer', () => {
return {
setIdleTimerDisabled: jest.fn(),
};
});
jest.mock('react-native-ios-context-menu', () => {
return {};
});

View file

@ -20,6 +20,9 @@ const keys = {
SaveChanges: 'saveChanges',
ClearClipboard: 'clearClipboard',
PaymentsCode: 'paymentsCode',
RemoveAllRecipients: 'RemoveAllRecipients',
AddRecipient: 'AddRecipient',
RemoveRecipient: 'RemoveRecipient',
};
const icons = {
@ -71,6 +74,9 @@ const icons = {
PaymentsCode: {
iconValue: 'qrcode',
},
RemoveAllRecipients: { iconValue: 'person.2.slash' },
AddRecipient: { iconValue: 'person.badge.plus' },
RemoveRecipient: { iconValue: 'person.badge.minus' },
};
export const CommonToolTipActions = {
@ -99,6 +105,16 @@ export const CommonToolTipActions = {
text: loc.transactions.details_copy_amount,
icon: icons.Clipboard,
},
AddRecipient: {
id: keys.AddRecipient,
text: loc.send.details_add_rec_add,
icon: icons.AddRecipient,
},
RemoveRecipient: {
id: keys.RemoveRecipient,
text: loc.send.details_add_rec_rem,
icon: icons.RemoveRecipient,
},
CopyNote: {
id: keys.CopyNote,
text: loc.transactions.details_copy_note,
@ -139,6 +155,11 @@ export const CommonToolTipActions = {
text: loc.wallets.add_entropy_provide,
icon: icons.Entropy,
},
RemoveAllRecipients: {
id: keys.RemoveAllRecipients,
text: loc.send.details_add_rec_rem_all,
icon: icons.RemoveAllRecipients,
},
SearchAccount: {
id: keys.SearchAccount,
text: loc.wallets.import_search_accounts,