diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index de838aa93e..7d8cc224b4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -25,6 +25,12 @@ + + + + + + RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; @@ -596,14 +582,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-hexa_keeper/Pods-hexa_keeper-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-hexa_keeper/Pods-hexa_keeper-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-hexa_keeper/Pods-hexa_keeper-resources.sh\"\n"; @@ -618,8 +600,6 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Core Configuration"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; diff --git a/ios/hexa_keeper/Info.plist b/ios/hexa_keeper/Info.plist index d8f154be92..0478c7bade 100644 --- a/ios/hexa_keeper/Info.plist +++ b/ios/hexa_keeper/Info.plist @@ -79,6 +79,10 @@ NSAllowsLocalNetworking + NSBluetoothAlwaysUsageDescription + Allow Keeper to scan and connect to your OneKey hardware wallet over Bluetooth + NSBluetoothPeripheralUsageDescription + Allow Keeper to scan and connect to your OneKey hardware wallet over Bluetooth NSCameraUsageDescription Please allow camera access for QR scanner NSContactsUsageDescription diff --git a/ios/hexa_keeper_dev-Info.plist b/ios/hexa_keeper_dev-Info.plist index f375929769..2e54d75f1f 100644 --- a/ios/hexa_keeper_dev-Info.plist +++ b/ios/hexa_keeper_dev-Info.plist @@ -46,6 +46,10 @@ NFCReaderUsageDescription Allow $(PRODUCT_NAME) to interact with nearby NFC devices + NSBluetoothAlwaysUsageDescription + Allow $(PRODUCT_NAME) to scan and connect to your OneKey hardware wallet over Bluetooth + NSBluetoothPeripheralUsageDescription + Allow $(PRODUCT_NAME) to scan and connect to your OneKey hardware wallet over Bluetooth NSAppTransportSecurity diff --git a/metro.config.js b/metro.config.js index 1748c03ea2..1e11c8b303 100644 --- a/metro.config.js +++ b/metro.config.js @@ -16,6 +16,8 @@ const config = { resolver: { assetExts: assetExts.filter((ext) => ext !== 'svg'), sourceExts: [...sourceExts, 'svg'], + // OneKey hd-core uses package.json "exports" field + unstable_enablePackageExports: true, }, }; diff --git a/package.json b/package.json index daf1e47ea7..04551c5500 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,11 @@ "@bitcoinerlab/miniscript": "1.4.0", "@gluestack-ui/themed-native-base": "0.1.108", "@ngraveio/bc-ur": "1.1.6", + "@noble/hashes": "^1.3.3", "@noble/secp256k1": "1.6.3", + "@onekeyfe/hd-ble-sdk": "1.1.16", + "@onekeyfe/hd-core": "1.1.16", + "@onekeyfe/react-native-ble-utils": "^0.1.4", "@react-native-clipboard/clipboard": "1.16.2", "@react-native-community/netinfo": "11.4.1", "@react-native-firebase/app": "24.0.0", @@ -75,6 +79,7 @@ "react-localization": "1.0.19", "react-native": "0.83.9", "react-native-background-timer": "2.4.1", + "react-native-ble-plx": "3.5.0", "react-native-biometrics": "2.2.0", "react-native-blob-util": "0.24.7", "react-native-change-icon": "5.0.0", @@ -122,6 +127,7 @@ "redux-persist": "6.0.0", "redux-saga": "1.1.3", "rn-qr-generator": "^1.4.4", + "ripple-keypairs": "^1.3.1", "satochip-react-native": "git+https://github.com/Toporin/satochip-react-native.git#1076e7c097571d561b5b903f31c6337455def19e", "semver": "7.3.8", "socket.io-client": "4.5.4", diff --git a/src/assets/images/flag-hongkong.svg b/src/assets/images/flag-hongkong.svg new file mode 100644 index 0000000000..65a0fd69ba --- /dev/null +++ b/src/assets/images/flag-hongkong.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/flag-japan.svg b/src/assets/images/flag-japan.svg new file mode 100644 index 0000000000..3c980d2058 --- /dev/null +++ b/src/assets/images/flag-japan.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/images/onekey-devices/classic-pure.png b/src/assets/images/onekey-devices/classic-pure.png new file mode 100644 index 0000000000..de31d59dc5 Binary files /dev/null and b/src/assets/images/onekey-devices/classic-pure.png differ diff --git a/src/assets/images/onekey-devices/classic.png b/src/assets/images/onekey-devices/classic.png new file mode 100644 index 0000000000..aaf787a4f0 Binary files /dev/null and b/src/assets/images/onekey-devices/classic.png differ diff --git a/src/assets/images/onekey-devices/pro-black.png b/src/assets/images/onekey-devices/pro-black.png new file mode 100644 index 0000000000..3057f8bc51 Binary files /dev/null and b/src/assets/images/onekey-devices/pro-black.png differ diff --git a/src/assets/images/onekey-devices/touch.png b/src/assets/images/onekey-devices/touch.png new file mode 100644 index 0000000000..fb85963c15 Binary files /dev/null and b/src/assets/images/onekey-devices/touch.png differ diff --git a/src/assets/images/onekey-green-dark.svg b/src/assets/images/onekey-green-dark.svg new file mode 100644 index 0000000000..7eb3c742a1 --- /dev/null +++ b/src/assets/images/onekey-green-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey-green-light.svg b/src/assets/images/onekey-green-light.svg new file mode 100644 index 0000000000..a9166d0064 --- /dev/null +++ b/src/assets/images/onekey-green-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey-shop-device.png b/src/assets/images/onekey-shop-device.png new file mode 100644 index 0000000000..e57d06cfab Binary files /dev/null and b/src/assets/images/onekey-shop-device.png differ diff --git a/src/assets/images/onekey_icon.svg b/src/assets/images/onekey_icon.svg new file mode 100644 index 0000000000..afa35e0aac --- /dev/null +++ b/src/assets/images/onekey_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey_icon_light.svg b/src/assets/images/onekey_icon_light.svg new file mode 100644 index 0000000000..afa35e0aac --- /dev/null +++ b/src/assets/images/onekey_icon_light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey_illustration.svg b/src/assets/images/onekey_illustration.svg new file mode 100644 index 0000000000..1b36bb43f3 --- /dev/null +++ b/src/assets/images/onekey_illustration.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/images/onekey_logo.svg b/src/assets/images/onekey_logo.svg new file mode 100644 index 0000000000..51b2596ff4 --- /dev/null +++ b/src/assets/images/onekey_logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/onekey_logo_white.svg b/src/assets/images/onekey_logo_white.svg new file mode 100644 index 0000000000..4d3d1cd7db --- /dev/null +++ b/src/assets/images/onekey_logo_white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/OneKeyBleModal.tsx b/src/components/OneKeyBleModal.tsx new file mode 100644 index 0000000000..2ea51f80df --- /dev/null +++ b/src/components/OneKeyBleModal.tsx @@ -0,0 +1,630 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { ActivityIndicator, FlatList, Image, StyleSheet, TouchableOpacity } from 'react-native'; +import { Box, useColorMode } from '@gluestack-ui/themed-native-base'; +import { useDispatch } from 'react-redux'; +import KeeperModal from 'src/components/KeeperModal'; +import Text from 'src/components/KeeperText'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import TickIcon from 'src/assets/images/icon_tick.svg'; +import { useAppSelector } from 'src/store/hooks'; +import { MultisigScriptType, NetworkType, SignerType } from 'src/services/wallets/enums'; +import { UI_REQUEST } from '@onekeyfe/hd-core'; +import { + assertOneKeyFingerprint, + ensureOneKeyBLEReady, + fetchOneKeySignerData, + getOneKeyDeviceInfo, + onekeyUIEmitter, + ONEKEY_UI_EVENT, + searchOneKeyDevices, + verifyAddressOnOneKey, + type OneKeyDeviceInfo, + type OneKeyUIEvent, +} from 'src/services/onekeyBle'; +import { + getDeviceImage, + getDeviceDisplayName, + getDeviceTypeName, +} from 'src/services/onekeyBle/deviceConstants'; +import { setupUSBSigner } from 'src/hardware/signerSetup'; +import { addSigningDevice } from 'src/store/sagaActions/vaults'; +import { updateKeyDetails } from 'src/store/sagaActions/wallets'; +import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; +import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; +import type { Vault, VaultSigner } from 'src/services/wallets/interfaces/vault'; +import { captureError } from 'src/services/sentry'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import type { Signer } from 'src/services/wallets/interfaces/vault'; +import type { SearchDevice } from '@onekeyfe/hd-core'; +import WalletUtilities from 'src/services/wallets/operations/utils'; + +// ─── SDK UI event descriptions ────────────────────────────────────────────── + +const UI_PROMPTS: Record = { + [UI_REQUEST.REQUEST_PIN]: 'Please enter PIN on your OneKey device', + [UI_REQUEST.REQUEST_BUTTON]: 'Please confirm on your OneKey device', + idle: '', +}; + +// ─── Types ────────────────────────────────────────────────────────────────── + +type ModalMode = 'setup' | 'identify' | 'recovery' | 'health-check' | 'verify-address'; +type ModalPhase = 'scan' | 'connecting' | 'sdk-prompt' | 'done'; + +type Props = { + visible: boolean; + close: () => void; + mode: ModalMode; + signer?: Signer; + isMultisig?: boolean; + addSignerFlow?: boolean; + accountNumber?: number; + onSignerAdded?: (signer: Signer) => void; + onDeviceIdentified?: (result: { device: SearchDevice; deviceInfo: OneKeyDeviceInfo }) => void; + // verify-address mode props + vaultKey?: VaultSigner; + vault?: Vault; + vaultId?: string; + receiveAddressIndex?: number; + receivingAddress?: string; +}; + +// ─── Component ────────────────────────────────────────────────────────────── + +function OneKeyBleModal({ + visible, + close, + mode, + signer, + isMultisig = true, + accountNumber = 0, + onSignerAdded, + onDeviceIdentified, + vaultKey, + vault, + vaultId, + receiveAddressIndex, + receivingAddress, +}: Props) { + const { colorMode } = useColorMode(); + const dispatch = useDispatch(); + const { showToast } = useToastMessage(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + + const { bitcoinNetworkType } = useAppSelector((state) => state.settings); + const networkType = + bitcoinNetworkType === NetworkType.TESTNET ? NetworkType.TESTNET : NetworkType.MAINNET; + + const [phase, setPhase] = useState('scan'); + const [devices, setDevices] = useState([]); + const [scanning, setScanning] = useState(false); + const [statusMessage, setStatusMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [sdkPrompt, setSdkPrompt] = useState('idle'); + + // Listen to SDK UI events + useEffect(() => { + const handler = (event: OneKeyUIEvent) => { + if (event === 'idle') { + setSdkPrompt('idle'); + if (phase === 'sdk-prompt') setPhase('connecting'); + } else { + setSdkPrompt(event); + setPhase('sdk-prompt'); + } + }; + const subscription = onekeyUIEmitter.addListener(ONEKEY_UI_EVENT, handler); + return () => { subscription.remove(); }; + }, [phase]); + + // Reset state when modal opens + useEffect(() => { + if (visible) { + setDevices([]); + setScanning(false); + setStatusMessage(''); + setErrorMessage(''); + setSdkPrompt('idle'); + + if (mode === 'health-check' || mode === 'verify-address') { + // Direct connect modes: skip scan + setPhase('connecting'); + setTimeout(() => mode === 'verify-address' ? runVerifyAddress() : runHealthCheck(), 300); + } else { + // Setup, identify, and recovery show scan UI so the user can choose among nearby devices. + setPhase('scan'); + setTimeout(() => scanDevices(), 300); + } + } + }, [visible]); + + // ─── Scan ────────────────────────────────────────────────────────────────── + + const scanDevices = async () => { + if (scanning) return; + try { + setScanning(true); + setErrorMessage(''); + setDevices([]); + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + const message = + bleReady.reason === 'MISSING_PERMISSION' + ? 'Please grant Bluetooth permissions' + : 'Please turn on Bluetooth and try again'; + setErrorMessage(message); + showToast(message, ); + return; + } + const found = await searchOneKeyDevices(); + setDevices(found || []); + } catch (error) { + captureError(error); + const message = error?.message || common.somethingWrong; + setErrorMessage(message); + showToast(message, ); + } finally { + setScanning(false); + } + }; + + // ─── Setup: tap device → connect → import keys ───────────────────────────── + + const handleSetupTap = async (device: SearchDevice) => { + if (!device?.connectId) return; + try { + setErrorMessage(''); + setPhase('connecting'); + setStatusMessage('Connecting to device...'); + + const deviceInfo = await getOneKeyDeviceInfo(device.connectId); + + // Clear any SDK prompt after device info is fetched + setPhase('connecting'); + setStatusMessage('Importing keys...'); + const signerData = await fetchOneKeySignerData({ + connectId: device.connectId, + deviceId: deviceInfo.deviceId, + networkType, + accountNumber, + }); + + // Clear any SDK prompt after keys imported + setPhase('connecting'); + setStatusMessage('Finalizing...'); + + const { signer: newSigner } = setupUSBSigner(SignerType.ONEKEY, signerData, isMultisig); + + // Title: "OneKey Pro" / "OneKey Classic", Subtitle: BLE name (e.g. "Pro 04DD") + newSigner.signerName = getDeviceTypeName(device); + const bleName = device?.name; + if (bleName && bleName !== 'Unknown') { + newSigner.signerDescription = bleName; + } + newSigner.extraData = { ...newSigner.extraData, bleConnectId: deviceInfo.connectId }; + + if (mode === 'setup') { + dispatch(addSigningDevice([newSigner])); + } + setPhase('done'); + showToast( + mode === 'recovery' ? 'OneKey connected successfully' : 'OneKey added successfully', + + ); + onSignerAdded?.(newSigner); + close(); + } catch (error) { + captureError(error); + const message = error?.message || common.somethingWrong; + setErrorMessage(message); + showToast(message, ); + setPhase('scan'); // Back to scan so user can retry + } + }; + + // ─── Identify: tap device → connect → match fingerprint ─────────────────── + + const handleIdentifyTap = async (device: SearchDevice) => { + if (!device?.connectId || !signer) return; + try { + setErrorMessage(''); + setPhase('connecting'); + setStatusMessage('Connecting to device...'); + + const deviceInfo = await getOneKeyDeviceInfo(device.connectId); + + try { + assertOneKeyFingerprint(deviceInfo, signer); + } catch (_) { + const message = 'Fingerprint mismatch. Please select the correct OneKey device.'; + setErrorMessage(message); + showToast(message, ); + setPhase('scan'); + return; + } + + setPhase('done'); + showToast('OneKey verified successfully', ); + onDeviceIdentified?.({ device, deviceInfo }); + close(); + } catch (error) { + captureError(error); + const message = error?.message || common.somethingWrong; + setErrorMessage(message); + showToast(message, ); + setPhase('scan'); + } + }; + + // ─── Health Check: direct connect via stored connectId ───────────────────── + + const runHealthCheck = async () => { + if (!signer) return; + try { + setPhase('connecting'); + + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + close(); + return; + } + + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + close(); + return; + } + + // BLE needs a brief scan to discover peripherals before connecting + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setStatusMessage('Verifying device...'); + const deviceInfo = await getOneKeyDeviceInfo(storedConnectId); + assertOneKeyFingerprint(deviceInfo, signer); + + // Clear any SDK prompt after verification + setPhase('connecting'); + setStatusMessage('Checking result...'); + + dispatch( + healthCheckStatusUpdate([ + { signerId: signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_SUCCESSFULL }, + ]) + ); + setPhase('done'); + showToast('OneKey verification successful', ); + close(); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + close(); + } + }; + + // ─── Verify Address: direct connect → show address on device ───────────── + + const runVerifyAddress = async () => { + if (!signer || !vaultKey || !receivingAddress || receiveAddressIndex === undefined) { + showToast('Missing address verification details. Please try again.', ); + close(); + return; + } + try { + setPhase('connecting'); + + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + close(); + return; + } + + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + close(); + return; + } + + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setStatusMessage('Reading device info...'); + const deviceInfo = await getOneKeyDeviceInfo(storedConnectId); + assertOneKeyFingerprint(deviceInfo, signer); + + setPhase('connecting'); + setStatusMessage('Verifying address on device...'); + const addressPath = `${vaultKey.derivationPath}/0/${receiveAddressIndex}`; + let multisigConfig; + if (vault?.isMultiSig) { + const multisigScriptType = + vault.scheme.multisigScriptType || MultisigScriptType.DEFAULT_MULTISIG; + if (multisigScriptType !== MultisigScriptType.DEFAULT_MULTISIG) { + throw new Error('OneKey address verification supports standard multisig vaults only.'); + } + const multisigAddress = WalletUtilities.createMultiSig(vault, receiveAddressIndex, false); + const sortedXpubs = [...vault.specs.xpubs].sort((a, b) => { + const pubA = multisigAddress.signerPubkeyMap.get(a)?.toString('hex') || ''; + const pubB = multisigAddress.signerPubkeyMap.get(b)?.toString('hex') || ''; + return pubA.localeCompare(pubB); + }); + multisigConfig = { + m: vault.scheme.m, + xpubs: sortedXpubs, + addressIndex: receiveAddressIndex, + }; + } + const deviceAddress = await verifyAddressOnOneKey({ + connectId: storedConnectId, + deviceId: deviceInfo.deviceId, + path: addressPath, + networkType, + multisigConfig, + }); + + setPhase('connecting'); + + if (deviceAddress === receivingAddress) { + dispatch(updateKeyDetails(vaultKey, 'registered', { registered: true, vaultId })); + dispatch( + healthCheckStatusUpdate([ + { signerId: signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_VERIFICATION }, + ]) + ); + showToast('Address verified successfully on OneKey', ); + } else { + showToast('Address mismatch! The address on device does not match.', ); + } + close(); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + close(); + } + }; + + const handleDeviceTap = mode === 'identify' ? handleIdentifyTap : handleSetupTap; + + // ─── Render helpers ──────────────────────────────────────────────────────── + + const renderDevice = ({ item: device }: { item: SearchDevice }) => { + const img = getDeviceImage(device?.deviceType); + return ( + handleDeviceTap(device)} + activeOpacity={0.7} + > + + {img ? ( + + ) : ( + + OK + + )} + + + {getDeviceDisplayName(device)} + + + + + + ); + }; + + const ModalContent = () => { + const ErrorMessage = () => + errorMessage ? ( + + {errorMessage} + + ) : null; + + // SDK prompt phase — show device interaction prompt + if (phase === 'sdk-prompt' && sdkPrompt !== 'idle') { + return ( + + + + {UI_PROMPTS[sdkPrompt]} + + + ); + } + + // Connecting phase + if (phase === 'connecting') { + return ( + + + + {statusMessage || 'Connecting...'} + + + ); + } + + // Scan phase — show device list or loading + if (scanning && devices.length === 0) { + return ( + + + + Looking for devices... + + + ); + } + + if (!scanning && devices.length === 0) { + return ( + + + + No devices found. Make sure your OneKey is unlocked and nearby. + + + + Rescan + + + + ); + } + + // Devices found + return ( + + + + Select your device + + + + Rescan + + + + + `${d?.uuid || ''}-${d?.connectId || ''}`} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + style={styles.list} + /> + + ); + }; + + if (!visible) return null; + + const title = + mode === 'setup' ? 'Setting up OneKey' + : mode === 'identify' ? 'Identify OneKey' + : mode === 'recovery' ? 'Recover with OneKey' + : mode === 'verify-address' ? 'Verify Address' + : 'Verify OneKey'; + const subTitle = + mode === 'setup' ? 'Connect OneKey hardware wallet via Bluetooth' + : mode === 'identify' ? 'Select your OneKey and confirm it matches this key' + : mode === 'recovery' ? 'Select your OneKey to continue wallet recovery' + : mode === 'verify-address' ? 'Confirm the address matches on your OneKey device' + : 'Verify your OneKey device is accessible'; + + return ( + + ); +} + +const styles = StyleSheet.create({ + centerContent: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40, + gap: 16, + }, + promptText: { + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + paddingHorizontal: 20, + }, + statusText: { + fontSize: 14, + textAlign: 'center', + paddingHorizontal: 20, + }, + rescanText: { + fontSize: 14, + fontWeight: '600', + }, + listHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + listHeaderText: { + fontSize: 13, + }, + list: { + maxHeight: 300, + }, + listContent: { + gap: 8, + }, + errorBanner: { + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + marginBottom: 12, + backgroundColor: '#FCEAEA', + }, + errorText: { + color: '#B42318', + fontSize: 13, + lineHeight: 18, + textAlign: 'center', + }, + deviceItem: { + borderWidth: 1, + borderRadius: 10, + paddingHorizontal: 16, + paddingVertical: 12, + }, + deviceRow: { + flexDirection: 'row', + alignItems: 'center', + }, + deviceImage: { + width: 40, + height: 40, + borderRadius: 8, + marginRight: 12, + }, + fallbackIcon: { + width: 40, + height: 40, + borderRadius: 8, + backgroundColor: '#44D62C', + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + fallbackText: { + fontSize: 14, + fontWeight: '700', + color: '#000', + }, + deviceName: { + fontSize: 15, + fontWeight: '500', + }, + arrow: { + fontSize: 22, + fontWeight: '300', + marginLeft: 8, + }, +}); + +export default OneKeyBleModal; diff --git a/src/components/ThemedSvg.tsx/ThemedIcons.js b/src/components/ThemedSvg.tsx/ThemedIcons.js index c5ac8bedeb..69a9c37b3c 100644 --- a/src/components/ThemedSvg.tsx/ThemedIcons.js +++ b/src/components/ThemedSvg.tsx/ThemedIcons.js @@ -150,6 +150,7 @@ import SatochipSetupSVG from 'src/assets/images/SatochipSetup.svg'; import PrivateSatochipIllustration from 'src/assets/privateImages/satochip-illustration.svg'; import PortalIllustration from 'src/assets/images/portal_illustration.svg'; import PrivatePortalIllustration from 'src/assets/privateImages/portal-illustration.svg'; +import OneKeyIllustration from 'src/assets/images/onekey_illustration.svg'; import WalletRecoveryIcon from 'src/assets/images/walletRecoveryIcon.svg'; import PrivateWalletRecovery from 'src/assets/privateImages/wallet-recovery-illlustration.svg'; import OrganizationIcon from 'src/assets/images/organizationIcon.svg'; @@ -767,6 +768,12 @@ const themeIcons = { PRIVATE: PrivatePortalIllustration, PRIVATE_LIGHT: PrivatePortalIllustration, }, + onekey_illustration: { + DARK: OneKeyIllustration, + LIGHT: OneKeyIllustration, + PRIVATE: OneKeyIllustration, + PRIVATE_LIGHT: OneKeyIllustration, + }, wallet_Recovery_icon: { DARK: WalletRecoveryIcon, diff --git a/src/constants/HardwareReferralLinks.ts b/src/constants/HardwareReferralLinks.ts index d402333bee..32a2576b1e 100644 --- a/src/constants/HardwareReferralLinks.ts +++ b/src/constants/HardwareReferralLinks.ts @@ -39,6 +39,11 @@ export const sellers = [ link: 'https://affil.trezor.io/aff_c?offer_id=134&aff_id=35017', id: '67befce7bb95d55d985d844a', }, + { + identifier: 'onekey', + link: 'https://onekey.so/zh_CN/products/onekey-pro/', + id: 'onekey_pro', + }, ]; export const resellers = [ { diff --git a/src/hardware/index.ts b/src/hardware/index.ts index 8ad7b62a09..fd204faf45 100644 --- a/src/hardware/index.ts +++ b/src/hardware/index.ts @@ -183,6 +183,9 @@ export const getSignerNameFromType = (type: SignerType, isMock = false, isAmf = case SignerType.LEDGER: name = 'Ledger'; break; + case SignerType.ONEKEY: + name = 'OneKey'; + break; case SignerType.MOBILE_KEY: name = 'Recovery Key'; break; @@ -476,6 +479,9 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { case SignerType.TREZOR: { return 'Trusted signers from SatoshiLabs'; } + case SignerType.ONEKEY: { + return 'OneKey hardware wallet over Bluetooth'; + } case SignerType.OTHER_SD: { return 'Varies with different signer'; } diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index 2f27ed9086..0db624a847 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -46,8 +46,10 @@ import WalletSettings from 'src/screens/WalletDetails/WalletSettings'; import Colors from 'src/theme/Colors'; import NodeSettings from 'src/screens/AppSettings/Node/NodeSettings'; import ConnectChannel from 'src/screens/Channel/ConnectChannel'; +import SignMessageOneKeyBle from 'src/screens/OneKey/SignMessageOneKeyBle'; import RegisterWithChannel from 'src/screens/QRScreens/RegisterWithChannel'; import SignWithChannel from 'src/screens/QRScreens/SignWithChannel'; +import SignWithOneKeyBle from 'src/screens/SignTransaction/SignWithOneKeyBle'; import UTXOLabeling from 'src/screens/UTXOManagement/UTXOLabeling'; import UTXOManagement from 'src/screens/UTXOManagement/UTXOManagement'; import ImportWalletDetailsScreen from 'src/screens/ImportWalletDetailsScreen/ImportWalletDetailsScreen'; @@ -210,6 +212,7 @@ function LoginStack() { {/* Channel Based SDs */} + {/* Mobile Key, Seed Key */} @@ -321,9 +324,11 @@ function AppStack() { + + diff --git a/src/navigation/types.ts b/src/navigation/types.ts index d29b4d6f13..8143751a40 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -96,9 +96,11 @@ export type AppStackParams = { ScanNode: undefined; PrivacyAndDisplay: undefined; ConnectChannel: undefined; + SignMessageOneKeyBle: undefined; RegisterWithChannel: undefined; SetupOtherSDScreen: undefined; SignWithChannel: undefined; + SignWithOneKeyBle: undefined; CosignerDetails: { signer: Signer }; AdditionalDetails: { signer: Signer }; KeyHistory: undefined; diff --git a/src/screens/Hardware/components/DeviceCard.tsx b/src/screens/Hardware/components/DeviceCard.tsx index 3fb788a889..c2124ee94a 100644 --- a/src/screens/Hardware/components/DeviceCard.tsx +++ b/src/screens/Hardware/components/DeviceCard.tsx @@ -146,7 +146,9 @@ const styles = StyleSheet.create({ flagContainer: { flexDirection: 'row', alignItems: 'center', - gap: wp(8), + gap: wp(4), + flexWrap: 'wrap', + flex: 1, }, subText: { marginTop: hp(10), diff --git a/src/screens/Hardware/components/HardwareDevices.tsx b/src/screens/Hardware/components/HardwareDevices.tsx index e2ac11dafe..ab6bf91f75 100644 --- a/src/screens/Hardware/components/HardwareDevices.tsx +++ b/src/screens/Hardware/components/HardwareDevices.tsx @@ -8,6 +8,13 @@ import ColdCard from 'src/assets/images/coinkite-image.svg'; import Passport from 'src/assets/images/foundation-passport-icon.svg'; import Legder from 'src/assets/images/Ledger-icon.svg'; import Trezor from 'src/assets/images/TREZOR-icon.svg'; +import { Image, ImageStyle } from 'react-native'; + +const OnekeyDevice = ({ width, height }: { width: number; height: number }) => ( + +); +import FlagHongKong from 'src/assets/images/flag-hongkong.svg'; +import FlagJapan from 'src/assets/images/flag-japan.svg'; import FlagCanada from 'src/assets/images/flag-canada.svg'; import FlagUSA from 'src/assets/images/flag-usa.svg'; import FlagSwizerland from 'src/assets/images/flag-swizerland.svg'; @@ -83,6 +90,16 @@ const HardwareDevices = ({ sellers }) => { subscribeText: '', unSubscribeText: '', }, + { + id: 6, + title: 'OneKey', + image: , + flagIcon: <>, + country: 'HK & Japan', + link: getSellerLink('onekey'), + subscribeText: '', + unSubscribeText: '', + }, ]; return ( diff --git a/src/screens/Home/components/Keys/SignerContent.tsx b/src/screens/Home/components/Keys/SignerContent.tsx index 25f6b8124a..4e3d3c7810 100644 --- a/src/screens/Home/components/Keys/SignerContent.tsx +++ b/src/screens/Home/components/Keys/SignerContent.tsx @@ -44,6 +44,7 @@ const SignerContent = ({ navigation, handleModalClose }) => { background: 'headerWhite', isTrue: false, }, + { type: SignerType.ONEKEY, background: 'pantoneGreen', isTrue: true }, ]; const hardwareSnippet = hardwareSigners.map(({ type, background, isTrue }) => ({ diff --git a/src/screens/OneKey/SignMessageOneKeyBle.tsx b/src/screens/OneKey/SignMessageOneKeyBle.tsx new file mode 100644 index 0000000000..9bfae00df9 --- /dev/null +++ b/src/screens/OneKey/SignMessageOneKeyBle.tsx @@ -0,0 +1,161 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { ActivityIndicator, StyleSheet } from 'react-native'; +import { Box, useColorMode } from '@gluestack-ui/themed-native-base'; +import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import WalletHeader from 'src/components/WalletHeader'; +import Text from 'src/components/KeeperText'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import TickIcon from 'src/assets/images/icon_tick.svg'; +import { useAppSelector } from 'src/store/hooks'; +import { NetworkType } from 'src/services/wallets/enums'; +import { + assertOneKeyFingerprint, + ensureOneKeyBLEReady, + getOneKeyDeviceInfo, + searchOneKeyDevices, + signMessageWithOneKey, + onekeyUIEmitter, + ONEKEY_UI_EVENT, + type OneKeyUIEvent, +} from 'src/services/onekeyBle'; +import { UI_REQUEST } from '@onekeyfe/hd-core'; +import { captureError } from 'src/services/sentry'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import type { Signer } from 'src/services/wallets/interfaces/vault'; + +const UI_PROMPTS: Record = { + [UI_REQUEST.REQUEST_PIN]: 'Please enter PIN on your OneKey device', + [UI_REQUEST.REQUEST_BUTTON]: 'Please confirm on your OneKey device', +}; + +type Params = { + message: string; + address: string; + derivationPath: string; + signer: Signer; + onSignatureReceived: (signature: string, address: string) => void; +}; + +function SignMessageOneKeyBle() { + const { colorMode } = useColorMode(); + const { params } = useRoute(); + const navigation = useNavigation(); + const { showToast } = useToastMessage(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + + const { message, address, derivationPath, signer, onSignatureReceived } = params as Params; + + const { bitcoinNetworkType } = useAppSelector((state) => state.settings); + const networkType = + bitcoinNetworkType === NetworkType.TESTNET ? NetworkType.TESTNET : NetworkType.MAINNET; + + const [statusMessage, setStatusMessage] = useState('Preparing...'); + const [sdkPrompt, setSdkPrompt] = useState(''); + + // Listen to SDK UI events + useEffect(() => { + const sub = onekeyUIEmitter.addListener(ONEKEY_UI_EVENT, (event: OneKeyUIEvent) => { + if (UI_PROMPTS[event]) { + setSdkPrompt(UI_PROMPTS[event]); + } + }); + return () => sub.remove(); + }, []); + + // Auto-run on mount + useEffect(() => { + const timer = setTimeout(() => runSignMessage(), 300); + return () => clearTimeout(timer); + }, []); + + const runSignMessage = async () => { + try { + if (!signer || !derivationPath) { + showToast('Signer not found. Please try again.', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + // BLE needs a brief scan to discover peripherals + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setStatusMessage('Reading device info...'); + setSdkPrompt(''); + const deviceInfo = await getOneKeyDeviceInfo(storedConnectId); + assertOneKeyFingerprint(deviceInfo, signer); + + setStatusMessage('Signing message...'); + setSdkPrompt(''); + const result = await signMessageWithOneKey({ + connectId: storedConnectId, + deviceId: deviceInfo.deviceId, + path: derivationPath, + message, + networkType, + }); + + setSdkPrompt(''); + if (address && result.address !== address) { + showToast('Address mismatch! The signed address does not match.', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + showToast('Message signed successfully', ); + onSignatureReceived?.(result.signature, result.address); + navigation.dispatch(CommonActions.goBack()); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + navigation.dispatch(CommonActions.goBack()); + } + }; + + const displayText = sdkPrompt || statusMessage; + + return ( + + + + + + {displayText} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 16, + paddingHorizontal: 20, + }, + statusText: { + fontSize: 15, + textAlign: 'center', + }, +}); + +export default SignMessageOneKeyBle; diff --git a/src/screens/Recieve/ReceiveScreen.tsx b/src/screens/Recieve/ReceiveScreen.tsx index 6befa1e08d..6f47a14594 100644 --- a/src/screens/Recieve/ReceiveScreen.tsx +++ b/src/screens/Recieve/ReceiveScreen.tsx @@ -44,10 +44,11 @@ import useExchangeRates from 'src/hooks/useExchangeRates'; import useCurrencyCode from 'src/store/hooks/state-selectors/useCurrencyCode'; import { SATOSHIS_IN_BTC } from 'src/constants/Bitcoin'; import { InteracationMode } from '../Vault/HardwareModalMap'; +import OneKeyBleModal from 'src/components/OneKeyBleModal'; import { Vault } from 'src/services/wallets/interfaces/vault'; import KeyPadView from 'src/components/AppNumPad/KeyPadView'; import AmountDetailsInput from '../Send/AmountDetailsInput'; -import { getAccountFromSigner } from 'src/utils/utilities'; +import { getAccountFromSigner, getKeyUID } from 'src/utils/utilities'; import WalletHeader from 'src/components/WalletHeader'; const AddressVerifiableSigners = [ @@ -57,6 +58,7 @@ const AddressVerifiableSigners = [ SignerType.COLDCARD, SignerType.JADE, SignerType.PORTAL, + SignerType.ONEKEY, ]; const SignerTypesNeedingRegistration = [ @@ -80,12 +82,13 @@ function ReceiveScreen({ route }: { route }) { // const amount = route?.params?.amount; const [receivingAddress, setReceivingAddress] = useState(null); const [paymentURI, setPaymentURI] = useState(null); + const [onekeyVerifyState, setOnekeyVerifyState] = useState({ visible: false }); const { translations } = useContext(LocalizationContext); const { common, home, wallet: walletTranslation, vault: vaultTranslations } = translations; const navigation = useNavigation(); - const { vaultSigners } = useSigners(wallet.id); + const { vaultSigners } = useSigners(wallet?.id ?? ''); const [addVerifiableSigners, setAddVerifiableSigners] = useState([]); const [signersNeedRegistration, setSignersNeedRegistration] = useState([]); @@ -281,6 +284,19 @@ function ReceiveScreen({ route }: { route }) { receiveAddressIndex: currentAddressIdx - 1, }) ); + } else if (signer.type === SignerType.ONEKEY) { + const vKey = (wallet as Vault).signers?.find( + (vaultSigner) => getKeyUID(vaultSigner) === getKeyUID(signer) + ); + setOnekeyVerifyState({ + visible: true, + signer, + vaultKey: vKey, + vault: wallet, + vaultId: wallet.id, + receiveAddressIndex: currentAddressIdx - 1, + receivingAddress, + }); } else { navigation.dispatch( CommonActions.navigate('ConnectChannel', { @@ -348,6 +364,7 @@ function ReceiveScreen({ route }: { route }) { }; return ( + <> )} + setOnekeyVerifyState({ visible: false })} + mode="verify-address" + signer={onekeyVerifyState.signer} + vaultKey={onekeyVerifyState.vaultKey} + vault={onekeyVerifyState.vault} + vaultId={onekeyVerifyState.vaultId} + receiveAddressIndex={onekeyVerifyState.receiveAddressIndex} + receivingAddress={onekeyVerifyState.receivingAddress} + /> + ); } diff --git a/src/screens/SignTransaction/SignTransactionScreen.tsx b/src/screens/SignTransaction/SignTransactionScreen.tsx index eae120b884..520d99f49f 100644 --- a/src/screens/SignTransaction/SignTransactionScreen.tsx +++ b/src/screens/SignTransaction/SignTransactionScreen.tsx @@ -144,6 +144,7 @@ function SignTransactionScreen() { const [trezorModal, setTrezorModal] = useState(false); const [bitbox02Modal, setBitbox02Modal] = useState(false); const [otherSDModal, setOtherSDModal] = useState(false); + const [oneKeyModal, setOneKeyModal] = useState(false); const [otpModal, showOTPModal] = useState(false); const [passwordModal, setPasswordModal] = useState(false); const [confirmPassVisible, setConfirmPassVisible] = useState(false); @@ -674,6 +675,9 @@ function SignTransactionScreen() { case SignerType.KRUX: setKruxModal(true); break; + case SignerType.ONEKEY: + setOneKeyModal(true); + break; default: showToast(`action not set for ${signer.type}`); break; @@ -838,10 +842,12 @@ function SignTransactionScreen() { trezorModal={trezorModal} bitbox02Modal={bitbox02Modal} otherSDModal={otherSDModal} + oneKeyModal={oneKeyModal} specterModal={specterModal} kruxModal={kruxModal} setSpecterModal={setSpecterModal} setOtherSDModal={setOtherSDModal} + setOneKeyModal={setOneKeyModal} setTrezorModal={setTrezorModal} setBitbox02Modal={setBitbox02Modal} setJadeModal={setJadeModal} diff --git a/src/screens/SignTransaction/SignWithOneKeyBle.tsx b/src/screens/SignTransaction/SignWithOneKeyBle.tsx new file mode 100644 index 0000000000..d18aed2554 --- /dev/null +++ b/src/screens/SignTransaction/SignWithOneKeyBle.tsx @@ -0,0 +1,196 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { ActivityIndicator, StyleSheet } from 'react-native'; +import { Box, useColorMode } from '@gluestack-ui/themed-native-base'; +import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import WalletHeader from 'src/components/WalletHeader'; +import Text from 'src/components/KeeperText'; +import { VaultSigner } from 'src/services/wallets/interfaces/vault'; +import { useDispatch } from 'react-redux'; +import { useAppSelector } from 'src/store/hooks'; +import { SerializedPSBTEnvelop } from 'src/services/wallets/interfaces'; +import { updatePSBTEnvelops } from 'src/store/reducers/send_and_receive'; +import { captureError } from 'src/services/sentry'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import { + assertOneKeyFingerprint, + ensureOneKeyBLEReady, + getOneKeyDeviceInfo, + searchOneKeyDevices, + signPsbtWithOneKey, + onekeyUIEmitter, + ONEKEY_UI_EVENT, + type OneKeyUIEvent, +} from 'src/services/onekeyBle'; +import { UI_REQUEST } from '@onekeyfe/hd-core'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; +import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; +import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; +import { validatePSBT } from 'src/utils/utilities'; +import { NetworkType } from 'src/services/wallets/enums'; + +const UI_PROMPTS: Record = { + [UI_REQUEST.REQUEST_PIN]: 'Please enter PIN on your OneKey device', + [UI_REQUEST.REQUEST_BUTTON]: 'Please confirm on your OneKey device', +}; + +type SignWithOneKeyBleParams = { + vaultKey: VaultSigner; + isRemoteKey?: boolean; + serializedPSBTEnvelopFromProps?: SerializedPSBTEnvelop; + signTransaction: (args: { signedSerializedPSBT: string }) => void; +}; + +function SignWithOneKeyBle() { + const { colorMode } = useColorMode(); + const { params } = useRoute(); + const navigation = useNavigation(); + const dispatch = useDispatch(); + const { showToast } = useToastMessage(); + const { translations } = useContext(LocalizationContext); + const { common, error: errorText } = translations; + + const { + vaultKey, + isRemoteKey = false, + serializedPSBTEnvelopFromProps, + signTransaction, + } = params as SignWithOneKeyBleParams; + + const { signer } = useSignerFromKey(vaultKey); + const { bitcoinNetworkType } = useAppSelector((state) => state.settings); + const serializedPSBTEnvelops: SerializedPSBTEnvelop[] = useAppSelector( + (state) => state.sendAndReceive.sendPhaseTwo.serializedPSBTEnvelops + ); + + const serializedPSBTEnvelop = useMemo(() => { + if (isRemoteKey) return serializedPSBTEnvelopFromProps; + return serializedPSBTEnvelops?.find((envelop) => envelop.xfp === vaultKey.xfp); + }, [isRemoteKey, serializedPSBTEnvelopFromProps, serializedPSBTEnvelops, vaultKey.xfp]); + + const networkType = + bitcoinNetworkType === NetworkType.TESTNET ? NetworkType.TESTNET : NetworkType.MAINNET; + + const [statusMessage, setStatusMessage] = useState('Preparing...'); + const [sdkPrompt, setSdkPrompt] = useState(''); + + // Listen to SDK UI events + useEffect(() => { + const sub = onekeyUIEmitter.addListener(ONEKEY_UI_EVENT, (event: OneKeyUIEvent) => { + if (UI_PROMPTS[event]) { + setSdkPrompt(UI_PROMPTS[event]); + } + }); + return () => sub.remove(); + }, []); + + // Auto-run on mount + useEffect(() => { + const timer = setTimeout(() => runAutoSign(), 300); + return () => clearTimeout(timer); + }, []); + + const runAutoSign = async () => { + if (!serializedPSBTEnvelop?.serializedPSBT) { + showToast('No PSBT found to sign', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + if (!signer) { + showToast('Signer not found. Please try again.', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + try { + setStatusMessage('Checking Bluetooth...'); + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + // Use stored connectId for direct connection + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + // BLE needs a brief scan to discover peripherals + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setSdkPrompt(''); + setStatusMessage('Reading device info...'); + const deviceInfo = await getOneKeyDeviceInfo(storedConnectId); + assertOneKeyFingerprint(deviceInfo, signer); + + setSdkPrompt(''); + setStatusMessage('Signing transaction on device...'); + const signedSerializedPSBT = await signPsbtWithOneKey({ + connectId: storedConnectId, + deviceId: deviceInfo.deviceId, + networkType, + serializedPSBT: serializedPSBTEnvelop.serializedPSBT, + }); + + setSdkPrompt(''); + validatePSBT(serializedPSBTEnvelop.serializedPSBT, signedSerializedPSBT, signer, errorText); + + dispatch( + healthCheckStatusUpdate([ + { signerId: signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_SIGNING }, + ]) + ); + + if (isRemoteKey) { + signTransaction({ signedSerializedPSBT }); + navigation.dispatch(CommonActions.goBack()); + return; + } + + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); + navigation.dispatch(CommonActions.navigate({ name: 'SignTransactionScreen', merge: true })); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + navigation.dispatch(CommonActions.goBack()); + } + }; + + const displayText = sdkPrompt || statusMessage; + + return ( + + + + + + {displayText} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 16, + paddingHorizontal: 20, + }, + statusText: { + fontSize: 15, + textAlign: 'center', + }, +}); + +export default SignWithOneKeyBle; diff --git a/src/screens/SignTransaction/SignerModals.tsx b/src/screens/SignTransaction/SignerModals.tsx index 0a3bf2e548..cbeb3c4860 100644 --- a/src/screens/SignTransaction/SignerModals.tsx +++ b/src/screens/SignTransaction/SignerModals.tsx @@ -593,6 +593,22 @@ const getSupportedSigningOptions = (signerType: SignerType, colorMode) => { }, ], }; + case SignerType.ONEKEY: + return { + supportedSigningOptions: [ + { + title: 'Bluetooth', + icon: ( + } + backgroundColor={`${colorMode}.pantoneGreen`} + width={35} + /> + ), + name: SigningMode.USB, + }, + ], + }; default: return { supportedSigningOptions: [], @@ -618,8 +634,10 @@ function SignerModals({ bitbox02Modal, portalModal, otherSDModal, + oneKeyModal, kruxModal, setOtherSDModal, + setOneKeyModal, setTrezorModal, setBitbox02Modal, setJadeModal, @@ -665,8 +683,10 @@ function SignerModals({ bitbox02Modal: boolean; portalModal: boolean; otherSDModal: boolean; + oneKeyModal: boolean; kruxModal: boolean; setOtherSDModal: any; + setOneKeyModal: any; setTrezorModal: any; setBitbox02Modal: any; setJadeModal: any; @@ -780,6 +800,18 @@ function SignerModals({ }) ); }; + + const navigateToOneKeySigning = (vaultKey: VaultSigner) => { + setOneKeyModal(false); + navigation.dispatch( + CommonActions.navigate('SignWithOneKeyBle', { + signTransaction, + vaultKey, + isRemoteKey, + serializedPSBTEnvelopFromProps, + }) + ); + }; const [registeredSigner, setRegisteredSigner] = useState(null); const [registeredVaultKey, setRegisteredVaultKey] = useState(null); const [registerActiveVault, setRegisterActiveVault] = useState(null); @@ -1688,6 +1720,23 @@ function SignerModals({ ); } + if (signer.type === SignerType.ONEKEY) { + return ( + setOneKeyModal(false)} + title="Connect OneKey" + subTitle="Connect your OneKey via Bluetooth to sign the transaction" + modalBackground={`${colorMode}.modalWhiteBackground`} + textColor={`${colorMode}.textGreen`} + subTitleColor={`${colorMode}.modalSubtitleBlack`} + buttonText={common.proceed} + buttonCallback={() => navigateToOneKeySigning(vaultKey)} + Content={() => null} + /> + ); + } return null; })} diff --git a/src/screens/Vault/AssignSignerType.tsx b/src/screens/Vault/AssignSignerType.tsx index b84f83d344..73df024c62 100644 --- a/src/screens/Vault/AssignSignerType.tsx +++ b/src/screens/Vault/AssignSignerType.tsx @@ -33,7 +33,7 @@ type IProps = { vault: Vault; signer: Signer; isImportFlow?: boolean; - onTypeSelection?: (type: SignerType) => void; + onTypeSelection?: (type: SignerType, signerUpdates?: Partial) => void; }; }; }; @@ -81,6 +81,7 @@ function AssignSignerType({ route }: IProps) { SignerType.KEYSTONE, SignerType.KRUX, SignerType.LEDGER, + SignerType.ONEKEY, SignerType.PASSPORT, SignerType.PORTAL, SignerType.SATOCHIP, diff --git a/src/screens/Vault/HardwareModalMap.tsx b/src/screens/Vault/HardwareModalMap.tsx index 919e75bd36..cdbadbaafc 100644 --- a/src/screens/Vault/HardwareModalMap.tsx +++ b/src/screens/Vault/HardwareModalMap.tsx @@ -29,6 +29,7 @@ import CVVInputsView from 'src/components/HealthCheck/CVVInputsView'; import DeleteIcon from 'src/assets/images/deleteBlack.svg'; import RecoverImage from 'src/assets/images/recover_white.svg'; import KeeperModal from 'src/components/KeeperModal'; +import OneKeyBleModal from 'src/components/OneKeyBleModal'; import KeyPadView from 'src/components/AppNumPad/KeyPadView'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import { Signer, VaultSigner } from 'src/services/wallets/interfaces/vault'; @@ -542,6 +543,18 @@ const getSignerContent = ( subTitle: ledger.SetupDescription, options: [], }; + case SignerType.ONEKEY: + return { + type: SignerType.ONEKEY, + Illustration: , + Instructions: [ + 'Unlock your OneKey device and enable Bluetooth.', + 'Keep the device nearby and tap Proceed to connect.', + ], + title: isHealthcheck ? `${common.verify} OneKey` : `${signerText.settingUp} OneKey`, + subTitle: 'Connect OneKey hardware wallet via Bluetooth', + options: [], + }; case SignerType.SEED_WORDS: return { type: SignerType.SEED_WORDS, @@ -1052,6 +1065,7 @@ function HardwareModalMap({ const data = useQuery(RealmSchema.BackupHistory); const [backupModalVisible, setBackupModalVisible] = useState(false); const [openSetup, setOpenSetup] = useState(false); + const [onekeyBleModalVisible, setOnekeyBleModalVisible] = useState(false); const getNfcSupport = async () => { const isSupported = await NFC.isNFCSupported(); @@ -1333,6 +1347,7 @@ function HardwareModalMap({ ); }; + const importSeedWordsBasedKey = (mnemonic, remember = false) => { try { const { signer, key } = setupSeedWordsBasedKey(mnemonic, isMultisig, remember); @@ -2021,7 +2036,8 @@ function HardwareModalMap({ signerType === SignerType.PASSPORT || signerType === SignerType.SEED_WORDS || signerType === SignerType.KEEPER || - signerType === SignerType.KRUX + signerType === SignerType.KRUX || + signerType === SignerType.ONEKEY ) { return ( @@ -2165,6 +2181,10 @@ function HardwareModalMap({ return navigateToSetupWithOtherSD(); case SignerType.PORTAL: return navigateToPortalSetup(); + case SignerType.ONEKEY: + close(); + setOnekeyBleModalVisible(true); + return; default: return null; } @@ -2447,6 +2467,35 @@ function HardwareModalMap({ /> {inProgress && } + setOnekeyBleModalVisible(false)} + mode={ + isHealthcheck + ? 'health-check' + : mode === InteracationMode.RECOVERY + ? 'recovery' + : 'setup' + } + signer={signer} + isMultisig={isMultisig} + addSignerFlow={addSignerFlow} + accountNumber={accountNumber} + onSignerAdded={(addedSigner) => { + setOnekeyBleModalVisible(false); + if (mode === InteracationMode.RECOVERY) { + dispatch(setSigningDevices(addedSigner)); + navigation.dispatch( + CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) + ); + return; + } + const navigationState = addSignerFlow + ? { name: 'Home', params: { selectedOption: 'Keys', addedSigner } } + : { name: 'AddSigningDevice', merge: true, params: { addedSigner } }; + navigation.dispatch(CommonActions.navigate(navigationState)); + }} + /> ); } diff --git a/src/screens/Vault/ImportedWalletSetup.tsx b/src/screens/Vault/ImportedWalletSetup.tsx index 9102fdab4e..31fd64dc1a 100644 --- a/src/screens/Vault/ImportedWalletSetup.tsx +++ b/src/screens/Vault/ImportedWalletSetup.tsx @@ -18,18 +18,23 @@ import { useDispatch } from 'react-redux'; import { resetSignersUpdateState } from 'src/store/reducers/bhr'; import useSignerMap from 'src/hooks/useSignerMap'; import { SignerType } from 'src/services/wallets/enums'; +import { Signer } from 'src/services/wallets/interfaces/vault'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; export const ImportedWalletSetup = ({ navigation, route }) => { const { vaultConfig } = route?.params; const { colorMode } = useColorMode(); const { createVault } = useConfigRecovery(); - const { common, wallet: walletText, importWallet } = useContext(LocalizationContext).translations; + const { common, wallet: walletText, importWallet, error: errorText } = + useContext(LocalizationContext).translations; const [vaultDetails, setVaultDetails] = useState(null); const [vaultName, setVaultName] = useState('Imported wallet'); const [vaultDesc, setVaultDesc] = useState('Secure your sats'); const isSmallDevice = useIsSmallDevices(); const [signers, setSigners] = useState([]); const { signerMap } = useSignerMap(); + const { showToast } = useToastMessage(); const dispatch = useDispatch(); @@ -45,7 +50,13 @@ export const ImportedWalletSetup = ({ navigation, route }) => { }, []); const updateSignerType = (selectedSigner) => { - if (!selectedSigner.isTemp) return; + if (!selectedSigner?.isTemp) { + showToast( + (errorText as any).keyAlreadyAdded || 'This key has already been added', + + ); + return; + } dispatch(resetSignersUpdateState()); navigation.dispatch( CommonActions.navigate({ @@ -54,11 +65,13 @@ export const ImportedWalletSetup = ({ navigation, route }) => { parentNavigation: navigation, signer: selectedSigner, isImportFlow: true, - onTypeSelection: (type) => { + onTypeSelection: (type, signerUpdates: Partial = {}) => { const updatedSigners = signers.map((signer) => { if (signer.id === selectedSigner.id) { + Object.assign(signer, signerUpdates); signer.type = type; - signer.signerName = getSignerNameFromType(type, signer.isMock); + signer.signerName = + signerUpdates.signerName || getSignerNameFromType(type, signer.isMock); } return signer; }); @@ -125,8 +138,7 @@ export const ImportedWalletSetup = ({ navigation, route }) => { isFullText colorVarient="green" colorMode={colorMode} - onCardSelect={(signer) => updateSignerType(signer)} - isSelected={signer} + onCardSelect={() => updateSignerType(signer)} /> ); })} diff --git a/src/screens/Vault/SignerAdvanceSettings.tsx b/src/screens/Vault/SignerAdvanceSettings.tsx index 39fc20d0d5..cea1a02cdb 100644 --- a/src/screens/Vault/SignerAdvanceSettings.tsx +++ b/src/screens/Vault/SignerAdvanceSettings.tsx @@ -932,6 +932,14 @@ function SignerAdvanceSettings({ route }: any) { description: signerTranslation.kruxDesc, FAQ: 'https://selfcustody.github.io/krux/faq/', }; + case SignerType.ONEKEY: + return { + title: 'OneKey', + subTitle: 'Connect OneKey hardware wallet via Bluetooth', + assert: , + description: 'OneKey hardware wallet over Bluetooth', + FAQ: 'https://help.onekey.so', + }; default: return { title: '', diff --git a/src/screens/Vault/SignerCategoryList.tsx b/src/screens/Vault/SignerCategoryList.tsx index 427a1fc370..d7e506be58 100644 --- a/src/screens/Vault/SignerCategoryList.tsx +++ b/src/screens/Vault/SignerCategoryList.tsx @@ -61,6 +61,7 @@ function SignerCategoryList() { { type: SignerType.SPECTER, background: 'pantoneGreen', isTrue: false }, { type: SignerType.KEYSTONE, background: 'brownBackground', isTrue: false }, { type: SignerType.LEDGER, background: 'headerWhite', isTrue: false }, + { type: SignerType.ONEKEY, background: 'headerWhite', isTrue: false }, { type: SignerType.PORTAL, background: 'pantoneGreen', isTrue: false }, { type: SignerType.TREZOR, background: 'brownBackground', isTrue: false }, { type: SignerType.BITBOX02, background: 'headerWhite', isTrue: false }, diff --git a/src/screens/Vault/SigningDeviceDetails.tsx b/src/screens/Vault/SigningDeviceDetails.tsx index 8f31fcc280..72f7ef59c5 100644 --- a/src/screens/Vault/SigningDeviceDetails.tsx +++ b/src/screens/Vault/SigningDeviceDetails.tsx @@ -231,6 +231,14 @@ const getSignerContent = (type: SignerType) => { description: signerTranslations.kruxDesc, FAQ: 'https://selfcustody.github.io/krux/faq/', }; + case SignerType.ONEKEY: + return { + title: 'OneKey', + subTitle: 'Connect OneKey hardware wallet via Bluetooth', + assert: , + description: 'OneKey hardware wallet over Bluetooth', + FAQ: 'https://help.onekey.so', + }; default: return { title: '', diff --git a/src/screens/Vault/SigningDeviceIcons.tsx b/src/screens/Vault/SigningDeviceIcons.tsx index 064be46936..141ede94b5 100644 --- a/src/screens/Vault/SigningDeviceIcons.tsx +++ b/src/screens/Vault/SigningDeviceIcons.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { SignerStorage, SignerType } from 'src/services/wallets/enums'; +import ONEKEYICON from 'src/assets/images/onekey_icon.svg'; +import ONEKEYICONLIGHT from 'src/assets/images/onekey_icon_light.svg'; +import ONEKEYLOGO from 'src/assets/images/onekey_logo.svg'; +import ONEKEYLOGOWHITE from 'src/assets/images/onekey_logo_white.svg'; import COLDCARDICON from 'src/assets/images/coldcard_icon.svg'; import COLDCARDICONLIGHT from 'src/assets/images/coldcard_light.svg'; import COLDCARDLOGO from 'src/assets/images/coldcard_logo.svg'; @@ -84,6 +88,8 @@ import BITBOXGREENLIGHT from 'src/assets/images/bitbox-green-light.svg'; import BITBOXGREENDARK from 'src/assets/images/bitbox-green-dark.svg'; import TREZORGREENLIGHT from 'src/assets/images/trezor-green-light.svg'; import TREZORGREENDARK from 'src/assets/images/trezor-green-dark.svg'; +import ONEKEYGREENLIGHT from 'src/assets/images/onekey-green-light.svg'; +import ONEKEYGREENDARK from 'src/assets/images/onekey-green-dark.svg'; import PortalLogo from 'src/assets/images/portalLogo.svg'; import PortalLogoLight from 'src/assets/images/PortalLogoLight.svg'; import PortalIcon from 'src/assets/images/portalIcon.svg'; @@ -156,6 +162,12 @@ export const SDIcons = ({ type, light = true, width = 20, height = 20 }: SDIconO Logo: colorMode === 'dark' ? : , type: SignerStorage.COLD, }; + case SignerType.ONEKEY: + return { + Icon: getColouredIcon(, , light, width, height), + Logo: colorMode === 'dark' ? : , + type: SignerStorage.COLD, + }; case SignerType.MOBILE_KEY: return { Icon: getColouredIcon(, , light, width, height), @@ -312,6 +324,11 @@ export const SDColoredIcons = (type: SignerType, light = true, width = 20, heigh Icon: getColouredIcon(, , light, width, height), type: SignerStorage.COLD, }; + case SignerType.ONEKEY: + return { + Icon: getColouredIcon(, , light, width, height), + type: SignerStorage.COLD, + }; case SignerType.MOBILE_KEY: return { Icon: getColouredIcon( diff --git a/src/screens/Vault/SigningDeviceList.tsx b/src/screens/Vault/SigningDeviceList.tsx index 6b895d9a1c..01fe546e03 100644 --- a/src/screens/Vault/SigningDeviceList.tsx +++ b/src/screens/Vault/SigningDeviceList.tsx @@ -88,6 +88,7 @@ const SigningDeviceList = () => { SignerType.KEYSTONE, SignerType.KRUX, SignerType.LEDGER, + SignerType.ONEKEY, SignerType.PASSPORT, SignerType.PORTAL, SignerType.SATOCHIP, diff --git a/src/screens/Vault/components/AssignSignerTypeCard.tsx b/src/screens/Vault/components/AssignSignerTypeCard.tsx index 3048e50850..b4ff019f67 100644 --- a/src/screens/Vault/components/AssignSignerTypeCard.tsx +++ b/src/screens/Vault/components/AssignSignerTypeCard.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import { Box, Toast, useColorMode } from '@gluestack-ui/themed-native-base'; +import { Box, useColorMode } from '@gluestack-ui/themed-native-base'; import { SignerType, XpubTypes } from 'src/services/wallets/enums'; import { Signer, Vault } from 'src/services/wallets/interfaces/vault'; import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; @@ -26,6 +26,10 @@ import WalletUtilities from 'src/services/wallets/operations/utils'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import TickIcon from 'src/assets/images/icon_tick.svg'; import ThemedSvg from 'src/components/ThemedSvg.tsx/ThemedSvg'; +import OneKeyBleModal from 'src/components/OneKeyBleModal'; +import { getDeviceTypeName } from 'src/services/onekeyBle/deviceConstants'; +import type { SearchDevice } from '@onekeyfe/hd-core'; +import type { OneKeyDeviceInfo } from 'src/services/onekeyBle'; type AssignSignerTypeCardProps = { type: SignerType; @@ -36,7 +40,7 @@ type AssignSignerTypeCardProps = { primaryMnemonic: string; signer?: Signer; isImportFlow?: boolean; - onTypeSelection?: (type: SignerType) => void; + onTypeSelection?: (type: SignerType, signerUpdates?: Partial) => void; }; function AssignSignerTypeCard({ @@ -51,6 +55,7 @@ function AssignSignerTypeCard({ }: AssignSignerTypeCardProps) { const [showConfirm, setShowConfirm] = useState(false); const [validationModal, showValidationModal] = useState(false); + const [oneKeyIdentifyVisible, setOneKeyIdentifyVisible] = useState(false); const [otp, setOtp] = useState(''); const { colorMode } = useColorMode(); const isDarkMode = colorMode === 'dark'; @@ -59,21 +64,51 @@ function AssignSignerTypeCard({ const { common, vault: vaultText, error: errorText } = translations; const navigation = useNavigation(); + const persistSignerType = (signerType: SignerType, signerUpdates: Partial = {}) => { + const signerName = + signerUpdates.signerName || getSignerNameFromType(signerType, signer?.isMock); + + if (isImportFlow) { + navigation.goBack(); + onTypeSelection(signerType, { ...signerUpdates, signerName }); + } else { + dispatch(updateSignerDetails(signer, 'type', signerType)); + dispatch(updateSignerDetails(signer, 'signerName', signerName)); + Object.entries(signerUpdates).forEach(([key, value]) => { + if (key !== 'signerName') dispatch(updateSignerDetails(signer, key, value)); + }); + } + }; + const changeSignerType = () => { setShowConfirm(false); if (type === SignerType.POLICY_SERVER) { showValidationModal(true); + } else if (type === SignerType.ONEKEY) { + setOneKeyIdentifyVisible(true); } else { - if (isImportFlow) { - navigation.goBack(); - onTypeSelection(type); - } else { - dispatch(updateSignerDetails(signer, 'type', type)); - dispatch( - updateSignerDetails(signer, 'signerName', getSignerNameFromType(type, signer.isMock)) - ); - } + persistSignerType(type); + } + }; + + const onOneKeyIdentified = ({ + device, + deviceInfo, + }: { + device: SearchDevice; + deviceInfo: OneKeyDeviceInfo; + }) => { + const bleName = device?.name; + const signerUpdates: Partial = { + signerName: getDeviceTypeName(device), + extraData: { ...signer?.extraData, bleConnectId: deviceInfo.connectId }, + }; + + if (bleName && bleName !== 'Unknown') { + signerUpdates.signerDescription = bleName; } + + persistSignerType(SignerType.ONEKEY, signerUpdates); }; const validateServerKey = async () => { @@ -276,6 +311,14 @@ function AssignSignerTypeCard({ subTitleColor={`${colorMode}.modalSubtitleBlack`} Content={otpContent} /> + + setOneKeyIdentifyVisible(false)} + mode="identify" + signer={signer} + onDeviceIdentified={onOneKeyIdentified} + /> ); } diff --git a/src/screens/Vault/components/IdentifySignerModal.tsx b/src/screens/Vault/components/IdentifySignerModal.tsx index 3bbae55f94..0d660ac878 100644 --- a/src/screens/Vault/components/IdentifySignerModal.tsx +++ b/src/screens/Vault/components/IdentifySignerModal.tsx @@ -34,6 +34,7 @@ function IdentifySignerModal({ visible, close, signer, secondaryCallback, vaultI name: 'AssignSignerType', params: { vault: activeVault, + signer, }, }) ); diff --git a/src/screens/WalletDetails/SignMessageScreen.tsx b/src/screens/WalletDetails/SignMessageScreen.tsx index b634f63d9d..22b8520919 100644 --- a/src/screens/WalletDetails/SignMessageScreen.tsx +++ b/src/screens/WalletDetails/SignMessageScreen.tsx @@ -14,14 +14,17 @@ import KeeperModal from 'src/components/KeeperModal'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import { useAppSelector } from 'src/store/hooks'; import WalletOperations from 'src/services/wallets/operations'; +import WalletUtilities from 'src/services/wallets/operations/utils'; import useWallets from 'src/hooks/useWallets'; import { useDispatch } from 'react-redux'; import { refreshWallets } from 'src/store/sagaActions/wallets'; import useVault from 'src/hooks/useVault'; -import { EntityKind, KeyGenerationMode } from 'src/services/wallets/enums'; +import useSigners from 'src/hooks/useSigners'; +import { EntityKind, KeyGenerationMode, SignerType } from 'src/services/wallets/enums'; import ShowXPub from 'src/components/XPub/ShowXPub'; import { CommonActions } from '@react-navigation/native'; import { InteracationMode } from '../Vault/HardwareModalMap'; +import BLEIcon from 'src/assets/images/usb_white.svg'; import CircleIconWrapper from 'src/components/CircleIconWrapper'; import QRComms from 'src/assets/images/qr_comms.svg'; import ImportIcon from 'src/assets/images/import.svg'; @@ -34,11 +37,20 @@ const MEDIUM_MODES = { IMPORT: 'IMPORT', }; +const getVaultExternalAddressIndex = (vault, targetAddress: string) => { + const externalAddresses = vault?.specs?.addresses?.external || {}; + for (const key in externalAddresses) { + if (externalAddresses[key] === targetAddress) return Number(key); + } + return null; +}; + export const SignMessageScreen = ({ route, navigation }) => { const { walletId = null, vaultId = null, type } = route.params; const wallet = useWallets({ walletIds: [walletId] }).wallets[0]; const { activeVault } = useVault({ vaultId: vaultId ?? '' }); - const { xpriv, addresses } = wallet.specs; + const { vaultSigners } = useSigners(vaultId ?? ''); + const { xpriv, addresses } = wallet?.specs ?? {}; const receiveAddressCache = addresses?.external; const { colorMode } = useColorMode(); const [message, setMessage] = useState(''); @@ -114,8 +126,67 @@ export const SignMessageScreen = ({ route, navigation }) => { navigation.pop(); }; + const hasOneKeySigner = + activeVault?.signers?.some((s) => { + const signerInfo = vaultSigners?.find( + (vs) => vs.masterFingerprint === s.masterFingerprint + ); + return signerInfo?.type === SignerType.ONEKEY; + }) ?? false; + const onSigningMediumSelection = (medium) => { setMediumModal(false); + + // OneKey BLE direct signing + if (medium === 'BLE') { + const messageAddress = address || activeVault?.specs?.addresses?.external?.[0]; + if (!messageAddress) { + showToast('Please enter the address', ); + return; + } + + const validAddress = WalletUtilities.isValidAddress(messageAddress, bitcoinNetwork); + if (!validAddress) { + showToast('Please enter a valid address', ); + return; + } + + const addressIndex = getVaultExternalAddressIndex(activeVault, messageAddress); + if (addressIndex == null) { + showToast('Please enter a valid address from the select wallet', ); + return; + } + + const oneKeyVaultKey = activeVault?.signers?.find((s) => { + const signerInfo = vaultSigners?.find( + (vs) => vs.masterFingerprint === s.masterFingerprint + ); + return signerInfo?.type === SignerType.ONEKEY; + }); + const oneKeySigner = vaultSigners?.find( + (vs) => + vs.type === SignerType.ONEKEY && + vs.masterFingerprint === oneKeyVaultKey?.masterFingerprint + ); + if (oneKeyVaultKey && oneKeySigner) { + navigation.dispatch( + CommonActions.navigate('SignMessageOneKeyBle', { + message: message.trim(), + address: messageAddress, + derivationPath: `${oneKeyVaultKey.derivationPath}/0/${addressIndex}`, + signer: oneKeySigner, + onSignatureReceived: (sig: string, addr: string) => { + setSignature(sig); + if (addr) setAddress(addr); + }, + }) + ); + } else { + showToast('OneKey signer not found. Please try again.', ); + } + return; + } + if (mediumMode == MEDIUM_MODES.EXPORT) { if (medium === KeyGenerationMode.QR) { const qrData = WalletOperations.createSignMessageString( @@ -321,7 +392,7 @@ export const SignMessageScreen = ({ route, navigation }) => { modalBackground={`${colorMode}.modalWhiteBackground`} textColor={`${colorMode}.textGreen`} subTitleColor={`${colorMode}.modalSubtitleBlack`} - Content={() => mediumSelectionContent(onSigningMediumSelection)} + Content={() => mediumSelectionContent(onSigningMediumSelection, hasOneKeySigner && mediumMode === MEDIUM_MODES.EXPORT)} /> ); @@ -374,7 +445,7 @@ const styles = StyleSheet.create({ }, }); -const mediumSelectionContent = (onSigningMediumSelection) => { +const mediumSelectionContent = (onSigningMediumSelection, showBleOption = false) => { const { colorMode } = useColorMode(); const options = [ @@ -400,6 +471,21 @@ const mediumSelectionContent = (onSigningMediumSelection) => { ), name: KeyGenerationMode.FILE, }, + ...(showBleOption + ? [ + { + title: 'OneKey (BLE)', + icon: ( + } + backgroundColor={`${colorMode}.pantoneGreen`} + width={35} + /> + ), + name: 'BLE', + }, + ] + : []), ]; return ( @@ -410,7 +496,7 @@ const mediumSelectionContent = (onSigningMediumSelection) => { key={option.name} name={option.title} icon={option.icon} - onSelect={onSigningMediumSelection} + onSelect={() => onSigningMediumSelection(option.name)} /> ))} diff --git a/src/services/onekeyBle/deviceConstants.ts b/src/services/onekeyBle/deviceConstants.ts new file mode 100644 index 0000000000..9211be8bef --- /dev/null +++ b/src/services/onekeyBle/deviceConstants.ts @@ -0,0 +1,37 @@ +import { ImageSourcePropType } from 'react-native'; +import type { SearchDevice } from '@onekeyfe/hd-core'; + +// ─── Device images ─────────────────────────────────────────────────────────── + +export const DEVICE_IMAGES: Record = { + classic: require('src/assets/images/onekey-devices/classic.png'), + classic1s: require('src/assets/images/onekey-devices/classic.png'), + classicpure: require('src/assets/images/onekey-devices/classic-pure.png'), + touch: require('src/assets/images/onekey-devices/touch.png'), + pro: require('src/assets/images/onekey-devices/pro-black.png'), +}; + +// ─── Device type names ─────────────────────────────────────────────────────── + +export const DEVICE_TYPE_NAMES: Record = { + classic: 'OneKey Classic', + classic1s: 'OneKey Classic 1S', + classicpure: 'OneKey Classic 1S Pure', + touch: 'OneKey Touch', + pro: 'OneKey Pro', +}; + +// ─── Helper functions ──────────────────────────────────────────────────────── + +export const getDeviceImage = (deviceOrType?: SearchDevice | string): ImageSourcePropType | null => { + const dt = typeof deviceOrType === 'string' ? deviceOrType : deviceOrType?.deviceType; + return dt ? DEVICE_IMAGES[dt] || null : null; +}; + +export const getDeviceDisplayName = (device: SearchDevice): string => { + if (device?.name && device.name !== 'Unknown') return device.name; + return DEVICE_TYPE_NAMES[device?.deviceType] || 'OneKey Device'; +}; + +export const getDeviceTypeName = (device: SearchDevice): string => + DEVICE_TYPE_NAMES[device?.deviceType] || 'OneKey'; diff --git a/src/services/onekeyBle/index.ts b/src/services/onekeyBle/index.ts new file mode 100644 index 0000000000..5f429158d9 --- /dev/null +++ b/src/services/onekeyBle/index.ts @@ -0,0 +1,514 @@ +import HardwareBLESDK from '@onekeyfe/hd-ble-sdk'; +import { + type CoreApi, + type Features, + type SearchDevice, + UI_EVENT, + UI_REQUEST, + UI_RESPONSE, +} from '@onekeyfe/hd-core'; +import type { + HDNodeType, + InputScriptType, + MultisigRedeemScriptType, +} from '@onekeyfe/hd-transport'; +import BIP32Factory from 'bip32'; +import { BleManager } from 'react-native-ble-plx'; +import { PermissionsAndroid, Platform } from 'react-native'; +import { DeviceEventEmitter } from 'react-native'; +import { NetworkType } from 'src/services/wallets/enums'; +import WalletUtilities from 'src/services/wallets/operations/utils'; +import ecc from 'src/services/wallets/operations/taproot-utils/noble_ecc'; + +// ─── UI Event Emitter ──────────────────────────────────────────────────────── +// Components can listen to these events to show appropriate UI prompts. + +export const onekeyUIEmitter = DeviceEventEmitter; +export const ONEKEY_UI_EVENT = 'onekey-ui-event'; + +// Use SDK's own constants as event values +export type OneKeyUIEvent = typeof UI_REQUEST.REQUEST_PIN | typeof UI_REQUEST.REQUEST_BUTTON | 'idle'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +type SDKResult = { + success: boolean; + payload: T & { error?: string; message?: string }; +}; + +export type OneKeySignerData = { + multiSigPath: string; + multiSigXpub: string; + singleSigPath: string; + singleSigXpub: string; + taprootPath: string; + taprootXpub: string; + mfp: string; +}; + +export type OneKeyDeviceInfo = { + connectId: string; + deviceId: string; + masterFingerprint: string; + deviceLabel: string; // Device name shown on device (e.g. "My OneKey") + serialNo: string; // Hardware serial number (e.g. "PRA471B") +}; + +type OneKeyMultisigAddressConfig = { + m: number; + xpubs: string[]; + addressIndex: number; + isInternal?: boolean; +}; + +// ─── Singleton state ────────────────────────────────────────────────────────── + +let sdkInstance: CoreApi | null = null; +let sdkInitPromise: Promise | null = null; +let bleManager: BleManager | null = null; +let uiListenerBound = false; + +const SCAN_TIMEOUT_MS = 15_000; +const bip32 = BIP32Factory(ecc); +const HD_HARDENED = 0x80000000; + +// ─── SDK core ───────────────────────────────────────────────────────────────── + +const getCoreSdk = () => HardwareBLESDK as unknown as CoreApi; + +const handleUIEvent = (message: any) => { + if (!sdkInstance) return; + + if (message?.type === UI_REQUEST.REQUEST_PIN) { + onekeyUIEmitter.emit(ONEKEY_UI_EVENT, UI_REQUEST.REQUEST_PIN); + sdkInstance.uiResponse({ + type: UI_RESPONSE.RECEIVE_PIN, + payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE', + }); + return; + } + + if (message?.type === UI_REQUEST.REQUEST_BUTTON) { + onekeyUIEmitter.emit(ONEKEY_UI_EVENT, UI_REQUEST.REQUEST_BUTTON); + return; + } + + if (message?.type === UI_REQUEST.REQUEST_PASSPHRASE) { + // Keeper does not support OneKey hidden-wallet passphrases. Keep all + // operations on the main wallet even if a call accidentally asks. + sdkInstance.uiResponse({ + type: UI_RESPONSE.RECEIVE_PASSPHRASE, + payload: { + value: '', + passphraseOnDevice: false, + save: false, + }, + }); + } +}; + +const bindUIListener = (sdk: CoreApi) => { + if (uiListenerBound) return; + sdk.on(UI_EVENT, handleUIEvent); + uiListenerBound = true; +}; + +const getErrorMessage = (result: any): string => { + const code = result?.payload?.code; + const error = result?.payload?.error || result?.payload?.message || ''; + + // Friendly messages for known error codes + if (code === 801 || error.includes('Pin invalid')) { + return 'PIN is incorrect. Please try again on your device.'; + } + if (code === 802 || error.includes('Pin cancelled')) { + return 'PIN entry was cancelled.'; + } + if (error.includes('Failure_ActionCancelled')) { + return 'Action was cancelled on the device.'; + } + + return error || 'OneKey operation failed'; +}; + +export const getOneKeySdk = async (): Promise => { + if (sdkInstance) return sdkInstance; + if (sdkInitPromise) return sdkInitPromise; + + sdkInitPromise = (async () => { + const sdk = getCoreSdk(); + await sdk.init({ debug: false, fetchConfig: true }); + sdkInstance = sdk; + bindUIListener(sdk); + return sdk; + })(); + + try { + return await sdkInitPromise; + } catch (e) { + // Only clear on failure so successful init is cached + sdkInitPromise = null; + throw e; + } +}; + +// ─── BLE readiness ──────────────────────────────────────────────────────────── + +const ensureAndroidBLEPermissions = async (): Promise => { + if (Platform.OS !== 'android') return true; + + const permissions: Parameters[0] = + Number(Platform.Version) >= 31 + ? [ + PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, + ] + : [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION]; + + const result = await PermissionsAndroid.requestMultiple(permissions); + return Object.values(result).every((v) => v === PermissionsAndroid.RESULTS.GRANTED); +}; + +/** + * Wait for BLE adapter to reach a definitive state (PoweredOn / PoweredOff / Unauthorized). + * BleManager may report 'Unknown' or 'Resetting' transiently after construction; we listen + * for the settled state via onStateChange (with emitCurrentState=true) and resolve as soon + * as we get something actionable, or after a 3 s timeout. + */ +const waitForBleState = (mgr: BleManager): Promise => + new Promise((resolve) => { + const sub = mgr.onStateChange((state) => { + if (state !== 'Unknown' && state !== 'Resetting') { + sub.remove(); + resolve(state); + } + }, true); // emitCurrentState = true + setTimeout(() => { + sub.remove(); + mgr.state().then(resolve); + }, 3000); + }); + +export const ensureOneKeyBLEReady = async () => { + const hasPermission = await ensureAndroidBLEPermissions(); + if (!hasPermission) { + return { ready: false as const, reason: 'MISSING_PERMISSION' as const }; + } + + if (!bleManager) bleManager = new BleManager(); + + const bleState = await waitForBleState(bleManager); + if (bleState !== 'PoweredOn') { + return { ready: false as const, reason: 'BLE_OFF' as const }; + } + + return { ready: true as const, reason: null }; +}; + +// ─── Device discovery ───────────────────────────────────────────────────────── + +export const searchOneKeyDevices = async (): Promise => { + const sdk = await getOneKeySdk(); + + const result = await Promise.race([ + sdk.searchDevices() as Promise>, + new Promise((_, reject) => + setTimeout(() => reject(new Error('BLE scan timed out')), SCAN_TIMEOUT_MS) + ), + ]); + + if (!result?.success) throw new Error(getErrorMessage(result)); + return result.payload || []; +}; + +/** + * Resolve device_id and master fingerprint from a connected device. + * Returns both so callers can verify device identity. + */ +export const getOneKeyDeviceInfo = async (connectId: string): Promise => { + const sdk = await getOneKeySdk(); + const result = (await sdk.getFeatures(connectId)) as SDKResult; + if (!result?.success) throw new Error(getErrorMessage(result)); + + const deviceId = result?.payload?.device_id; + if (!deviceId) throw new Error('Failed to get OneKey device_id'); + + const serialNo = + (result.payload as any)?.onekey_serial_no || + (result.payload as any)?.onekey_serial || + (result.payload as any)?.serial_no || + ''; + const deviceLabel = + result?.payload?.label || result?.payload?.ble_name || `OneKey ${deviceId.slice(-4)}`; + + // Fetch root fingerprint via a lightweight key derivation + const fpResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: "m/84'/0'/0'", + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + + if (!fpResult?.success) throw new Error(getErrorMessage(fpResult)); + + const mfp = toMasterFingerprint(fpResult?.payload?.root_fingerprint); + return { connectId, deviceId, masterFingerprint: mfp, deviceLabel, serialNo }; +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +const getCoinTypeByNetwork = (networkType: NetworkType) => + networkType === NetworkType.TESTNET ? 1 : 0; + +const getCoinNameByNetwork = (networkType: NetworkType) => + networkType === NetworkType.TESTNET ? 'TEST' : 'Bitcoin'; + +const toMasterFingerprint = (rootFingerprint?: number): string => { + if (rootFingerprint === undefined || rootFingerprint === null) { + throw new Error('Missing OneKey root_fingerprint'); + } + const fp = rootFingerprint >>> 0; // Force unsigned 32-bit + return fp.toString(16).padStart(8, '0').toUpperCase(); +}; + +export const normalizeOneKeyFingerprint = (fingerprint?: string | null): string => + (fingerprint || '').toUpperCase(); + +export const assertOneKeyFingerprint = ( + deviceInfo: Pick, + signer?: { masterFingerprint?: string } +) => { + const expectedFingerprint = normalizeOneKeyFingerprint(signer?.masterFingerprint); + const actualFingerprint = normalizeOneKeyFingerprint(deviceInfo?.masterFingerprint); + + if (!expectedFingerprint || !actualFingerprint) { + throw new Error('Missing OneKey fingerprint. Please re-add this device.'); + } + if (expectedFingerprint !== actualFingerprint) { + throw new Error('Fingerprint mismatch. Wrong OneKey device connected.'); + } +}; + +const extractXpub = (payload: any): string => { + if (!payload?.xpub) throw new Error('Invalid xpub from OneKey'); + return payload.xpub; +}; + +const getHDPathArray = (path: string): number[] => + path + .split('/') + .filter((part) => part && part !== 'm') + .map((part) => { + const hardened = part.slice(part.length - 1) === "'"; + const index = Number(part.replace("'", '')); + if (isNaN(index) || index < 0) throw new Error(`Invalid derivation path: ${path}`); + return hardened ? (index | HD_HARDENED) >>> 0 : index; + }); + +const toHex = (value: Buffer | Uint8Array): string => Buffer.from(value).toString('hex'); + +const buildMultisigRedeemScript = ({ + m, + xpubs, + addressIndex, + networkType, + isInternal = false, +}: OneKeyMultisigAddressConfig & { networkType: NetworkType }): MultisigRedeemScriptType => { + const network = WalletUtilities.getNetworkByType(networkType); + const toHDNode = (xpub: string): HDNodeType => { + const node = bip32.fromBase58(xpub, network); + return { + depth: node.depth, + fingerprint: node.parentFingerprint, + child_num: node.index, + chain_code: toHex(node.chainCode), + public_key: toHex(node.publicKey), + }; + }; + + return { + pubkeys: xpubs.map((xpub) => ({ + node: toHDNode(xpub), + address_n: [isInternal ? 1 : 0, addressIndex], + })), + signatures: xpubs.map(() => ''), + m, + }; +}; + +const getMultisigAddressScriptType = (path: string): InputScriptType => { + const segments = getHDPathArray(path); + const bip48ScriptType = (segments[3] ?? 0) & ~HD_HARDENED; + + if (bip48ScriptType === 2) return 'SPENDWITNESS'; + if (bip48ScriptType === 1) return 'SPENDP2SHWITNESS'; + + return 'SPENDMULTISIG'; +}; + +// ─── Signer data (xpub fetch) ───────────────────────────────────────────────── + +export const fetchOneKeySignerData = async ({ + connectId, + deviceId, + networkType, + accountNumber = 0, +}: { + connectId: string; + deviceId: string; + networkType: NetworkType; + accountNumber?: number; +}): Promise => { + const sdk = await getOneKeySdk(); + const coinType = getCoinTypeByNetwork(networkType); + + const singleSigPath = `m/84'/${coinType}'/${accountNumber}'`; + const multiSigPath = `m/48'/${coinType}'/${accountNumber}'/2'`; + const taprootPath = `m/86'/${coinType}'/${accountNumber}'`; + + const singleSigResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: singleSigPath, + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + if (!singleSigResult?.success) throw new Error(getErrorMessage(singleSigResult)); + + const multiSigResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: multiSigPath, + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + if (!multiSigResult?.success) throw new Error(getErrorMessage(multiSigResult)); + + const taprootResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: taprootPath, + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + if (!taprootResult?.success) throw new Error(getErrorMessage(taprootResult)); + + const mfp = toMasterFingerprint( + singleSigResult?.payload?.root_fingerprint ?? + multiSigResult?.payload?.root_fingerprint ?? + taprootResult?.payload?.root_fingerprint + ); + + return { + multiSigPath, + multiSigXpub: extractXpub(multiSigResult.payload), + singleSigPath, + singleSigXpub: extractXpub(singleSigResult.payload), + taprootPath, + taprootXpub: extractXpub(taprootResult.payload), + mfp, + }; +}; + +// ─── PSBT signing ───────────────────────────────────────────────────────────── + +const convertSignedPsbtToBase64 = (psbt: string): string => { + const sanitized = psbt?.startsWith('0x') ? psbt.slice(2) : psbt; + if (sanitized && /^[a-fA-F0-9]+$/.test(sanitized)) { + return Buffer.from(sanitized, 'hex').toString('base64'); + } + return psbt; +}; + +export const signPsbtWithOneKey = async ({ + connectId, + deviceId, + networkType, + serializedPSBT, +}: { + connectId: string; + deviceId: string; + networkType: NetworkType; + serializedPSBT: string; +}): Promise => { + const sdk = await getOneKeySdk(); + const psbtHex = Buffer.from(serializedPSBT, 'base64').toString('hex'); + const coin = getCoinNameByNetwork(networkType); + + const result = (await sdk.btcSignPsbt(connectId, deviceId, { + psbt: psbtHex, + coin, + useEmptyPassphrase: true, + })) as SDKResult<{ psbt: string }>; + + if (!result?.success) throw new Error(getErrorMessage(result)); + + const signedPsbt = result?.payload?.psbt; + if (!signedPsbt) throw new Error('OneKey returned empty signed PSBT'); + + return convertSignedPsbtToBase64(signedPsbt); +}; + +// ─── Address verification ───────────────────────────────────────────────────── + +export const verifyAddressOnOneKey = async ({ + connectId, + deviceId, + path, + networkType, + multisigConfig, +}: { + connectId: string; + deviceId: string; + path: string; + networkType: NetworkType; + multisigConfig?: OneKeyMultisigAddressConfig; +}): Promise => { + const sdk = await getOneKeySdk(); + const coin = getCoinNameByNetwork(networkType); + const multisig = multisigConfig + ? buildMultisigRedeemScript({ ...multisigConfig, networkType }) + : undefined; + + const result = (await sdk.btcGetAddress(connectId, deviceId, { + path: multisig ? getHDPathArray(path) : path, + coin, + showOnOneKey: true, + multisig, + scriptType: multisig ? getMultisigAddressScriptType(path) : undefined, + useEmptyPassphrase: true, + })) as SDKResult<{ address: string }>; + + if (!result?.success) throw new Error(getErrorMessage(result)); + if (!result?.payload?.address) throw new Error('OneKey returned empty address'); + + return result.payload.address; +}; + +// ─── Message signing ────────────────────────────────────────────────────────── + +export const signMessageWithOneKey = async ({ + connectId, + deviceId, + path, + message, + networkType, +}: { + connectId: string; + deviceId: string; + path: string; + message: string; + networkType: NetworkType; +}): Promise<{ address: string; signature: string }> => { + const sdk = await getOneKeySdk(); + const coin = getCoinNameByNetwork(networkType); + const messageHex = Buffer.from(message, 'utf8').toString('hex'); + + const result = (await sdk.btcSignMessage(connectId, deviceId, { + path, + messageHex, + coin, + useEmptyPassphrase: true, + })) as SDKResult<{ address: string; signature: string }>; + + if (!result?.success) throw new Error(getErrorMessage(result)); + if (!result?.payload?.signature) throw new Error('OneKey returned empty signature'); + + return { + address: result.payload.address, + signature: result.payload.signature, + }; +}; diff --git a/src/services/wallets/enums/index.ts b/src/services/wallets/enums/index.ts index ba2d2ae832..e79ab8e63f 100644 --- a/src/services/wallets/enums/index.ts +++ b/src/services/wallets/enums/index.ts @@ -95,6 +95,7 @@ export enum SignerType { MY_KEEPER = 'MY_KEEPER', TREZOR = 'TREZOR', LEDGER = 'LEDGER', + ONEKEY = 'ONEKEY', COLDCARD = 'COLDCARD', PASSPORT = 'PASSPORT', JADE = 'JADE', diff --git a/src/services/wallets/interfaces/vault.ts b/src/services/wallets/interfaces/vault.ts index 85f2a136b8..19eeda073c 100644 --- a/src/services/wallets/interfaces/vault.ts +++ b/src/services/wallets/interfaces/vault.ts @@ -84,6 +84,7 @@ export type SignerExtraData = { familyName?: string; recordID?: string; thumbnailPath?: string; + bleConnectId?: string; // OneKey BLE connectId for direct reconnection }; export interface HealthCheckDetails { diff --git a/yarn.lock b/yarn.lock index 14fa3730db..cd32dcf9d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2753,9 +2753,9 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@^1.2.0": +"@noble/hashes@^1.2.0", "@noble/hashes@^1.3.3": version "1.8.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== "@noble/secp256k1@1.6.3": @@ -2789,6 +2789,58 @@ resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@onekeyfe/hd-ble-sdk@1.1.16": + version "1.1.16" + resolved "https://registry.npmjs.org/@onekeyfe/hd-ble-sdk/-/hd-ble-sdk-1.1.16.tgz#124c5cb9bb3c0d16b6ca2ea615e1867cbf55ba18" + integrity sha512-8t5mhPS7QQkcQXAqJJ0VVr+7EX5vT327U32cUYwxdf5N/opvNRkNc3k1io8N8YO6XprGk5+vAqWLT95/C30/Bw== + dependencies: + "@onekeyfe/hd-core" "1.1.16" + "@onekeyfe/hd-shared" "1.1.16" + "@onekeyfe/hd-transport-react-native" "1.1.16" + +"@onekeyfe/hd-core@1.1.16": + version "1.1.16" + resolved "https://registry.npmjs.org/@onekeyfe/hd-core/-/hd-core-1.1.16.tgz#31df132b7cc7e31275b39fdd0a08f001a7afa9a1" + integrity sha512-ViTzwUySTXWB81kVHvZuxaHScfcQUZ1+Tgk9We7UXz8zKwLRsU5GfIbqpS1qSZA65/OCCB/fJoMA0zto84FH0Q== + dependencies: + "@onekeyfe/hd-shared" "1.1.16" + "@onekeyfe/hd-transport" "1.1.16" + axios "1.12.2" + bignumber.js "^9.0.2" + bytebuffer "^5.0.1" + jszip "^3.10.1" + parse-uri "^1.0.7" + semver "^7.3.7" + +"@onekeyfe/hd-shared@1.1.16": + version "1.1.16" + resolved "https://registry.npmjs.org/@onekeyfe/hd-shared/-/hd-shared-1.1.16.tgz#7fa2b8f1e52348c924a8fc0be18f7d2f0f1b9043" + integrity sha512-PLIK9yZyT8TpbfN+4EaBKzY4mFXSVNBvOLNKmIxH/GnIy5hDBdGyybbhtCTEXjGr55aFLwR0GBy1I+nYXQmoEw== + +"@onekeyfe/hd-transport-react-native@1.1.16": + version "1.1.16" + resolved "https://registry.npmjs.org/@onekeyfe/hd-transport-react-native/-/hd-transport-react-native-1.1.16.tgz#381e7117e08767bc28c28ab23218a32540a7ea66" + integrity sha512-CDlmOWYKgJ2pxelEC4w8dSG44Iz6bSJ1LY65lbFdutZ4ZO84GtUmceZ//K+i88dbULKe4qogcyqKG7P6EZcTqA== + dependencies: + "@onekeyfe/hd-shared" "1.1.16" + "@onekeyfe/hd-transport" "1.1.16" + "@onekeyfe/react-native-ble-utils" "^0.1.4" + react-native-ble-plx "3.5.0" + +"@onekeyfe/hd-transport@1.1.16": + version "1.1.16" + resolved "https://registry.npmjs.org/@onekeyfe/hd-transport/-/hd-transport-1.1.16.tgz#3ec4ee1f786df1a630e79113dc561b33c5d57bd9" + integrity sha512-z+HKqYkGz+Ub1GV+REFqJqTG+tpZt7296O3qIlwmsURWdWXX4TcpXmCQDcXMt7vru8m9A4MUMkpGr+23MFBluw== + dependencies: + bytebuffer "^5.0.1" + long "^4.0.0" + protobufjs "^6.11.2" + +"@onekeyfe/react-native-ble-utils@^0.1.4": + version "0.1.4" + resolved "https://registry.npmjs.org/@onekeyfe/react-native-ble-utils/-/react-native-ble-utils-0.1.4.tgz#4a262d30cd226e94f1c697c08a3a85a44310690c" + integrity sha512-DTKjjFKdkGktx/qqdu7STFAjpD6ZI3Rdfcyvv/a1Ho4GBs9F/nOJyxEhqiUNbY4XZoVHSwYy2cxYrdX4zGY6eA== + "@otplib/core@^12.0.1": version "12.0.1" resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d" @@ -4354,6 +4406,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f" integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "25.6.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" @@ -5183,6 +5240,15 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.3.tgz#d23cf404edaa5f97bdfd9afed6eea8405e5326e7" integrity sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg== +axios@1.12.2: + version "1.12.2" + resolved "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7" + integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" + axios@1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979" @@ -5376,7 +5442,7 @@ base-x@^1.1.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac" integrity sha512-c0WLeG3K5OlL4Skz2/LVdS+MjggByKhowxQpG+JpCLA48s/bGwIDyzA1naFjywtNvp/37fLK0p0FpjTNNLLUXQ== -base-x@^3.0.2: +base-x@^3.0.2, base-x@^3.0.9: version "3.0.11" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== @@ -5437,7 +5503,7 @@ bignumber.js@9.1.2: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== -bignumber.js@^9.0.1: +bignumber.js@^9.0.1, bignumber.js@^9.0.2: version "9.3.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== @@ -5539,7 +5605,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.3.tgz#2cc2c679188eb35b006f2d0d4710bed8437a769e" integrity sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g== -bn.js@^5.2.1, bn.js@^5.2.2: +bn.js@^5.1.1, bn.js@^5.2.1, bn.js@^5.2.2: version "5.2.3" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== @@ -5585,7 +5651,7 @@ braces@^3.0.2, braces@^3.0.3: dependencies: fill-range "^7.1.1" -brorand@^1.0.1, brorand@^1.1.0: +brorand@^1.0.1, brorand@^1.0.5, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== @@ -5788,6 +5854,13 @@ buffer@^5.1.0, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bytebuffer@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" + integrity sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ== + dependencies: + long "~3" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -7709,9 +7782,9 @@ for-each@^0.3.3, for-each@^0.3.5: dependencies: is-callable "^1.2.7" -form-data@^4.0.0: +form-data@^4.0.0, form-data@^4.0.4: version "4.0.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== dependencies: asynckit "^0.4.0" @@ -8213,6 +8286,11 @@ image-size@^1.0.2: dependencies: queue "6.0.2" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immer@^9.0.7: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -9187,6 +9265,16 @@ jsonfile@^4.0.0: object.assign "^4.1.4" object.values "^1.1.6" +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -9240,6 +9328,13 @@ levn@^0.4.1: version "0.3.0" resolved "git+https://github.com/bithyve/libportal-react-native.git#2ff681b0b725009768acadb35b2731fb1a402fa3" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lighthouse-logger@^1.0.0: version "1.4.2" resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa" @@ -9413,11 +9508,21 @@ logkitty@^0.7.1: dayjs "^1.8.15" yargs "^15.1.0" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + long@^5.0.0: version "5.3.2" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== +long@~3: + version "3.2.0" + resolved "https://registry.npmjs.org/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + integrity sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg== + loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -10494,6 +10599,11 @@ pako@2.1.0: resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -10522,6 +10632,11 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-uri@^1.0.7: + version "1.0.16" + resolved "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.16.tgz#9e730ccc7358d080f90a29e0334c80c8c845e544" + integrity sha512-WMX9ygt2zzbtd3UlChi8S2Uj/dZa0N9QaotTkyRD7v06c50dor4qEWrM5ZvHiiaZYpXal4otRS9hynwwX0DVoA== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -10813,6 +10928,25 @@ prop-types@*, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.0, prop-t object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@^6.11.2: + version "6.11.6" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.6.tgz#f02b04ef842469a2bf89da18be8fd5c41dc820ca" + integrity sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + protobufjs@^7.2.5: version "7.5.5" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.5.tgz#b7089ca4410374c75150baf277353ef76db69f96" @@ -11042,6 +11176,11 @@ react-native-biometrics@2.2.0: resolved "https://registry.yarnpkg.com/react-native-biometrics/-/react-native-biometrics-2.2.0.tgz#6fc3e801b83389ae1f6c9c41f7b37b50e7014c4e" integrity sha512-V2O0s2ic7PxVP76CguCfBXvVEyazbjtWv7r20T7D+ZQqBB1XSZ1WzK/Gnr12CVRxHLUZ3/tKHOsz7mzIaXyNoA== +react-native-ble-plx@3.5.0: + version "3.5.0" + resolved "https://registry.npmjs.org/react-native-ble-plx/-/react-native-ble-plx-3.5.0.tgz#6cfa33c007bf5cc8b573dfcca8915de57cec60be" + integrity sha512-PeSnRswHLwLRVMQkOfDaRICtrGmo94WGKhlSC09XmHlqX2EuYgH+vNJpGcLkd8lyiYpEJyf8wlFAdj9Akliwmw== + react-native-blob-util@0.24.7: version "0.24.7" resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.24.7.tgz#889e3626e3b20f9b10bb265ea91d9ae44ce65ecf" @@ -11481,9 +11620,9 @@ readable-stream@^1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.8: +readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" @@ -11790,6 +11929,25 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: hash-base "^3.1.2" inherits "^2.0.4" +ripple-address-codec@^4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.1.tgz#68fbaf646bb8567f70743af7f1ce4479f73efbf6" + integrity sha512-Qa3+9wKVvpL/xYtT6+wANsn0A1QcC5CT6IMZbRJZ/1lGt7gmwIfsrCuz1X0+LCEO7zgb+3UT1I1dc0k/5dwKQQ== + dependencies: + base-x "^3.0.9" + create-hash "^1.1.2" + +ripple-keypairs@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.1.tgz#7fa531df36b138134afb53555a87d7f5eb465b2e" + integrity sha512-dmPlraWKJciFJxHcoubDahGnoIalG5e/BtV6HNDUs7wLXmtnLMHt6w4ed9R8MTL2zNrVPiIdI/HCtMMo0Tm7JQ== + dependencies: + bn.js "^5.1.1" + brorand "^1.0.5" + elliptic "^6.5.4" + hash.js "^1.0.3" + ripple-address-codec "^4.3.1" + "rn-nodeify@github:tradle/rn-nodeify#338d8d6ba8438403093e9409e9a9d88ad884926f": version "10.3.0" resolved "https://codeload.github.com/tradle/rn-nodeify/tar.gz/338d8d6ba8438403093e9409e9a9d88ad884926f" @@ -12024,6 +12182,11 @@ set-proto@^1.0.0: es-errors "^1.3.0" es-object-atoms "^1.0.0" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"