mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 07:15:35 +01:00
Merge branch 'master' into bottommodalheader
This commit is contained in:
commit
c1433caba9
372 changed files with 13354 additions and 4106 deletions
|
@ -19,6 +19,7 @@
|
|||
"react/display-name": "off",
|
||||
"react-native/no-inline-styles": "error",
|
||||
"react-native/no-unused-styles": "error",
|
||||
"react/no-is-mounted": "off",
|
||||
"react-native/no-single-element-style-arrays": "error",
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
|
|
|
@ -12,7 +12,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 180
|
||||
outputs:
|
||||
new_build_number: ${{ steps.generate_build_number.outputs.build_number }}
|
||||
|
@ -25,29 +25,36 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetches all history
|
||||
|
||||
|
||||
- name: Specify node version
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: 15.4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install dependencies with Bundler
|
||||
run: bundle install
|
||||
run: |
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3 --quiet
|
||||
|
||||
- name: Install node_modules
|
||||
run: npm install
|
||||
run: npm install --omit=dev --yes
|
||||
|
||||
- name: Install CocoaPods Dependencies
|
||||
run: |
|
||||
gem install cocoapods
|
||||
bundle exec pod install
|
||||
working-directory: ./ios
|
||||
bundle exec fastlane ios install_pods
|
||||
- name: Cache CocoaPods Pods
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ios/Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
|
@ -67,7 +74,6 @@ jobs:
|
|||
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
|
||||
working-directory: ./ios
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
- name: Setup Provisioning Profiles
|
||||
|
@ -80,10 +86,9 @@ jobs:
|
|||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane ios setup_provisioning_profiles
|
||||
working-directory: ./ios
|
||||
- name: Cache Provisioning Profiles
|
||||
id: cache_provisioning_profiles
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/Library/MobileDevice/Provisioning Profiles
|
||||
key: ${{ runner.os }}-provisioning-profiles-${{ github.sha }}
|
||||
|
@ -112,28 +117,28 @@ jobs:
|
|||
echo "::set-output name=build_number::$NEW_BUILD_NUMBER"
|
||||
- name: Set Build Number
|
||||
run: bundle exec fastlane ios increment_build_number_lane
|
||||
working-directory: ./ios
|
||||
- name: Determine Marketing Version
|
||||
id: determine_marketing_version
|
||||
run: |
|
||||
MARKETING_VERSION=$(grep MARKETING_VERSION ios/BlueWallet.xcodeproj/project.pbxproj | awk -F '= ' '{print $2}' | tr -d ' ;' | head -1)
|
||||
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
|
||||
working-directory: ./ios
|
||||
- name: Upload IPA as Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
|
||||
path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
|
||||
path: ./build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
|
||||
|
||||
testflight-upload:
|
||||
needs: build
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight')
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
|
@ -149,7 +154,7 @@ jobs:
|
|||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
- name: Cache Ruby Gems
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
|
@ -160,15 +165,15 @@ jobs:
|
|||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3 --quiet
|
||||
- name: Download IPA from Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa
|
||||
path: ./ios/build
|
||||
path: ./
|
||||
- name: Create App Store Connect API Key JSON
|
||||
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./ios/appstore_api_key.json
|
||||
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json
|
||||
- name: Upload to TestFlight
|
||||
env:
|
||||
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/ios/appstore_api_key.p8
|
||||
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8
|
||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
|
||||
GIT_URL: ${{ secrets.GIT_URL }}
|
||||
|
@ -178,7 +183,6 @@ jobs:
|
|||
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
|
||||
working-directory: ./ios
|
||||
- name: Post PR Comment
|
||||
if: success() && github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
|
|
110
.github/workflows/build-release-apk.yml
vendored
110
.github/workflows/build-release-apk.yml
vendored
|
@ -1,19 +1,21 @@
|
|||
name: BuildReleaseApk
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
buildReleaseApk:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
|
@ -23,7 +25,7 @@ jobs:
|
|||
node-version: 20
|
||||
|
||||
- name: Use npm caches
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
|
@ -31,37 +33,103 @@ jobs:
|
|||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Use specific Java version for sdkmanager to work
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Install node_modules
|
||||
run: npm install --production
|
||||
run: npm install --omit=dev --yes
|
||||
|
||||
- name: Extract Version Name
|
||||
id: version_name
|
||||
run: |
|
||||
VERSION_NAME=$(grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '"')
|
||||
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
|
||||
echo "::set-output name=version_name::$VERSION_NAME"
|
||||
- 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:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gems-
|
||||
|
||||
- name: Generate Build Number based on timestamp
|
||||
id: build_number
|
||||
run: |
|
||||
NEW_BUILD_NUMBER=$(date +%s)
|
||||
NEW_BUILD_NUMBER="$(date +%s)"
|
||||
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
|
||||
echo "::set-output name=build_number::$NEW_BUILD_NUMBER"
|
||||
|
||||
- name: Build
|
||||
- name: Prepare Keystore
|
||||
run: bundle exec fastlane android prepare_keystore
|
||||
env:
|
||||
KEYSTORE_FILE_HEX: ${{ secrets.KEYSTORE_FILE_HEX }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
|
||||
run: ./scripts/build-release-apk.sh
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: success()
|
||||
- name: Update Version Code, Build, and Sign APK
|
||||
id: build_and_sign_apk
|
||||
run: |
|
||||
bundle exec fastlane android update_version_build_and_sign_apk
|
||||
env:
|
||||
BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
|
||||
- name: Determine APK Filename and Path
|
||||
id: determine_apk_path
|
||||
run: |
|
||||
VERSION_NAME=$(grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '"')
|
||||
BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}
|
||||
BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9_-]/-/g')
|
||||
|
||||
if [ -n "$BRANCH_NAME" ] && [ "$BRANCH_NAME" != "master" ]; then
|
||||
EXPECTED_FILENAME="BlueWallet-${VERSION_NAME}-${NEW_BUILD_NUMBER}-${BRANCH_NAME}.apk"
|
||||
else
|
||||
EXPECTED_FILENAME="BlueWallet-${VERSION_NAME}-${NEW_BUILD_NUMBER}.apk"
|
||||
fi
|
||||
|
||||
APK_PATH="android/app/build/outputs/apk/release/${EXPECTED_FILENAME}"
|
||||
echo "EXPECTED_FILENAME=${EXPECTED_FILENAME}" >> $GITHUB_ENV
|
||||
echo "APK_PATH=${APK_PATH}" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: BlueWallet-${{ env.VERSION_NAME }}(${{ env.NEW_BUILD_NUMBER }}).apk
|
||||
path: ./android/app/build/outputs/apk/release/BlueWallet-${{ env.VERSION_NAME }}(${{ env.NEW_BUILD_NUMBER }}).apk
|
||||
name: signed-apk
|
||||
path: ${{ env.APK_PATH }}
|
||||
|
||||
browserstack:
|
||||
runs-on: ubuntu-latest
|
||||
needs: buildReleaseApk
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install dependencies with Bundler
|
||||
run: bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Download APK artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signed-apk
|
||||
|
||||
- name: Set APK Path
|
||||
run: |
|
||||
APK_PATH=$(find ${{ github.workspace }} -name '*.apk')
|
||||
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK to BrowserStack and Post PR Comment
|
||||
env:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
|
||||
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
|
||||
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: bundle exec fastlane upload_to_browserstack_and_comment
|
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
@ -30,16 +30,9 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Use node_modules caches
|
||||
id: cache-nm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-nm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install node_modules
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
run: npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test || npm test || npm test || npm test
|
||||
|
@ -55,6 +48,9 @@ jobs:
|
|||
MNEMONICS_COLDCARD: ${{ secrets.MNEMONICS_COLDCARD }}
|
||||
RETRY: 1
|
||||
|
||||
- name: Prune devDependencies
|
||||
run: npm prune --omit=dev
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
97
.github/workflows/lockfiles_update.yml
vendored
Normal file
97
.github/workflows/lockfiles_update.yml
vendored
Normal file
|
@ -0,0 +1,97 @@
|
|||
name: Lock Files Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
pod-update:
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
||||
- name: Checkout master branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master # Ensures we're checking out the master branch
|
||||
fetch-depth: 0 # Ensures full history to enable branch deletion and recreation
|
||||
|
||||
- name: Delete existing branch
|
||||
run: |
|
||||
git push origin --delete pod-update-branch || echo "Branch does not exist, continuing..."
|
||||
git branch -D pod-update-branch || echo "Local branch does not exist, continuing..."
|
||||
|
||||
- name: Create new branch from master
|
||||
run: git checkout -b pod-update-branch # Create a new branch from the master branch
|
||||
|
||||
- name: Specify node version
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install node modules
|
||||
run: npm install
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install and update Ruby Gems
|
||||
run: |
|
||||
bundle install
|
||||
|
||||
- name: Install CocoaPods Dependencies
|
||||
run: |
|
||||
cd ios
|
||||
pod install
|
||||
pod update
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
git diff --quiet package-lock.json ios/Podfile.lock || echo "Changes detected"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stop job if no changes
|
||||
if: steps.check-changes.outcome == 'success'
|
||||
run: |
|
||||
echo "No changes detected in package-lock.json or Podfile.lock. Stopping the job."
|
||||
exit 0
|
||||
|
||||
- name: Commit changes
|
||||
if: steps.check-changes.outcome != 'success'
|
||||
run: |
|
||||
git add package-lock.json ios/Podfile.lock
|
||||
git commit -m "Update lock files"
|
||||
|
||||
# Step 10: Get the list of changed files for PR description
|
||||
- name: Get changed files for PR description
|
||||
id: get-changes
|
||||
if: steps.check-changes.outcome != 'success'
|
||||
run: |
|
||||
git diff --name-only HEAD^ HEAD > changed_files.txt
|
||||
echo "CHANGES=$(cat changed_files.txt)" >> $GITHUB_ENV
|
||||
|
||||
# Step 11: Push the changes and create the PR using the LockFiles PAT
|
||||
- name: Push and create PR
|
||||
if: steps.check-changes.outcome != 'success'
|
||||
run: |
|
||||
git push origin pod-update-branch
|
||||
gh pr create --title "Lock Files Updates" --body "The following lock files were updated:\n\n${{ env.CHANGES }}" --base master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.LOCKFILES_WORKFLOW }} # Use the LockFiles PAT for PR creation
|
||||
|
||||
cleanup:
|
||||
runs-on: macos-latest
|
||||
if: github.event.pull_request.merged == true || github.event.pull_request.state == 'closed'
|
||||
needs: pod-update
|
||||
steps:
|
||||
|
||||
- name: Delete branch after PR merge/close
|
||||
run: |
|
||||
git push origin --delete pod-update-branch
|
|
@ -1,22 +1,8 @@
|
|||
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import React, { forwardRef } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
I18nManager,
|
||||
InputAccessoryView,
|
||||
Keyboard,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { ActivityIndicator, Dimensions, I18nManager, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { Icon, Text } from '@rneui/themed';
|
||||
|
||||
import { useTheme } from './components/themes';
|
||||
import loc from './loc';
|
||||
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const aspectRatio = height / width;
|
||||
|
@ -38,8 +24,8 @@ export const BlueButtonLink = forwardRef((props, ref) => {
|
|||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
style={{
|
||||
minHeight: 60,
|
||||
minWidth: 100,
|
||||
minHeight: 36,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
{...props}
|
||||
|
@ -137,60 +123,6 @@ export const BlueSpacing10 = props => {
|
|||
return <View {...props} style={{ height: 10, opacity: 0 }} />;
|
||||
};
|
||||
|
||||
export const BlueDismissKeyboardInputAccessory = () => {
|
||||
const { colors } = useTheme();
|
||||
BlueDismissKeyboardInputAccessory.InputAccessoryViewID = 'BlueDismissKeyboardInputAccessory';
|
||||
|
||||
return Platform.OS !== 'ios' ? null : (
|
||||
<InputAccessoryView nativeID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
height: 44,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<BlueButtonLink title={loc.send.input_done} onPress={Keyboard.dismiss} />
|
||||
</View>
|
||||
</InputAccessoryView>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlueDoneAndDismissKeyboardInputAccessory = props => {
|
||||
const { colors } = useTheme();
|
||||
BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID = 'BlueDoneAndDismissKeyboardInputAccessory';
|
||||
|
||||
const onPasteTapped = async () => {
|
||||
const clipboard = await Clipboard.getString();
|
||||
props.onPasteTapped(clipboard);
|
||||
};
|
||||
|
||||
const inputView = (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
maxHeight: 44,
|
||||
}}
|
||||
>
|
||||
<BlueButtonLink title={loc.send.input_clear} onPress={props.onClearTapped} />
|
||||
<BlueButtonLink title={loc.send.input_paste} onPress={onPasteTapped} />
|
||||
<BlueButtonLink title={loc.send.input_done} onPress={Keyboard.dismiss} />
|
||||
</View>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return <InputAccessoryView nativeID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
|
||||
} else {
|
||||
return inputView;
|
||||
}
|
||||
};
|
||||
|
||||
export const BlueLoading = props => {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center' }} {...props}>
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -6,3 +6,5 @@ gem 'rubyzip', '2.3.2'
|
|||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem "fastlane"
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
|
28
Gemfile.lock
28
Gemfile.lock
|
@ -24,17 +24,17 @@ GEM
|
|||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.970.0)
|
||||
aws-sdk-core (3.202.2)
|
||||
aws-partitions (1.973.0)
|
||||
aws-sdk-core (3.204.0)
|
||||
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.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (1.90.0)
|
||||
aws-sdk-core (~> 3, >= 3.203.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.159.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-s3 (1.161.0)
|
||||
aws-sdk-core (~> 3, >= 3.203.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
|
@ -167,6 +167,8 @@ GEM
|
|||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-browserstack (0.3.3)
|
||||
rest-client (~> 2.0, >= 2.0.2)
|
||||
ffi (1.17.0)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
|
@ -208,6 +210,7 @@ GEM
|
|||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.7)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
|
@ -218,6 +221,9 @@ GEM
|
|||
jwt (2.8.2)
|
||||
base64
|
||||
logger (1.6.1)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.0903)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.1)
|
||||
|
@ -238,9 +244,13 @@ GEM
|
|||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
rest-client (2.1.0)
|
||||
http-accept (>= 1.7.0, < 2.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rexml (3.3.7)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
|
@ -255,7 +265,6 @@ GEM
|
|||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
|
@ -290,6 +299,7 @@ DEPENDENCIES
|
|||
activesupport (>= 6.1.7.5, != 7.1.0)
|
||||
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
|
||||
fastlane
|
||||
fastlane-plugin-browserstack
|
||||
rubyzip (= 2.3.2)
|
||||
|
||||
RUBY VERSION
|
||||
|
|
|
@ -83,7 +83,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "7.0.4"
|
||||
versionName "7.0.5"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
|
@ -52,6 +52,12 @@
|
|||
<meta-data
|
||||
android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||
android:resource="@color/white" />
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="false" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
||||
|
|
|
@ -28,8 +28,8 @@ class BitcoinPriceWidget : AppWidgetProvider() {
|
|||
}
|
||||
|
||||
private fun clearCache(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
|
||||
sharedPref.edit().clear().apply()
|
||||
Log.d("BitcoinPriceWidget", "Cache cleared")
|
||||
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
sharedPref.edit().clear().apply() // Clear all preferences in the group
|
||||
Log.d("BitcoinPriceWidget", "Cache cleared from group.io.bluewallet.bluewallet")
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import android.util.Log
|
|||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.work.*
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
@ -33,17 +34,22 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
}
|
||||
}
|
||||
|
||||
private lateinit var sharedPref: SharedPreferences
|
||||
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
|
||||
override fun doWork(): Result {
|
||||
Log.d(TAG, "Widget update worker running")
|
||||
|
||||
sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
registerPreferenceChangeListener()
|
||||
|
||||
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
|
||||
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
|
||||
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
|
||||
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
|
||||
|
||||
val sharedPref = applicationContext.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
|
||||
val preferredCurrency = sharedPref.getString("preferredCurrency", "USD")
|
||||
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", "en-US")
|
||||
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
|
||||
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
|
||||
val previousPrice = sharedPref.getString("previous_price", null)
|
||||
|
||||
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
|
||||
|
@ -51,13 +57,47 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
fetchPrice(preferredCurrency) { fetchedPrice, error ->
|
||||
handlePriceResult(
|
||||
appWidgetManager, appWidgetIds, views, sharedPref,
|
||||
fetchedPrice, previousPrice, currentTime, preferredCurrencyLocale, error
|
||||
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun registerPreferenceChangeListener() {
|
||||
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
if (key == "preferredCurrency" || key == "preferredCurrencyLocale" || key == "previous_price") {
|
||||
Log.d(TAG, "Preference changed: $key")
|
||||
updateWidgetOnPreferenceChange()
|
||||
}
|
||||
}
|
||||
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
super.onStopped()
|
||||
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
}
|
||||
|
||||
private fun updateWidgetOnPreferenceChange() {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
|
||||
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
|
||||
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
|
||||
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
|
||||
|
||||
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
|
||||
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
|
||||
val previousPrice = sharedPref.getString("previous_price", null)
|
||||
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
|
||||
|
||||
fetchPrice(preferredCurrency) { fetchedPrice, error ->
|
||||
handlePriceResult(
|
||||
appWidgetManager, appWidgetIds, views, sharedPref,
|
||||
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePriceResult(
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
|
@ -66,6 +106,7 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
fetchedPrice: String?,
|
||||
previousPrice: String?,
|
||||
currentTime: String,
|
||||
preferredCurrency: String?,
|
||||
preferredCurrencyLocale: String?,
|
||||
error: String?
|
||||
) {
|
||||
|
@ -77,11 +118,11 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
if (!isPriceCached) {
|
||||
showLoadingError(views)
|
||||
} else {
|
||||
displayCachedPrice(views, previousPrice, currentTime, preferredCurrencyLocale)
|
||||
displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale)
|
||||
}
|
||||
} else {
|
||||
displayFetchedPrice(
|
||||
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrencyLocale
|
||||
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
|
||||
)
|
||||
savePrice(sharedPref, fetchedPrice)
|
||||
}
|
||||
|
@ -103,11 +144,10 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
views: RemoteViews,
|
||||
previousPrice: String?,
|
||||
currentTime: String,
|
||||
preferredCurrency: String?,
|
||||
preferredCurrencyLocale: String?
|
||||
) {
|
||||
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.forLanguageTag(preferredCurrencyLocale!!)).apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
val currencyFormat = getCurrencyFormat(preferredCurrency, preferredCurrencyLocale)
|
||||
|
||||
views.apply {
|
||||
setViewVisibility(R.id.loading_indicator, View.GONE)
|
||||
|
@ -125,12 +165,11 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
fetchedPrice: String,
|
||||
previousPrice: String?,
|
||||
currentTime: String,
|
||||
preferredCurrency: String?,
|
||||
preferredCurrencyLocale: String?
|
||||
) {
|
||||
val currentPrice = fetchedPrice.toDouble().let { it.toInt() } // Remove cents
|
||||
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.forLanguageTag(preferredCurrencyLocale!!)).apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
val currentPrice = fetchedPrice.toDouble().toInt() // Remove cents
|
||||
val currencyFormat = getCurrencyFormat(preferredCurrency, preferredCurrencyLocale)
|
||||
|
||||
views.apply {
|
||||
setViewVisibility(R.id.loading_indicator, View.GONE)
|
||||
|
@ -153,6 +192,30 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyFormat(currencyCode: String?, localeString: String?): NumberFormat {
|
||||
val localeParts = localeString?.split("-") ?: listOf("en", "US")
|
||||
val locale = if (localeParts.size == 2) {
|
||||
Locale(localeParts[0], localeParts[1])
|
||||
} else {
|
||||
Locale.getDefault()
|
||||
}
|
||||
val currencyFormat = NumberFormat.getCurrencyInstance(locale)
|
||||
val currency = try {
|
||||
Currency.getInstance(currencyCode ?: "USD")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Currency.getInstance("USD") // Default to USD if an invalid code is provided
|
||||
}
|
||||
currencyFormat.currency = currency
|
||||
currencyFormat.maximumFractionDigits = 0 // No cents
|
||||
|
||||
// Remove the ISO country code and keep only the symbol
|
||||
val decimalFormatSymbols = (currencyFormat as java.text.DecimalFormat).decimalFormatSymbols
|
||||
decimalFormatSymbols.currencySymbol = currency.symbol
|
||||
currencyFormat.decimalFormatSymbols = decimalFormatSymbols
|
||||
|
||||
return currencyFormat
|
||||
}
|
||||
|
||||
private fun fetchPrice(currency: String?, callback: (String?, String?) -> Unit) {
|
||||
val price = MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
|
||||
if (price == null) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_layout"
|
||||
android:minWidth="160dp"
|
||||
android:minHeight="80dp"
|
||||
android:minHeight="100dp"
|
||||
android:updatePeriodMillis="0"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@drawable/widget_preview"
|
||||
|
|
|
@ -93,7 +93,10 @@ export const writeFileAndExport = async function (fileName: string, contents: st
|
|||
export const openSignedTransaction = async function (): Promise<string | false> {
|
||||
try {
|
||||
const res = await DocumentPicker.pickSingle({
|
||||
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
|
||||
type:
|
||||
Platform.OS === 'ios'
|
||||
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.json]
|
||||
: [DocumentPicker.types.allFiles],
|
||||
});
|
||||
|
||||
return await _readPsbtFileIntoBase64(res.uri);
|
||||
|
@ -160,7 +163,7 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
|
|||
'io.bluewallet.psbt.txn',
|
||||
'io.bluewallet.backup',
|
||||
DocumentPicker.types.plainText,
|
||||
'public.json',
|
||||
DocumentPicker.types.json,
|
||||
DocumentPicker.types.images,
|
||||
]
|
||||
: [DocumentPicker.types.allFiles],
|
||||
|
|
|
@ -109,7 +109,7 @@ function Notifications(props) {
|
|||
* - if you are not using remote notification or do not have Firebase installed, use this:
|
||||
* requestPermissions: Platform.OS === 'ios'
|
||||
*/
|
||||
requestPermissions: true,
|
||||
requestPermissions: Platform.OS === 'ios',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -67,13 +67,12 @@ const isReactNative = typeof navigator !== 'undefined' && navigator?.product ===
|
|||
export class BlueApp {
|
||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
||||
static LNDHUB = 'lndhub';
|
||||
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
||||
static DO_NOT_TRACK = 'donottrack';
|
||||
static HANDOFF_STORAGE_KEY = 'HandOff';
|
||||
|
||||
private static _instance: BlueApp | null = null;
|
||||
|
||||
static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK, BlueApp.ADVANCED_MODE_ENABLED];
|
||||
static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK];
|
||||
|
||||
public cachedPassword?: false | string;
|
||||
public tx_metadata: TTXMetadata;
|
||||
|
@ -882,17 +881,6 @@ export class BlueApp {
|
|||
return finalBalance;
|
||||
};
|
||||
|
||||
isAdvancedModeEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(BlueApp.ADVANCED_MODE_ENABLED));
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
setIsAdvancedModeEnabled = async (value: boolean) => {
|
||||
await AsyncStorage.setItem(BlueApp.ADVANCED_MODE_ENABLED, value ? '1' : '');
|
||||
};
|
||||
|
||||
isHandoffEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(BlueApp.HANDOFF_STORAGE_KEY));
|
||||
|
|
|
@ -3,11 +3,51 @@ import bolt11 from 'bolt11';
|
|||
import createHash from 'create-hash';
|
||||
import { createHmac } from 'crypto';
|
||||
import CryptoJS from 'crypto-js';
|
||||
// @ts-ignore theres no types for secp256k1
|
||||
import secp256k1 from 'secp256k1';
|
||||
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
|
||||
|
||||
const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL
|
||||
|
||||
interface LnurlPayServicePayload {
|
||||
callback: string;
|
||||
fixed: boolean;
|
||||
min: number;
|
||||
max: number;
|
||||
domain: string;
|
||||
metadata: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
amount: number;
|
||||
commentAllowed?: number;
|
||||
}
|
||||
|
||||
interface LnurlPayServiceBolt11Payload {
|
||||
pr: string;
|
||||
successAction?: any;
|
||||
disposable?: boolean;
|
||||
tag: string;
|
||||
metadata: any;
|
||||
minSendable: number;
|
||||
maxSendable: number;
|
||||
callback: string;
|
||||
commentAllowed: number;
|
||||
}
|
||||
|
||||
interface DecodedInvoice {
|
||||
destination: string;
|
||||
num_satoshis: string;
|
||||
num_millisatoshis: string;
|
||||
timestamp: string;
|
||||
fallback_addr: string;
|
||||
route_hints: any[];
|
||||
payment_hash?: string;
|
||||
description_hash?: string;
|
||||
cltv_expiry?: string;
|
||||
expiry?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-pay.md
|
||||
*/
|
||||
|
@ -16,15 +56,21 @@ export default class Lnurl {
|
|||
static TAG_WITHDRAW_REQUEST = 'withdrawRequest'; // type of LNURL
|
||||
static TAG_LOGIN_REQUEST = 'login'; // type of LNURL
|
||||
|
||||
constructor(url, AsyncStorage) {
|
||||
this._lnurl = url;
|
||||
private _lnurl: string;
|
||||
private _lnurlPayServiceBolt11Payload: LnurlPayServiceBolt11Payload | false;
|
||||
private _lnurlPayServicePayload: LnurlPayServicePayload | false;
|
||||
private _AsyncStorage: any;
|
||||
private _preimage: string | false;
|
||||
|
||||
constructor(url: string | false, AsyncStorage?: any) {
|
||||
this._lnurl = url || '';
|
||||
this._lnurlPayServiceBolt11Payload = false;
|
||||
this._lnurlPayServicePayload = false;
|
||||
this._AsyncStorage = AsyncStorage;
|
||||
this._preimage = false;
|
||||
}
|
||||
|
||||
static findlnurl(bodyOfText) {
|
||||
static findlnurl(bodyOfText: string): string | null {
|
||||
const res = /^(?:http.*[&?]lightning=|lightning:)?(lnurl1[02-9ac-hj-np-z]+)/.exec(bodyOfText.toLowerCase());
|
||||
if (res) {
|
||||
return res[1];
|
||||
|
@ -32,7 +78,7 @@ export default class Lnurl {
|
|||
return null;
|
||||
}
|
||||
|
||||
static getUrlFromLnurl(lnurlExample) {
|
||||
static getUrlFromLnurl(lnurlExample: string): string | false {
|
||||
const found = Lnurl.findlnurl(lnurlExample);
|
||||
if (!found) {
|
||||
if (Lnurl.isLightningAddress(lnurlExample)) {
|
||||
|
@ -49,22 +95,22 @@ export default class Lnurl {
|
|||
return Buffer.from(bech32.fromWords(decoded.words)).toString();
|
||||
}
|
||||
|
||||
static isLnurl(url) {
|
||||
static isLnurl(url: string): boolean {
|
||||
return Lnurl.findlnurl(url) !== null;
|
||||
}
|
||||
|
||||
static isOnionUrl(url) {
|
||||
static isOnionUrl(url: string): boolean {
|
||||
return Lnurl.parseOnionUrl(url) !== null;
|
||||
}
|
||||
|
||||
static parseOnionUrl(url) {
|
||||
static parseOnionUrl(url: string): [string, string] | null {
|
||||
const match = url.match(ONION_REGEX);
|
||||
if (match === null) return null;
|
||||
const [, baseURI, path] = match;
|
||||
return [baseURI, path];
|
||||
}
|
||||
|
||||
async fetchGet(url) {
|
||||
async fetchGet(url: string): Promise<any> {
|
||||
const resp = await fetch(url, { method: 'GET' });
|
||||
if (resp.status >= 300) {
|
||||
throw new Error('Bad response from server');
|
||||
|
@ -76,14 +122,14 @@ export default class Lnurl {
|
|||
return reply;
|
||||
}
|
||||
|
||||
decodeInvoice(invoice) {
|
||||
decodeInvoice(invoice: string): DecodedInvoice {
|
||||
const { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
|
||||
|
||||
const decoded = {
|
||||
destination: payeeNodeKey,
|
||||
const decoded: DecodedInvoice = {
|
||||
destination: payeeNodeKey ?? '',
|
||||
num_satoshis: satoshis ? satoshis.toString() : '0',
|
||||
num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
|
||||
timestamp: timestamp.toString(),
|
||||
timestamp: timestamp?.toString() ?? '',
|
||||
fallback_addr: '',
|
||||
route_hints: [],
|
||||
};
|
||||
|
@ -92,10 +138,10 @@ export default class Lnurl {
|
|||
const { tagName, data } = tags[i];
|
||||
switch (tagName) {
|
||||
case 'payment_hash':
|
||||
decoded.payment_hash = data;
|
||||
decoded.payment_hash = String(data);
|
||||
break;
|
||||
case 'purpose_commit_hash':
|
||||
decoded.description_hash = data;
|
||||
decoded.description_hash = String(data);
|
||||
break;
|
||||
case 'min_final_cltv_expiry':
|
||||
decoded.cltv_expiry = data.toString();
|
||||
|
@ -104,21 +150,21 @@ export default class Lnurl {
|
|||
decoded.expiry = data.toString();
|
||||
break;
|
||||
case 'description':
|
||||
decoded.description = data;
|
||||
decoded.description = String(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!decoded.expiry) decoded.expiry = '3600'; // default
|
||||
|
||||
if (parseInt(decoded.num_satoshis, 10) === 0 && decoded.num_millisatoshis > 0) {
|
||||
decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString();
|
||||
if (parseInt(decoded.num_satoshis, 10) === 0 && parseInt(decoded.num_millisatoshis, 10) > 0) {
|
||||
decoded.num_satoshis = (parseInt(decoded.num_millisatoshis, 10) / 1000).toString();
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
async requestBolt11FromLnurlPayService(amountSat, comment = '') {
|
||||
async requestBolt11FromLnurlPayService(amountSat: number, comment: string = ''): Promise<LnurlPayServiceBolt11Payload> {
|
||||
if (!this._lnurlPayServicePayload) throw new Error('this._lnurlPayServicePayload is not set');
|
||||
if (!this._lnurlPayServicePayload.callback) throw new Error('this._lnurlPayServicePayload.callback is not set');
|
||||
if (amountSat < this._lnurlPayServicePayload.min || amountSat > this._lnurlPayServicePayload.max)
|
||||
|
@ -132,15 +178,13 @@ export default class Lnurl {
|
|||
);
|
||||
const nonce = Math.floor(Math.random() * 2e16).toString(16);
|
||||
const separator = this._lnurlPayServicePayload.callback.indexOf('?') === -1 ? '?' : '&';
|
||||
if (this.getCommentAllowed() && comment && comment.length > this.getCommentAllowed()) {
|
||||
comment = comment.substr(0, this.getCommentAllowed());
|
||||
if (this.getCommentAllowed() && comment && comment.length > (this.getCommentAllowed() as number)) {
|
||||
comment = comment.substr(0, this.getCommentAllowed() as number);
|
||||
}
|
||||
if (comment) comment = `&comment=${encodeURIComponent(comment)}`;
|
||||
const urlToFetch =
|
||||
this._lnurlPayServicePayload.callback + separator + 'amount=' + Math.floor(amountSat * 1000) + '&nonce=' + nonce + comment;
|
||||
this._lnurlPayServiceBolt11Payload = await this.fetchGet(urlToFetch);
|
||||
if (this._lnurlPayServiceBolt11Payload.status === 'ERROR')
|
||||
throw new Error(this._lnurlPayServiceBolt11Payload.reason || 'requestBolt11FromLnurlPayService() error');
|
||||
this._lnurlPayServiceBolt11Payload = (await this.fetchGet(urlToFetch)) as LnurlPayServiceBolt11Payload;
|
||||
|
||||
// check pr description_hash, amount etc:
|
||||
const decoded = this.decodeInvoice(this._lnurlPayServiceBolt11Payload.pr);
|
||||
|
@ -155,11 +199,12 @@ export default class Lnurl {
|
|||
return this._lnurlPayServiceBolt11Payload;
|
||||
}
|
||||
|
||||
async callLnurlPayService() {
|
||||
async callLnurlPayService(): Promise<LnurlPayServicePayload> {
|
||||
if (!this._lnurl) throw new Error('this._lnurl is not set');
|
||||
const url = Lnurl.getUrlFromLnurl(this._lnurl);
|
||||
if (!url) throw new Error('Invalid LNURL');
|
||||
// calling the url
|
||||
const reply = await this.fetchGet(url);
|
||||
const reply = (await this.fetchGet(url)) as LnurlPayServiceBolt11Payload;
|
||||
|
||||
if (reply.tag !== Lnurl.TAG_PAY_REQUEST) {
|
||||
throw new Error('lnurl-pay expected, found tag ' + reply.tag);
|
||||
|
@ -168,8 +213,8 @@ export default class Lnurl {
|
|||
const data = reply;
|
||||
|
||||
// parse metadata and extract things from it
|
||||
let image;
|
||||
let description;
|
||||
let image: string | undefined;
|
||||
let description: string | undefined;
|
||||
const kvs = JSON.parse(data.metadata);
|
||||
for (let i = 0; i < kvs.length; i++) {
|
||||
const [k, v] = kvs[i];
|
||||
|
@ -185,14 +230,15 @@ export default class Lnurl {
|
|||
}
|
||||
|
||||
// setting the payment screen with the parameters
|
||||
const min = Math.ceil((data.minSendable || 0) / 1000);
|
||||
const max = Math.floor(data.maxSendable / 1000);
|
||||
const min = Math.ceil((data.minSendable ?? 0) / 1000);
|
||||
const max = Math.floor((data.maxSendable ?? 0) / 1000);
|
||||
|
||||
this._lnurlPayServicePayload = {
|
||||
callback: data.callback,
|
||||
fixed: min === max,
|
||||
min,
|
||||
max,
|
||||
// @ts-ignore idk
|
||||
domain: data.callback.match(/^(https|http):\/\/([^/]+)\//)[2],
|
||||
metadata: data.metadata,
|
||||
description,
|
||||
|
@ -204,7 +250,7 @@ export default class Lnurl {
|
|||
return this._lnurlPayServicePayload;
|
||||
}
|
||||
|
||||
async loadSuccessfulPayment(paymentHash) {
|
||||
async loadSuccessfulPayment(paymentHash: string): Promise<boolean> {
|
||||
if (!paymentHash) throw new Error('No paymentHash provided');
|
||||
let data;
|
||||
try {
|
||||
|
@ -224,7 +270,7 @@ export default class Lnurl {
|
|||
return true;
|
||||
}
|
||||
|
||||
async storeSuccess(paymentHash, preimage) {
|
||||
async storeSuccess(paymentHash: string, preimage: string | { data: Buffer }): Promise<void> {
|
||||
if (typeof preimage === 'object') {
|
||||
preimage = Buffer.from(preimage.data).toString('hex');
|
||||
}
|
||||
|
@ -241,35 +287,39 @@ export default class Lnurl {
|
|||
);
|
||||
}
|
||||
|
||||
getSuccessAction() {
|
||||
return this._lnurlPayServiceBolt11Payload.successAction;
|
||||
getSuccessAction(): any | undefined {
|
||||
return this._lnurlPayServiceBolt11Payload && 'successAction' in this._lnurlPayServiceBolt11Payload
|
||||
? this._lnurlPayServiceBolt11Payload.successAction
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getDomain() {
|
||||
return this._lnurlPayServicePayload.domain;
|
||||
getDomain(): string | undefined {
|
||||
return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.domain : undefined;
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this._lnurlPayServicePayload.description;
|
||||
getDescription(): string | undefined {
|
||||
return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.description : undefined;
|
||||
}
|
||||
|
||||
getImage() {
|
||||
return this._lnurlPayServicePayload.image;
|
||||
getImage(): string | undefined {
|
||||
return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.image : undefined;
|
||||
}
|
||||
|
||||
getLnurl() {
|
||||
getLnurl(): string {
|
||||
return this._lnurl;
|
||||
}
|
||||
|
||||
getDisposable() {
|
||||
return this._lnurlPayServiceBolt11Payload.disposable;
|
||||
getDisposable(): boolean | undefined {
|
||||
return this._lnurlPayServiceBolt11Payload && 'disposable' in this._lnurlPayServiceBolt11Payload
|
||||
? this._lnurlPayServiceBolt11Payload.disposable
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getPreimage() {
|
||||
getPreimage(): string | false {
|
||||
return this._preimage;
|
||||
}
|
||||
|
||||
static decipherAES(ciphertextBase64, preimageHex, ivBase64) {
|
||||
static decipherAES(ciphertextBase64: string, preimageHex: string, ivBase64: string): string {
|
||||
const iv = CryptoJS.enc.Base64.parse(ivBase64);
|
||||
const key = CryptoJS.enc.Hex.parse(preimageHex);
|
||||
return CryptoJS.AES.decrypt(Buffer.from(ciphertextBase64, 'base64').toString('hex'), key, {
|
||||
|
@ -279,27 +329,30 @@ export default class Lnurl {
|
|||
}).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
getCommentAllowed() {
|
||||
return this?._lnurlPayServicePayload?.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed, 10) : false;
|
||||
getCommentAllowed(): number | false {
|
||||
if (!this._lnurlPayServicePayload) return false;
|
||||
return this._lnurlPayServicePayload.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed.toString(), 10) : false;
|
||||
}
|
||||
|
||||
getMin() {
|
||||
return this?._lnurlPayServicePayload?.min ? parseInt(this._lnurlPayServicePayload.min, 10) : false;
|
||||
getMin(): number | false {
|
||||
if (!this._lnurlPayServicePayload) return false;
|
||||
return this._lnurlPayServicePayload.min ? parseInt(this._lnurlPayServicePayload.min.toString(), 10) : false;
|
||||
}
|
||||
|
||||
getMax() {
|
||||
return this?._lnurlPayServicePayload?.max ? parseInt(this._lnurlPayServicePayload.max, 10) : false;
|
||||
getMax(): number | false {
|
||||
if (!this._lnurlPayServicePayload) return false;
|
||||
return this._lnurlPayServicePayload.max ? parseInt(this._lnurlPayServicePayload.max.toString(), 10) : false;
|
||||
}
|
||||
|
||||
getAmount() {
|
||||
getAmount(): number | false {
|
||||
return this.getMin();
|
||||
}
|
||||
|
||||
authenticate(secret) {
|
||||
authenticate(secret: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._lnurl) throw new Error('this._lnurl is not set');
|
||||
|
||||
const url = parse(Lnurl.getUrlFromLnurl(this._lnurl), true);
|
||||
const url = parse(Lnurl.getUrlFromLnurl(this._lnurl) || '', true);
|
||||
|
||||
const hmac = createHmac('sha256', secret);
|
||||
hmac.on('readable', async () => {
|
||||
|
@ -308,7 +361,7 @@ export default class Lnurl {
|
|||
if (!privateKey) return;
|
||||
const privateKeyBuf = Buffer.from(privateKey, 'hex');
|
||||
const publicKey = secp256k1.publicKeyCreate(privateKeyBuf);
|
||||
const signatureObj = secp256k1.sign(Buffer.from(url.query.k1, 'hex'), privateKeyBuf);
|
||||
const signatureObj = secp256k1.sign(Buffer.from(url.query.k1 as string, 'hex'), privateKeyBuf);
|
||||
const derSignature = secp256k1.signatureExport(signatureObj.signature);
|
||||
|
||||
const reply = await this.fetchGet(`${url.href}&sig=${derSignature.toString('hex')}&key=${publicKey.toString('hex')}`);
|
||||
|
@ -326,7 +379,7 @@ export default class Lnurl {
|
|||
});
|
||||
}
|
||||
|
||||
static isLightningAddress(address) {
|
||||
static isLightningAddress(address: string) {
|
||||
// ensure only 1 `@` present:
|
||||
if (address.split('@').length !== 2) return false;
|
||||
const splitted = address.split('@');
|
|
@ -5,6 +5,7 @@ import { LegacyWallet } from './legacy-wallet';
|
|||
export class LightningCustodianWallet extends LegacyWallet {
|
||||
static readonly type = 'lightningCustodianWallet';
|
||||
static readonly typeReadable = 'Lightning';
|
||||
static readonly subtitleReadable = 'LNDhub';
|
||||
// @ts-ignore: override
|
||||
public readonly type = LightningCustodianWallet.type;
|
||||
// @ts-ignore: override
|
||||
|
|
53
components/AddWalletButton.tsx
Normal file
53
components/AddWalletButton.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { StyleSheet, TouchableOpacity, GestureResponderEvent } from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
import { useTheme } from './themes';
|
||||
import ToolTipMenu from './TooltipMenu';
|
||||
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
|
||||
import loc from '../loc';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
|
||||
type AddWalletButtonProps = {
|
||||
onPress?: (event: GestureResponderEvent) => void;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ball: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => {
|
||||
const { colors } = useTheme();
|
||||
const stylesHook = StyleSheet.create({
|
||||
ball: {
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
const onPressMenuItem = useCallback((action: string) => {
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ImportWallet.id:
|
||||
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []);
|
||||
|
||||
return (
|
||||
<ToolTipMenu accessibilityRole="button" accessibilityLabel={loc.wallets.add_title} onPressMenuItem={onPressMenuItem} actions={actions}>
|
||||
<TouchableOpacity style={[styles.ball, stylesHook.ball]} onPress={onPress}>
|
||||
<Icon name="add" size={22} type="ionicons" color={colors.foregroundColor} />
|
||||
</TouchableOpacity>
|
||||
</ToolTipMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWalletButton;
|
|
@ -264,6 +264,7 @@ class AmountInput extends Component {
|
|||
{amount !== BitcoinUnit.MAX ? (
|
||||
<TextInput
|
||||
{...this.props}
|
||||
caretHidden
|
||||
testID="BitcoinAmountInput"
|
||||
keyboardType="numeric"
|
||||
adjustsFontSizeToFit
|
||||
|
|
|
@ -3,8 +3,11 @@ import { Dimensions } from 'react-native';
|
|||
|
||||
import { isDesktop, isTablet } from '../../blue_modules/environment';
|
||||
|
||||
type ScreenSize = 'Handheld' | 'LargeScreen' | undefined;
|
||||
|
||||
interface ILargeScreenContext {
|
||||
isLargeScreen: boolean;
|
||||
setLargeScreenValue: (value: ScreenSize) => void;
|
||||
}
|
||||
|
||||
export const LargeScreenContext = createContext<ILargeScreenContext | undefined>(undefined);
|
||||
|
@ -15,7 +18,7 @@ interface LargeScreenProviderProps {
|
|||
|
||||
export const LargeScreenProvider: React.FC<LargeScreenProviderProps> = ({ children }) => {
|
||||
const [windowWidth, setWindowWidth] = useState<number>(Dimensions.get('window').width);
|
||||
const screenWidth: number = useMemo(() => Dimensions.get('screen').width, []);
|
||||
const [largeScreenValue, setLargeScreenValue] = useState<ScreenSize>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const updateScreenUsage = (): void => {
|
||||
|
@ -30,13 +33,23 @@ export const LargeScreenProvider: React.FC<LargeScreenProviderProps> = ({ childr
|
|||
}, [windowWidth]);
|
||||
|
||||
const isLargeScreen: boolean = useMemo(() => {
|
||||
if (largeScreenValue === 'LargeScreen') {
|
||||
return true;
|
||||
} else if (largeScreenValue === 'Handheld') {
|
||||
return false;
|
||||
}
|
||||
const screenWidth: number = Dimensions.get('screen').width;
|
||||
const halfScreenWidth = windowWidth >= screenWidth / 2;
|
||||
const condition = (isTablet && halfScreenWidth) || isDesktop;
|
||||
console.debug(
|
||||
`LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTablet}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`,
|
||||
);
|
||||
return condition;
|
||||
}, [windowWidth, screenWidth]);
|
||||
return (isTablet && halfScreenWidth) || isDesktop;
|
||||
}, [windowWidth, largeScreenValue]);
|
||||
|
||||
return <LargeScreenContext.Provider value={{ isLargeScreen }}>{children}</LargeScreenContext.Provider>;
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
isLargeScreen,
|
||||
setLargeScreenValue,
|
||||
}),
|
||||
[isLargeScreen, setLargeScreenValue],
|
||||
);
|
||||
|
||||
return <LargeScreenContext.Provider value={contextValue}>{children}</LargeScreenContext.Provider>;
|
||||
};
|
||||
|
|
|
@ -69,8 +69,6 @@ interface SettingsContextType {
|
|||
setIsHandOffUseEnabledAsyncStorage: (value: boolean) => Promise<void>;
|
||||
isPrivacyBlurEnabled: boolean;
|
||||
setIsPrivacyBlurEnabledState: (value: boolean) => void;
|
||||
isAdvancedModeEnabled: boolean;
|
||||
setIsAdvancedModeEnabledStorage: (value: boolean) => Promise<void>;
|
||||
isDoNotTrackEnabled: boolean;
|
||||
setDoNotTrackStorage: (value: boolean) => Promise<void>;
|
||||
isWidgetBalanceDisplayAllowed: boolean;
|
||||
|
@ -96,8 +94,6 @@ const defaultSettingsContext: SettingsContextType = {
|
|||
setIsHandOffUseEnabledAsyncStorage: async () => {},
|
||||
isPrivacyBlurEnabled: true,
|
||||
setIsPrivacyBlurEnabledState: () => {},
|
||||
isAdvancedModeEnabled: false,
|
||||
setIsAdvancedModeEnabledStorage: async () => {},
|
||||
isDoNotTrackEnabled: false,
|
||||
setDoNotTrackStorage: async () => {},
|
||||
isWidgetBalanceDisplayAllowed: true,
|
||||
|
@ -125,8 +121,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||
const [isHandOffUseEnabled, setHandOffUseEnabled] = useState<boolean>(false);
|
||||
// PrivacyBlur
|
||||
const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState<boolean>(true);
|
||||
// AdvancedMode
|
||||
const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useState<boolean>(false);
|
||||
// DoNotTrack
|
||||
const [isDoNotTrackEnabled, setIsDoNotTrackEnabled] = useState<boolean>(false);
|
||||
// WidgetCommunication
|
||||
|
@ -141,19 +135,10 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||
const [isTotalBalanceEnabled, setIsTotalBalanceEnabled] = useState<boolean>(true);
|
||||
const [totalBalancePreferredUnit, setTotalBalancePreferredUnitState] = useState<BitcoinUnit>(BitcoinUnit.BTC);
|
||||
|
||||
const advancedModeStorage = useAsyncStorage(BlueApp.ADVANCED_MODE_ENABLED);
|
||||
const languageStorage = useAsyncStorage(STORAGE_KEY);
|
||||
const { walletsInitialized } = useStorage();
|
||||
|
||||
useEffect(() => {
|
||||
advancedModeStorage
|
||||
.getItem()
|
||||
.then(advMode => {
|
||||
console.debug('SettingsContext advMode:', advMode);
|
||||
setIsAdvancedModeEnabled(advMode ? JSON.parse(advMode) : false);
|
||||
})
|
||||
.catch(error => console.error('Error fetching advanced mode settings:', error));
|
||||
|
||||
getIsHandOffUseEnabled()
|
||||
.then(handOff => {
|
||||
console.debug('SettingsContext handOff:', handOff);
|
||||
|
@ -216,7 +201,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||
getTotalBalancePreferredUnit()
|
||||
.then(unit => {
|
||||
console.debug('SettingsContext totalBalancePreferredUnit:', unit);
|
||||
setTotalBalancePreferredUnit(unit);
|
||||
setTotalBalancePreferredUnitState(unit);
|
||||
})
|
||||
.catch(error => console.error('Error fetching total balance preferred unit:', error));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -243,14 +228,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||
setLanguage(newLanguage);
|
||||
}, []);
|
||||
|
||||
const setIsAdvancedModeEnabledStorage = useCallback(
|
||||
async (value: boolean) => {
|
||||
await advancedModeStorage.setItem(JSON.stringify(value));
|
||||
setIsAdvancedModeEnabled(value);
|
||||
},
|
||||
[advancedModeStorage],
|
||||
);
|
||||
|
||||
const setDoNotTrackStorage = useCallback(async (value: boolean) => {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
if (value) {
|
||||
|
@ -321,8 +298,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||
setIsHandOffUseEnabledAsyncStorage,
|
||||
isPrivacyBlurEnabled,
|
||||
setIsPrivacyBlurEnabledState,
|
||||
isAdvancedModeEnabled,
|
||||
setIsAdvancedModeEnabledStorage,
|
||||
isDoNotTrackEnabled,
|
||||
setDoNotTrackStorage,
|
||||
isWidgetBalanceDisplayAllowed,
|
||||
|
@ -347,8 +322,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||
setIsHandOffUseEnabledAsyncStorage,
|
||||
isPrivacyBlurEnabled,
|
||||
setIsPrivacyBlurEnabledState,
|
||||
isAdvancedModeEnabled,
|
||||
setIsAdvancedModeEnabledStorage,
|
||||
isDoNotTrackEnabled,
|
||||
setDoNotTrackStorage,
|
||||
isWidgetBalanceDisplayAllowed,
|
||||
|
|
187
components/DevMenu.tsx
Normal file
187
components/DevMenu.tsx
Normal file
|
@ -0,0 +1,187 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { DevSettings, Alert, Platform, AlertButton } from 'react-native';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { HDSegwitBech32Wallet } from '../class';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
|
||||
import { TWallet } from '../class/wallets/types';
|
||||
|
||||
const getRandomLabelFromSecret = (secret: string): string => {
|
||||
const words = secret.split(' ');
|
||||
const firstWord = words[0];
|
||||
const lastWord = words[words.length - 1];
|
||||
return `[Developer] ${firstWord} ${lastWord}`;
|
||||
};
|
||||
|
||||
const showAlertWithWalletOptions = (
|
||||
wallets: TWallet[],
|
||||
title: string,
|
||||
message: string,
|
||||
onWalletSelected: (wallet: TWallet) => void,
|
||||
filterFn?: (wallet: TWallet) => boolean,
|
||||
) => {
|
||||
const filteredWallets = filterFn ? wallets.filter(filterFn) : wallets;
|
||||
|
||||
const showWallet = (index: number) => {
|
||||
if (index >= filteredWallets.length) return;
|
||||
const wallet = filteredWallets[index];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// Android: Use a limited number of buttons since the alert dialog has a limit
|
||||
Alert.alert(
|
||||
`${title}: ${wallet.getLabel()}`,
|
||||
`${message}\n\nSelected Wallet: ${wallet.getLabel()}\n\nWould you like to select this wallet or see the next one?`,
|
||||
[
|
||||
{
|
||||
text: 'Select This Wallet',
|
||||
onPress: () => onWalletSelected(wallet),
|
||||
},
|
||||
{
|
||||
text: 'Show Next Wallet',
|
||||
onPress: () => showWallet(index + 1),
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
{ cancelable: true },
|
||||
);
|
||||
} else {
|
||||
const options: AlertButton[] = filteredWallets.map(w => ({
|
||||
text: w.getLabel(),
|
||||
onPress: () => onWalletSelected(w),
|
||||
}));
|
||||
|
||||
options.push({
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
Alert.alert(title, message, options, { cancelable: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (filteredWallets.length > 0) {
|
||||
showWallet(0);
|
||||
} else {
|
||||
Alert.alert('No wallets available');
|
||||
}
|
||||
};
|
||||
|
||||
const DevMenu: React.FC = () => {
|
||||
const { wallets, addWallet } = useStorage();
|
||||
const { setLargeScreenValue } = useIsLargeScreen();
|
||||
|
||||
useEffect(() => {
|
||||
if (__DEV__) {
|
||||
// Clear existing Dev Menu items to prevent duplication
|
||||
DevSettings.addMenuItem('Reset Dev Menu', () => {
|
||||
DevSettings.reload();
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Add New Wallet', async () => {
|
||||
const wallet = new HDSegwitBech32Wallet();
|
||||
await wallet.generate();
|
||||
const label = getRandomLabelFromSecret(wallet.getSecret());
|
||||
wallet.setLabel(label);
|
||||
addWallet(wallet);
|
||||
|
||||
Clipboard.setString(wallet.getSecret());
|
||||
Alert.alert('New Wallet created!', `Wallet secret copied to clipboard.\nLabel: ${label}`);
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Copy Wallet Secret', () => {
|
||||
if (wallets.length === 0) {
|
||||
Alert.alert('No wallets available');
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertWithWalletOptions(wallets, 'Copy Wallet Secret', 'Select the wallet to copy the secret', wallet => {
|
||||
Clipboard.setString(wallet.getSecret());
|
||||
Alert.alert('Wallet Secret copied to clipboard!');
|
||||
});
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Copy Wallet ID', () => {
|
||||
if (wallets.length === 0) {
|
||||
Alert.alert('No wallets available');
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertWithWalletOptions(wallets, 'Copy Wallet ID', 'Select the wallet to copy the ID', wallet => {
|
||||
Clipboard.setString(wallet.getID());
|
||||
Alert.alert('Wallet ID copied to clipboard!');
|
||||
});
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Copy Wallet Xpub', () => {
|
||||
if (wallets.length === 0) {
|
||||
Alert.alert('No wallets available');
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertWithWalletOptions(
|
||||
wallets,
|
||||
'Copy Wallet Xpub',
|
||||
'Select the wallet to copy the Xpub',
|
||||
wallet => {
|
||||
const xpub = wallet.getXpub();
|
||||
if (xpub) {
|
||||
Clipboard.setString(xpub);
|
||||
Alert.alert('Wallet Xpub copied to clipboard!');
|
||||
} else {
|
||||
Alert.alert('This wallet does not have an Xpub.');
|
||||
}
|
||||
},
|
||||
wallet => typeof wallet.getXpub === 'function',
|
||||
);
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Purge Wallet Transactions', () => {
|
||||
if (wallets.length === 0) {
|
||||
Alert.alert('No wallets available');
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertWithWalletOptions(wallets, 'Purge Wallet Transactions', 'Select the wallet to purge transactions', wallet => {
|
||||
const msg = 'Transactions purged successfully!';
|
||||
|
||||
if (wallet.type === HDSegwitBech32Wallet.type) {
|
||||
wallet._txs_by_external_index = {};
|
||||
wallet._txs_by_internal_index = {};
|
||||
}
|
||||
|
||||
// @ts-ignore: Property '_hdWalletInstance' does not exist on type 'Wallet'. Pls help
|
||||
if (wallet._hdWalletInstance) {
|
||||
// @ts-ignore: Property '_hdWalletInstance' does not exist on type 'Wallet'. Pls help
|
||||
wallet._hdWalletInstance._txs_by_external_index = {};
|
||||
// @ts-ignore: Property '_hdWalletInstance' does not exist on type 'Wallet'. Pls help
|
||||
wallet._hdWalletInstance._txs_by_internal_index = {};
|
||||
}
|
||||
|
||||
Alert.alert(msg);
|
||||
});
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Force Large Screen Interface', () => {
|
||||
setLargeScreenValue('LargeScreen');
|
||||
Alert.alert('Large Screen Interface forced.');
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Force Handheld Interface', () => {
|
||||
setLargeScreenValue('Handheld');
|
||||
Alert.alert('Handheld Interface forced.');
|
||||
});
|
||||
|
||||
DevSettings.addMenuItem('Reset Screen Interface', () => {
|
||||
setLargeScreenValue(undefined);
|
||||
Alert.alert('Screen Interface reset to default.');
|
||||
});
|
||||
}
|
||||
}, [wallets, addWallet, setLargeScreenValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DevMenu;
|
36
components/DismissKeyboardInputAccessory.tsx
Normal file
36
components/DismissKeyboardInputAccessory.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { InputAccessoryView, Keyboard, Platform, StyleSheet, View } from 'react-native';
|
||||
import { useTheme } from './themes';
|
||||
import { BlueButtonLink } from '../BlueComponents';
|
||||
import loc from '../loc';
|
||||
|
||||
export const DismissKeyboardInputAccessoryViewID = 'DismissKeyboardInputAccessory';
|
||||
export const DismissKeyboardInputAccessory: React.FC = () => {
|
||||
const { colors } = useTheme();
|
||||
const styleHooks = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
if (Platform.OS !== 'ios') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputAccessoryView nativeID={DismissKeyboardInputAccessoryViewID}>
|
||||
<View style={[styles.container, styleHooks.container]}>
|
||||
<BlueButtonLink title={loc.send.input_done} onPress={Keyboard.dismiss} />
|
||||
</View>
|
||||
</InputAccessoryView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
maxHeight: 44,
|
||||
},
|
||||
});
|
49
components/DoneAndDismissKeyboardInputAccessory.tsx
Normal file
49
components/DoneAndDismissKeyboardInputAccessory.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { InputAccessoryView, Keyboard, Platform, StyleSheet, View } from 'react-native';
|
||||
import { BlueButtonLink } from '../BlueComponents';
|
||||
import loc from '../loc';
|
||||
import { useTheme } from './themes';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
interface DoneAndDismissKeyboardInputAccessoryProps {
|
||||
onPasteTapped: (clipboard: string) => void;
|
||||
onClearTapped: () => void;
|
||||
}
|
||||
export const DoneAndDismissKeyboardInputAccessoryViewID = 'DoneAndDismissKeyboardInputAccessory';
|
||||
export const DoneAndDismissKeyboardInputAccessory: React.FC<DoneAndDismissKeyboardInputAccessoryProps> = props => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const styleHooks = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
const onPasteTapped = async () => {
|
||||
const clipboard = await Clipboard.getString();
|
||||
props.onPasteTapped(clipboard);
|
||||
};
|
||||
|
||||
const inputView = (
|
||||
<View style={[styles.container, styleHooks.container]}>
|
||||
<BlueButtonLink title={loc.send.input_clear} onPress={props.onClearTapped} />
|
||||
<BlueButtonLink title={loc.send.input_paste} onPress={onPasteTapped} />
|
||||
<BlueButtonLink title={loc.send.input_done} onPress={Keyboard.dismiss} />
|
||||
</View>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return <InputAccessoryView nativeID={DoneAndDismissKeyboardInputAccessoryViewID}>{inputView}</InputAccessoryView>;
|
||||
} else {
|
||||
return inputView;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
maxHeight: 44,
|
||||
},
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import PlusIcon from './icons/PlusIcon';
|
||||
import { useTheme } from './themes';
|
||||
import AddWalletButton from './AddWalletButton';
|
||||
|
||||
interface HeaderProps {
|
||||
leftText: string;
|
||||
|
@ -25,7 +25,7 @@ export const Header: React.FC<HeaderProps> = ({ leftText, isDrawerList, onNewWal
|
|||
return (
|
||||
<View style={[styles.root, styleWithProps.root]}>
|
||||
<Text style={[styles.text, styleWithProps.text]}>{leftText}</Text>
|
||||
{onNewWalletPress && <PlusIcon onPress={onNewWalletPress} />}
|
||||
{onNewWalletPress && <AddWalletButton onPress={onNewWalletPress} />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { InputAccessoryView, Keyboard, Platform, StyleSheet, View } from 'react-native';
|
||||
import { Text } from '@rneui/themed';
|
||||
|
||||
import { BlueButtonLink } from '../BlueComponents';
|
||||
import loc from '../loc';
|
||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
import { useTheme } from './themes';
|
||||
|
||||
const InputAccessoryAllFunds = ({ balance, canUseAll, onUseAllPressed }) => {
|
||||
interface InputAccessoryAllFundsProps {
|
||||
balance: string;
|
||||
canUseAll: boolean;
|
||||
onUseAllPressed: () => void;
|
||||
}
|
||||
|
||||
const InputAccessoryAllFunds: React.FC<InputAccessoryAllFundsProps> = ({ balance, canUseAll, onUseAllPressed }) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
|
@ -42,7 +46,7 @@ const InputAccessoryAllFunds = ({ balance, canUseAll, onUseAllPressed }) => {
|
|||
);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return <InputAccessoryView nativeID={InputAccessoryAllFunds.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
|
||||
return <InputAccessoryView nativeID={InputAccessoryAllFundsAccessoryViewID}>{inputView}</InputAccessoryView>;
|
||||
}
|
||||
|
||||
// androidPlaceholder View is needed to force shrink screen (KeyboardAvoidingView) where this component is used
|
||||
|
@ -54,13 +58,7 @@ const InputAccessoryAllFunds = ({ balance, canUseAll, onUseAllPressed }) => {
|
|||
);
|
||||
};
|
||||
|
||||
InputAccessoryAllFunds.InputAccessoryViewID = 'useMaxInputAccessoryViewID';
|
||||
|
||||
InputAccessoryAllFunds.propTypes = {
|
||||
balance: PropTypes.string.isRequired,
|
||||
canUseAll: PropTypes.bool.isRequired,
|
||||
onUseAllPressed: PropTypes.func.isRequired,
|
||||
};
|
||||
export const InputAccessoryAllFundsAccessoryViewID = 'useMaxInputAccessoryViewID';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
|
@ -45,11 +45,10 @@ const MenuElements = () => {
|
|||
if (reloadTransactionsMenuActionFunction && typeof reloadTransactionsMenuActionFunction === 'function') {
|
||||
reloadTransactionsMenuActionFunction();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [reloadTransactionsMenuActionFunction]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('MenuElements: useEffect');
|
||||
console.debug('MenuElements: useEffect');
|
||||
if (walletsInitialized) {
|
||||
eventEmitter?.addListener('openSettings', openSettings);
|
||||
eventEmitter?.addListener('addWalletMenuAction', addWalletMenuAction);
|
||||
|
@ -62,8 +61,7 @@ const MenuElements = () => {
|
|||
eventEmitter?.removeAllListeners('importWalletMenuAction');
|
||||
eventEmitter?.removeAllListeners('reloadTransactionsMenuAction');
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [walletsInitialized]);
|
||||
}, [addWalletMenuAction, importWalletMenuAction, openSettings, reloadTransactionsMenuElementsFunction, walletsInitialized]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Ref, useCallback, useMemo } from 'react';
|
||||
import { Platform, Pressable, TouchableOpacity } from 'react-native';
|
||||
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
|
||||
import {
|
||||
ContextMenuView,
|
||||
RenderItem,
|
||||
|
@ -8,7 +9,6 @@ import {
|
|||
IconConfig,
|
||||
MenuElementConfig,
|
||||
} from 'react-native-ios-context-menu';
|
||||
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
|
||||
import { ToolTipMenuProps, Action } from './types';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
|
||||
|
@ -30,6 +30,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
|
||||
const { language } = useSettings();
|
||||
|
||||
// Map Menu Items for iOS Context Menu
|
||||
const mapMenuItemForContextMenuView = useCallback((action: Action) => {
|
||||
if (!action.id) return null;
|
||||
return {
|
||||
|
@ -41,14 +42,30 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Map Menu Items for RN Menu (supports subactions and displayInline)
|
||||
const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => {
|
||||
if (!action.id) return null;
|
||||
|
||||
// Check for subactions
|
||||
const subactions =
|
||||
action.subactions?.map(subaction => ({
|
||||
id: subaction.id.toString(),
|
||||
title: subaction.text,
|
||||
subtitle: subaction.subtitle,
|
||||
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
|
||||
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
id: action.id.toString(),
|
||||
title: action.text,
|
||||
subtitle: action.subtitle,
|
||||
image: action.icon?.iconValue ? action.icon.iconValue : undefined,
|
||||
state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: action.disabled },
|
||||
attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
|
||||
subactions: subactions.length > 0 ? subactions : undefined,
|
||||
displayInline: action.displayInline || false,
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -98,7 +115,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
);
|
||||
|
||||
const renderContextMenuView = () => {
|
||||
console.debug('ToolTipMenu.tsx rendering: renderContextMenuView');
|
||||
return (
|
||||
<ContextMenuView
|
||||
lazyPreview
|
||||
|
@ -139,7 +155,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
};
|
||||
|
||||
const renderMenuView = () => {
|
||||
console.debug('ToolTipMenu.tsx rendering: renderMenuView');
|
||||
return (
|
||||
<MenuView
|
||||
title={title}
|
||||
|
@ -147,7 +162,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
onPressAction={handlePressMenuItemForMenuView}
|
||||
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
|
||||
shouldOpenOnLongPress={!isMenuPrimaryAction}
|
||||
// @ts-ignore: its not in the types but it works
|
||||
// @ts-ignore: Not exposed in types
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
accessibilityRole={props.accessibilityRole}
|
||||
|
|
|
@ -289,7 +289,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
handleOnViewOnBlockExplorer,
|
||||
],
|
||||
);
|
||||
const toolTipActions = useMemo((): Action[] | Action[][] => {
|
||||
const toolTipActions = useMemo((): Action[] => {
|
||||
const actions: (Action | Action[])[] = [];
|
||||
|
||||
if (rowTitle !== loc.lnd.expired) {
|
||||
|
@ -308,7 +308,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
actions.push([CommonToolTipActions.ExpandNote]);
|
||||
}
|
||||
|
||||
return actions as Action[] | Action[][];
|
||||
return actions as Action[];
|
||||
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
|
||||
|
||||
const accessibilityState = useMemo(() => {
|
||||
|
|
|
@ -38,13 +38,15 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
.allowOnchainAddress()
|
||||
.then((value: boolean) => setAllowOnchainAddress(value))
|
||||
.catch((e: Error) => {
|
||||
console.log('This Lndhub wallet does not have an onchain address API.');
|
||||
console.log('This LNDhub wallet does not have an onchain address API.');
|
||||
setAllowOnchainAddress(false);
|
||||
});
|
||||
}
|
||||
}, [wallet]);
|
||||
|
||||
useEffect(() => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
|
||||
setWallet(initialWallet);
|
||||
}, [initialWallet]);
|
||||
|
||||
|
@ -82,9 +84,9 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
newWalletPreferredUnit = BitcoinUnit.BTC;
|
||||
}
|
||||
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
|
||||
const updatedWallet = updateWalletWithNewUnit(wallet, newWalletPreferredUnit);
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
setWallet(updatedWallet);
|
||||
onWalletUnitChange?.(updatedWallet);
|
||||
};
|
||||
|
@ -132,8 +134,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
? formatBalance(wallet.getBalance(), balanceUnit, true)
|
||||
: formatBalanceWithoutSuffix(wallet.getBalance(), balanceUnit, true);
|
||||
return !hideBalance && balanceFormatted;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wallet.hideBalance, wallet.getPreferredBalanceUnit()]);
|
||||
}, [wallet]);
|
||||
|
||||
const toolTipWalletBalanceActions = useMemo(() => {
|
||||
return wallet.hideBalance
|
||||
|
@ -181,7 +182,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
style={styles.lineaderGradient}
|
||||
{...WalletGradient.linearGradientProps(wallet.type)}
|
||||
>
|
||||
<Image source={imageSource} defaultSource={imageSource} style={styles.chainIcon} />
|
||||
<Image source={imageSource} style={styles.chainIcon} />
|
||||
|
||||
<Text testID="WalletLabel" numberOfLines={1} style={styles.walletLabel} selectable>
|
||||
{wallet.getLabel()}
|
||||
|
@ -218,7 +219,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
<TouchableOpacity style={styles.walletPreferredUnitView} onPress={changeWalletBalanceUnit}>
|
||||
<Text style={styles.walletPreferredUnitText}>
|
||||
{wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY
|
||||
? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD
|
||||
? (preferredFiatCurrency?.endPointKey ?? FiatUnit.USD)
|
||||
: wallet.getPreferredBalanceUnit()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -61,7 +61,7 @@ const NewWalletPanel: React.FC<NewWalletPanelProps> = ({ onPress }) => {
|
|||
const { colors } = useTheme();
|
||||
const { width } = useWindowDimensions();
|
||||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
const { isLargeScreen } = useIsLargeScreen();
|
||||
const nStylesHooks = StyleSheet.create({
|
||||
container: isLargeScreen
|
||||
? {
|
||||
|
@ -192,7 +192,7 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
const { walletTransactionUpdateStatus } = useStorage();
|
||||
const { width } = useWindowDimensions();
|
||||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
const { isLargeScreen } = useIsLargeScreen();
|
||||
|
||||
const onPressedIn = useCallback(() => {
|
||||
if (animationsEnabled) {
|
||||
|
@ -248,7 +248,7 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
isLargeScreen || !horizontal ? [iStyles.rootLargeDevice, customStyle] : customStyle ?? { ...iStyles.root, width: itemWidth },
|
||||
isLargeScreen || !horizontal ? [iStyles.rootLargeDevice, customStyle] : (customStyle ?? { ...iStyles.root, width: itemWidth }),
|
||||
{ opacity, transform: [{ scale: scaleValue }] },
|
||||
]}
|
||||
>
|
||||
|
@ -264,7 +264,7 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
>
|
||||
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
|
||||
<Image defaultSource={image} source={image} style={iStyles.image} />
|
||||
<Image source={image} style={iStyles.image} />
|
||||
<Text style={iStyles.br} />
|
||||
{!isPlaceHolder && (
|
||||
<>
|
||||
|
@ -374,9 +374,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
|
||||
const flatListRef = useRef<FlatList<any>>(null);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): any => {
|
||||
useImperativeHandle(ref, (): any => {
|
||||
return {
|
||||
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params),
|
||||
scrollToIndex: (params: {
|
||||
|
@ -396,9 +394,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
flashScrollIndicators: () => flatListRef.current?.flashScrollIndicators(),
|
||||
getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => {
|
||||
console.debug('onScrollToIndexFailed');
|
||||
|
|
|
@ -209,7 +209,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] | Action[][] => {
|
||||
const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] => {
|
||||
const actions = [
|
||||
{
|
||||
id: actionKeys.CopyToClipboard,
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, TouchableOpacity, ViewStyle } from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
|
||||
import { useTheme } from '../themes';
|
||||
import loc from '../../loc';
|
||||
|
||||
type PlusIconProps = {
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ball: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
} as ViewStyle,
|
||||
});
|
||||
|
||||
const PlusIcon: React.FC<PlusIconProps> = ({ onPress }) => {
|
||||
const { colors } = useTheme();
|
||||
const stylesHook = StyleSheet.create({
|
||||
ball: {
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.ball, stylesHook.ball]}
|
||||
accessibilityLabel={loc.wallets.add_title}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Icon name="add" size={22} type="ionicons" color={colors.foregroundColor} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlusIcon;
|
|
@ -1,9 +1,11 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
import { useTheme } from '../themes';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import loc from '../../loc';
|
||||
import ToolTipMenu from '../TooltipMenu';
|
||||
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
|
||||
|
||||
const SettingsButton = () => {
|
||||
const { colors } = useTheme();
|
||||
|
@ -12,7 +14,22 @@ const SettingsButton = () => {
|
|||
navigate('Settings');
|
||||
};
|
||||
|
||||
const onPressMenuItem = useCallback(
|
||||
(menuItem: string) => {
|
||||
switch (menuItem) {
|
||||
case CommonToolTipActions.ManageWallet.id:
|
||||
navigate('ManageWallets');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => [CommonToolTipActions.ManageWallet], []);
|
||||
return (
|
||||
<ToolTipMenu onPressMenuItem={onPressMenuItem} actions={actions}>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc.settings.default_title}
|
||||
|
@ -22,6 +39,7 @@ const SettingsButton = () => {
|
|||
>
|
||||
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
|
||||
</TouchableOpacity>
|
||||
</ToolTipMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AccessibilityRole, ViewStyle } from 'react-native';
|
||||
import { AccessibilityRole, ViewStyle, ColorValue } from 'react-native';
|
||||
|
||||
export interface Action {
|
||||
id: string | number;
|
||||
|
@ -7,13 +7,19 @@ export interface Action {
|
|||
iconValue: string;
|
||||
};
|
||||
menuTitle?: string;
|
||||
subtitle?: string;
|
||||
menuState?: 'mixed' | boolean | undefined;
|
||||
displayInline?: boolean; // Indicates if subactions should be displayed inline or nested (iOS only)
|
||||
image?: string;
|
||||
imageColor?: ColorValue;
|
||||
destructive?: boolean;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
displayInline?: boolean;
|
||||
subactions?: Action[]; // Nested/Inline actions (subactions) within an action
|
||||
}
|
||||
|
||||
export interface ToolTipMenuProps {
|
||||
actions: Action[] | Action[][];
|
||||
actions: Action[];
|
||||
children: React.ReactNode;
|
||||
enableAndroidRipple?: boolean;
|
||||
dismissMenu?: () => void;
|
||||
|
|
|
@ -2,6 +2,156 @@ def app_identifiers
|
|||
["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__)
|
||||
|
||||
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...")
|
||||
|
||||
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|
|
||||
UI.user_error!("Error reverting hex to keystore") unless status.success?
|
||||
end
|
||||
UI.message("Keystore created successfully.")
|
||||
|
||||
File.delete("bluewallet-release-key.keystore.hex")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
version_name = sh("grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '\"'").strip
|
||||
|
||||
# Manually update the 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
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Continue with the build process
|
||||
Dir.chdir("android") do
|
||||
UI.message("Building APK...")
|
||||
gradle(
|
||||
task: "assembleRelease",
|
||||
project_dir: "android"
|
||||
)
|
||||
UI.message("APK build completed.")
|
||||
|
||||
# Define the output 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
|
||||
if File.exist?(unsigned_apk_path)
|
||||
UI.message("Renaming APK to #{signed_apk_name}...")
|
||||
FileUtils.mv(unsigned_apk_path, signed_apk_path)
|
||||
ENV['APK_OUTPUT_PATH'] = File.expand_path(signed_apk_path)
|
||||
else
|
||||
UI.error("Unsigned APK not found at path: #{unsigned_apk_path}")
|
||||
next
|
||||
end
|
||||
|
||||
# Sign the APK using apksigner
|
||||
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}")
|
||||
UI.message("APK signed successfully: #{signed_apk_path}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
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...")
|
||||
apk_path = `find ./ -name "*.apk"`.strip
|
||||
UI.user_error!("No APK file found") if apk_path.nil? || apk_path.empty?
|
||||
end
|
||||
|
||||
UI.message("Uploading APK to BrowserStack: #{apk_path}...")
|
||||
upload_to_browserstack_app_live(
|
||||
file_path: apk_path,
|
||||
browserstack_username: ENV['BROWSERSTACK_USERNAME'],
|
||||
browserstack_access_key: ENV['BROWSERSTACK_ACCESS_KEY']
|
||||
)
|
||||
|
||||
# Extract the BrowserStack URL from the output
|
||||
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
|
||||
apk_filename = File.basename(apk_path)
|
||||
apk_download_url = ENV['APK_OUTPUT_PATH'] # Assuming this path is accessible to the PR
|
||||
browserstack_hashed_id = app_url.gsub('bs://', '')
|
||||
pr_number = ENV['GITHUB_PR_NUMBER']
|
||||
|
||||
comment = <<~COMMENT
|
||||
### APK Successfully Uploaded to BrowserStack
|
||||
|
||||
You can test it on the following devices:
|
||||
|
||||
- [Google Pixel 5 (Android 12.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 7 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 8 (Android 14.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 3a (Android 9.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
|
||||
- [Samsung Galaxy Z Fold 5 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Z Fold 6 (Android 14.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Tab S9 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
|
||||
**Filename**: [#{apk_filename}](#{apk_download_url})
|
||||
**BrowserStack App URL**: #{app_url}
|
||||
COMMENT
|
||||
|
||||
if pr_number
|
||||
begin
|
||||
sh("GH_TOKEN=#{ENV['GH_TOKEN']} gh pr comment #{pr_number} --body '#{comment}'")
|
||||
UI.success("Posted comment to PR ##{pr_number}")
|
||||
rescue => e
|
||||
UI.error("Failed to post comment to PR: #{e.message}")
|
||||
end
|
||||
else
|
||||
UI.important("No PR number found. Skipping PR comment.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
platform :ios do
|
||||
|
||||
before_all do |lane, options|
|
||||
|
@ -29,7 +179,8 @@ platform :ios do
|
|||
type: "development",
|
||||
app_identifier: app_identifier,
|
||||
readonly: false, # This will regenerate the provisioning profile if needed
|
||||
force_for_new_devices: true # This forces match to add new devices to the profile
|
||||
force_for_new_devices: true,
|
||||
clone_branch_directly: true
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -73,6 +224,7 @@ platform :ios do
|
|||
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)
|
||||
platform: platform,
|
||||
app_identifier: app_identifier,
|
||||
team_id: ENV["ITC_TEAM_ID"],
|
||||
|
@ -90,7 +242,8 @@ platform :ios do
|
|||
type: "development",
|
||||
platform: "catalyst",
|
||||
app_identifier: app_identifiers,
|
||||
readonly: true
|
||||
readonly: true,
|
||||
clone_branch_directly: true
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -100,7 +253,9 @@ platform :ios do
|
|||
type: "appstore",
|
||||
platform: "catalyst",
|
||||
app_identifier: app_identifiers,
|
||||
readonly: true
|
||||
readonly: true,
|
||||
clone_branch_directly: true
|
||||
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -112,14 +267,16 @@ platform :ios do
|
|||
platform: "catalyst",
|
||||
app_identifier: app_identifier,
|
||||
readonly: false,
|
||||
force_for_new_devices: true
|
||||
force_for_new_devices: true,
|
||||
clone_branch_directly: true
|
||||
)
|
||||
|
||||
match(
|
||||
type: "appstore",
|
||||
platform: "catalyst",
|
||||
app_identifier: app_identifier,
|
||||
readonly: false
|
||||
readonly: false,
|
||||
clone_branch_directly: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -136,7 +293,7 @@ platform :ios do
|
|||
|
||||
# Set the new build number
|
||||
increment_build_number(
|
||||
xcodeproj: "BlueWallet.xcodeproj",
|
||||
xcodeproj: "ios/BlueWallet.xcodeproj",
|
||||
build_number: ENV["NEW_BUILD_NUMBER"]
|
||||
)
|
||||
|
||||
|
@ -146,7 +303,7 @@ platform :ios do
|
|||
desc "Install CocoaPods dependencies"
|
||||
lane :install_pods do
|
||||
UI.message("Installing CocoaPods dependencies...")
|
||||
cocoapods
|
||||
cocoapods(podfile: "ios/Podfile")
|
||||
end
|
||||
|
||||
desc "Build the application"
|
||||
|
@ -154,7 +311,7 @@ platform :ios do
|
|||
UI.message("Building the application...")
|
||||
build_app(
|
||||
scheme: "BlueWallet",
|
||||
workspace: "BlueWallet.xcworkspace",
|
||||
workspace: "ios/BlueWallet.xcworkspace",
|
||||
export_method: "app-store",
|
||||
include_bitcode: false,
|
||||
configuration: "Release",
|
||||
|
@ -188,8 +345,8 @@ platform :ios do
|
|||
changelog = ENV["LATEST_COMMIT_MESSAGE"]
|
||||
|
||||
upload_to_testflight(
|
||||
api_key_path: "appstore_api_key.json",
|
||||
ipa: "./build/BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa",
|
||||
api_key_path: "./appstore_api_key.json",
|
||||
ipa: "./BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa",
|
||||
skip_waiting_for_build_processing: true, # Do not wait for processing
|
||||
changelog: changelog
|
||||
)
|
||||
|
@ -309,7 +466,7 @@ lane :update_release_notes do |options|
|
|||
'it' => release_notes_text, # Italian
|
||||
'ja' => release_notes_text, # Japanese
|
||||
'ms' => release_notes_text, # Malay
|
||||
'nb-NO' => release_notes_text, # Norwegian
|
||||
'nb' => release_notes_text, # Norwegian
|
||||
'pl' => release_notes_text, # Polish
|
||||
'pt-BR' => release_notes_text, # Portuguese (Brazil)
|
||||
'pt-PT' => release_notes_text, # Portuguese (Portugal)
|
5
fastlane/Pluginfile
Normal file
5
fastlane/Pluginfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Autogenerated by fastlane
|
||||
#
|
||||
# Ensure this file is checked in to source control!
|
||||
|
||||
gem 'fastlane-plugin-browserstack'
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue