[#19946] feat: add key pair QR code view (#20073)

This commit is contained in:
Mohsen 2024-05-20 19:35:51 +03:00 committed by GitHub
parent 4cdbfb6cc6
commit 389a730eff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 281 additions and 9 deletions

View File

@ -75,4 +75,17 @@ class NetworkManager(private val reactContext: ReactApplicationContext) : ReactC
utils.executeRunnableStatusGoMethod({ Statusgo.recover(rpcParams) }, callback)
}
@ReactMethod
fun getConnectionStringForExportingKeypairsKeystores(configJSON: String, callback: Callback) {
val jsonConfig = JSONObject(configJSON)
val senderConfig = jsonConfig.getJSONObject("senderConfig")
val keyUID = senderConfig.getString("loggedInKeyUid")
val keyStorePath = utils.getKeyStorePath(keyUID)
senderConfig.put("keystorePath", keyStorePath)
utils.executeRunnableStatusGoMethod(
{ Statusgo.getConnectionStringForExportingKeypairsKeystores(jsonConfig.toString()) },
callback)
}
}

View File

@ -106,4 +106,22 @@ RCT_EXPORT_METHOD(recover:(NSString *)message
callback(@[result]);
}
RCT_EXPORT_METHOD(getConnectionStringForExportingKeypairsKeystores:(NSString *)configJSON
callback:(RCTResponseSenderBlock)callback) {
NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSMutableDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:&error];
NSMutableDictionary *senderConfig = configDict[@"senderConfig"];
NSString *keyUID = senderConfig[@"loggedInKeyUid"];
NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID];
NSString *keystoreDir = multiaccountKeystoreDir.path;
[senderConfig setValue:keystoreDir forKey:@"keystorePath"];
NSString *modifiedConfigJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:configDict];
NSString *result = StatusgoGetConnectionStringForExportingKeypairsKeystores(modifiedConfigJSON);
callback(@[result]);
}
@end

View File

@ -601,3 +601,11 @@
(.createAccountFromMnemonicAndDeriveAccountsForPaths ^js (account-manager)
(types/clj->json mnemonic)
#(callback (types/json->clj %))))
(defn get-connection-string-for-exporting-keypairs-keystores
"Generates connection string form status-go for the purpose of exporting keypairs and keystores on sender side"
[config-json callback]
(log/info "[native-module] Fetching Export Keypairs Connection String"
{:fn :get-connection-string-for-exporting-keypairs-keystores
:config-json config-json})
(.getConnectionStringForExportingKeypairsKeystores ^js (network) config-json callback))

View File

@ -1,8 +1,12 @@
(ns status-im.contexts.settings.wallet.events
(:require
[native-module.core :as native-module]
[status-im.contexts.syncing.utils :as sync-utils]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
[utils.re-frame :as rf]
[utils.security.core :as security]
[utils.transforms :as transforms]))
(rf/reg-event-fx
:wallet/rename-keypair-success
@ -30,3 +34,22 @@
:on-error #(log/info "failed to rename keypair " %)}]]]})
(rf/reg-event-fx :wallet/rename-keypair rename-keypair)
(defn get-key-pair-export-connection
[{:keys [db]} [{:keys [sha3-pwd keypair-key-uid callback]}]]
(let [key-uid (get-in db [:profile/profile :key-uid])
config-map (transforms/clj->json {:senderConfig {:loggedInKeyUid key-uid
:keystorePath ""
:keypairsToExport [keypair-key-uid]
:password (security/safe-unmask-data
sha3-pwd)}
:serverConfig {:timeout 0}})
handle-connection (fn [response]
(when (sync-utils/valid-connection-string? response)
(callback response)
(rf/dispatch [:hide-bottom-sheet])))]
(native-module/get-connection-string-for-exporting-keypairs-keystores
config-map
handle-connection)))
(rf/reg-event-fx :wallet/get-key-pair-export-connection get-key-pair-export-connection)

View File

@ -7,13 +7,23 @@
[data]
(rf/dispatch [:open-modal :screen/settings.rename-keypair data]))
(defn on-show-qr
[data]
(rf/dispatch [:open-modal :screen/settings.encrypted-key-pair-qr data]))
(defn view
[props data]
(let [has-paired-device (rf/sub [:pairing/has-paired-devices])]
[:<>
[quo/drawer-top props]
[quo/action-drawer
[(when (= (:type props) :keypair)
[(when has-paired-device
[{:icon :i/qr-code
:accessibility-label :show-key-pr-qr
:label (i18n/label :t/show-encrypted-qr-of-key-pairs)
:on-press #(on-show-qr data)}])
(when (= (:type props) :keypair)
[{:icon :i/edit
:accessibility-label :rename-key-pair
:label (i18n/label :t/rename-key-pair)
:on-press #(on-rename-request data)}])]]])
:on-press #(on-rename-request data)}])]]]))

View File

@ -0,0 +1,38 @@
(ns status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.countdown.view
(:require
[quo.core :as quo]
[quo.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.hooks :as hooks]
[utils.datetime :as datetime]
[utils.i18n :as i18n]))
(def code-valid-for-ms 120000)
(def one-min-ms 60000)
(defn current-ms
[]
(* 1000 (js/Math.ceil (/ (datetime/timestamp) 1000))))
(defn view
[on-clear]
(let [[valid-for-ms set-valid-for-ms] (rn/use-state code-valid-for-ms)
[timestamp set-timestamp] (rn/use-state current-ms)
clock (rn/use-callback (fn []
(let [remaining (- code-valid-for-ms
(- (current-ms)
timestamp))]
(when (pos? remaining)
(set-valid-for-ms remaining))
(when (zero? remaining)
(set-timestamp (current-ms))
(set-valid-for-ms code-valid-for-ms)
(on-clear))))
[code-valid-for-ms])]
(hooks/use-interval clock on-clear 1000)
[quo/text
{:size :paragraph-2
:style {:color (if (< valid-for-ms one-min-ms)
colors/danger-60
colors/white-opa-40)}}
(i18n/label :t/valid-for-time {:valid-for (datetime/ms-to-duration valid-for-ms)})]))

View File

@ -0,0 +1,46 @@
(ns status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.style
(:require
[quo.foundations.colors :as colors]
[react-native.safe-area :as safe-area]))
(defn container-main
[]
{:background-color colors/neutral-95
:padding-top (safe-area/get-top)
:flex 1})
(def page-container
{:margin-top 14
:margin-horizontal 20})
(def title-container
{:flex-direction :row
:align-items :center
:justify-content :space-between})
(def standard-auth
{:margin-top 12
:flex 1})
(def qr-container
{:margin-top 12
:background-color colors/white-opa-5
:border-radius 20
:flex 1
:padding 12})
(def sub-text-container
{:margin-bottom 8
:justify-content :space-between
:align-items :center
:flex-direction :row})
(def valid-cs-container
{:flex 1
:margin 12})
(def warning-text
{:margin-horizontal 16
:margin-top 20
:text-align :center
:color colors/white-opa-70})

View File

@ -0,0 +1,98 @@
(ns status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view
(:require
[quo.core :as quo]
[quo.foundations.colors :as colors]
[react-native.clipboard :as clipboard]
[react-native.core :as rn]
[status-im.common.qr-codes.view :as qr-codes]
[status-im.common.resources :as resources]
[status-im.common.standard-authentication.core :as standard-auth]
[status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.countdown.view :as countdown]
[status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.style :as style]
[status-im.contexts.syncing.utils :as sync-utils]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn navigate-back [] (rf/dispatch [:navigate-back]))
(defn view
[]
(let [{:keys [key-uid]} (rf/sub [:get-screen-params])
{:keys [customization-color]} (rf/sub [:profile/profile-with-image])
[code set-code] (rn/use-state nil)
valid-connection-string? (rn/use-memo #(sync-utils/valid-connection-string? code) [code])
validate-and-set-code (rn/use-callback (fn [connection-string]
(when (sync-utils/valid-connection-string?
connection-string)
(set-code connection-string))))
cleanup-clock (rn/use-callback #(set-code nil))
on-auth-success (rn/use-callback (fn [entered-password]
(rf/dispatch
[:wallet/get-key-pair-export-connection
{:sha3-pwd entered-password
:keypair-key-uid key-uid
:callback validate-and-set-code}]))
[key-uid])]
[rn/view {:style (style/container-main)}
[rn/scroll-view
[quo/page-nav
{:type :no-title
:icon-name :i/close
:background :blur
:on-press navigate-back}]
[rn/view {:style style/page-container}
[rn/view {:style style/title-container}
[quo/text
{:size :heading-1
:weight :semi-bold
:style {:color colors/white}}
(i18n/label :t/encrypted-key-pairs)]]
[rn/view {:style style/qr-container}
(if valid-connection-string?
[qr-codes/qr-code {:url code}]
[rn/view {:style {:flex-direction :row}}
[rn/image
{:source (resources/get-image :qr-code)
:style {:width "100%"
:background-color colors/white-opa-70
:border-radius 12
:aspect-ratio 1}}]])
(when valid-connection-string?
[rn/view {:style style/valid-cs-container}
[rn/view {:style style/sub-text-container}
[quo/text
{:size :paragraph-2
:style {:color colors/white-opa-40}}
(i18n/label :t/encrypted-key-pairs-code)]
[countdown/view cleanup-clock]]
[quo/input
{:default-value code
:type :password
:default-shown? true
:editable false}]
[quo/button
{:on-press (fn []
(clipboard/set-string code)
(rf/dispatch [:toasts/upsert
{:type :positive
:text (i18n/label
:t/sharing-copied-to-clipboard)}]))
:type :grey
:container-style {:margin-top 12}
:icon-left :i/copy}
(i18n/label :t/copy-qr)]])
(when-not valid-connection-string?
[rn/view {:style style/standard-auth}
[standard-auth/slide-button
{:blur? true
:size :size-40
:track-text (i18n/label :t/slide-to-reveal-qr-code)
:customization-color customization-color
:on-auth-success on-auth-success
:auth-button-label (i18n/label :t/reveal-qr-code)
:auth-button-icon-left :i/reveal}]])]]
(when-not valid-connection-string?
[quo/text
{:size :paragraph-2
:style style/warning-text}
(i18n/label :t/make-sure-no-camera-warning)])]]))

View File

@ -57,6 +57,8 @@
[status-im.contexts.profile.settings.screens.password.change-password.view :as change-password]
[status-im.contexts.profile.settings.screens.password.view :as settings-password]
[status-im.contexts.profile.settings.view :as settings]
[status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view :as
encrypted-key-pair-qr]
[status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename]
[status-im.contexts.settings.wallet.keypairs-and-accounts.view :as keypairs-and-accounts]
[status-im.contexts.settings.wallet.network-settings.view :as network-settings]
@ -509,6 +511,10 @@
:options (assoc options/dark-screen :sheet? true)
:component keypair-rename/view}
{:name :screen/settings.encrypted-key-pair-qr
:options options/transparent-screen-options
:component encrypted-key-pair-qr/view}
{:name :screen/settings.saved-addresses
:options options/transparent-modal-screen-options
:component saved-addresses-settings/view}

View File

@ -28,3 +28,9 @@
:<- [:syncing]
(fn [syncing]
(:pairing-status syncing)))
(re-frame/reg-sub
:pairing/has-paired-devices
:<- [:pairing/enabled-installations]
(fn [installations]
(> (count installations) 1)))

View File

@ -524,6 +524,8 @@
"enable": "Enable",
"enable-notifications-sub-title": "Receive notifications about your new messages or wallet transactions",
"encrypt-with-password": "Encrypt with password",
"encrypted-key-pairs": "Encrypted key pairs",
"encrypted-key-pairs-code": "Encrypted key pairs code",
"ending-not-allowed": "{{ending}} ending is not allowed",
"ends-with-space": "Cannot end with space",
"ens-10-SNT": "10 SNT",
@ -939,6 +941,7 @@
"main-wallet": "Main Wallet",
"make-admin": "Make admin",
"make-moderator": "Make moderator",
"make-sure-no-camera-warning": "Make sure no camera or person can see this screen before revealing",
"manage-keys-and-storage": "Manage keys and storage",
"mark-as-read": "Mark as read",
"mark-all-read": "Mark all read",
@ -1272,6 +1275,7 @@
"reset-card-description": "This operation will reset card to initial state. It will erase all card data including private keys. Operation is not reversible.",
"retry": "Retry",
"reveal-sync-code": "Reveal sync code",
"reveal-qr-code": "Reveal QR code",
"revoke-access": "Revoke access",
"save": "Save",
"save-address": "Save address",
@ -1337,6 +1341,7 @@
"show-more": "Show more",
"show-qr": "Show QR code",
"show-transaction-data": "Show transaction data",
"show-encrypted-qr-of-key-pairs": "Show encrypted QR of key pairs on device",
"sign-and-send": "Sign and send",
"sign-in": "Sign in",
"sign-message": "Sign Message",
@ -1970,6 +1975,7 @@
"slide-to-request-to-join": "Slide to request to join",
"slide-to-reveal-code": "Slide to reveal code",
"slide-to-create-account": "Slide to create account",
"slide-to-reveal-qr-code": "Slide to reveal QR code",
"minimum-received": "Minimum received",
"powered-by-paraswap": "Powered by Paraswap",
"priority": "Priority",