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"