From 2df1b46975fd71f531ec4301443c448ecc9a53f3 Mon Sep 17 00:00:00 2001 From: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com> Date: Fri, 9 Jun 2023 18:25:15 +0800 Subject: [PATCH] [iOS] Perform preflight check for local network permission (#16150) Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com> --- .../ios/RCTStatus/RCTStatus.m | 8 ++ src/native_module/core.cljs | 7 + src/status_im/signals/core.cljs | 28 ++-- src/status_im2/contexts/syncing/events.cljs | 26 +++- .../contexts/syncing/scan_sync_code/view.cljs | 123 ++++++++++++------ translations/en.json | 3 + 6 files changed, 138 insertions(+), 57 deletions(-) diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index be22920d7b..c28ec0b262 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -304,6 +304,14 @@ RCT_EXPORT_METHOD(hashMessage:(NSString *)message callback(@[result]); } +RCT_EXPORT_METHOD(localPairingPreflightOutboundCheck:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"LocalPairingPreflightOutboundCheck() method called"); +#endif + NSString *result = StatusgoLocalPairingPreflightOutboundCheck(); + callback(@[result]); +} + RCT_EXPORT_METHOD(getConnectionStringForBootstrappingAnotherDevice:(NSString *)configJSON callback:(RCTResponseSenderBlock)callback) { diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs index 26ff69457a..c0e1c75964 100644 --- a/src/native_module/core.cljs +++ b/src/native_module/core.cljs @@ -248,6 +248,13 @@ (log/debug "[native-module] hash-message") (.hashMessage ^js (status) message callback)) +(defn local-pairing-preflight-outbound-check + "Checks whether the device has allows connecting to the local server" + [callback] + (log/info "[native-module] Performing local pairing preflight check") + (when platform/ios? + (.localPairingPreflightOutboundCheck ^js (status) callback))) + (defn get-connection-string-for-bootstrapping-another-device "Generates connection string form status-go for the purpose of local pairing on the sender end" [config-json callback] diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs index 8412290930..2c012b0ee3 100644 --- a/src/status_im/signals/core.cljs +++ b/src/status_im/signals/core.cljs @@ -10,7 +10,8 @@ [utils.re-frame :as rf] [status-im2.contexts.chat.messages.link-preview.events :as link-preview] [taoensso.timbre :as log] - [status-im2.constants :as constants])) + [status-im2.constants :as constants] + [quo2.foundations.colors :as colors])) (rf/defn status-node-started [{db :db :as cofx} {:keys [error]}] @@ -55,7 +56,7 @@ :peers-count (count (:peers peer-stats)))})) (rf/defn handle-local-pairing-signals - [{:keys [db] :as cofx} {:keys [type action data] :as event}] + [{:keys [db] :as cofx} {:keys [type action data error] :as event}] (log/info "local pairing signal received" {:event event}) (let [{:keys [account password]} data @@ -78,7 +79,7 @@ (and (some? account) (some? password))) multiaccount-data (when received-account? (merge account {:password password})) - navigate-to-syncing-devices? (and connection-success? receiver?) + navigate-to-syncing-devices? (and (or connection-success? error-on-pairing?) receiver?) user-in-syncing-devices-screen? (= (:view-id db) :syncing-progress)] (merge {:db (cond-> db connection-success? @@ -92,12 +93,21 @@ completed-pairing? (assoc-in [:syncing :pairing-status] :completed))} - (when (and navigate-to-syncing-devices? (not user-in-syncing-devices-screen?)) - {:dispatch [:navigate-to :syncing-progress]}) - (when (and completed-pairing? sender?) - {:dispatch [:syncing/clear-states]}) - (when (and completed-pairing? receiver?) - {:dispatch [:multiaccounts.login/local-paired-user]})))) + (cond + (and navigate-to-syncing-devices? (not user-in-syncing-devices-screen?)) + {:dispatch [:navigate-to :syncing-progress]} + + (and completed-pairing? sender?) + {:dispatch [:syncing/clear-states]} + + (and completed-pairing? receiver?) + {:dispatch [:multiaccounts.login/local-paired-user]} + + (and error-on-pairing? (some? error)) + {:dispatch [:toasts/upsert + {:icon :i/alert + :icon-color colors/danger-50 + :text error}]})))) (rf/defn process {:events [:signals/signal-received]} diff --git a/src/status_im2/contexts/syncing/events.cljs b/src/status_im2/contexts/syncing/events.cljs index 7ff4a87850..85f599b8ae 100644 --- a/src/status_im2/contexts/syncing/events.cljs +++ b/src/status_im2/contexts/syncing/events.cljs @@ -1,14 +1,15 @@ (ns status-im2.contexts.syncing.events (:require [native-module.core :as native-module] + [re-frame.core :as re-frame] + [status-im.data-store.settings :as data-store.settings] + [status-im.node.core :as node] + [status-im.utils.platform :as utils.platform] + [status-im2.config :as config] + [status-im2.constants :as constants] [taoensso.timbre :as log] [utils.re-frame :as rf] [utils.security.core :as security] - [status-im2.config :as config] - [status-im.node.core :as node] - [re-frame.core :as re-frame] - [status-im.data-store.settings :as data-store.settings] - [status-im.utils.platform :as utils.platform] - [status-im2.constants :as constants])) + [utils.transforms :as transforms])) (rf/defn local-pairing-update-role {:events [:syncing/update-role]} @@ -31,6 +32,19 @@ :custom-bootnodes-enabled? false}}] (node/get-multiaccount-node-config db))) +(rf/defn preflight-outbound-check-for-local-pairing + {:events [:syncing/preflight-outbound-check]} + [_ set-checks-passed] + (let [callback + (fn [raw-response] + (log/info "Local pairing preflight check" + {:response raw-response + :event :syncing/preflight-outbound-check}) + (let [^js response-js (transforms/json->js raw-response) + error (transforms/js->clj (.-error response-js))] + (set-checks-passed (empty? error))))] + (native-module/local-pairing-preflight-outbound-check callback))) + (rf/defn initiate-local-pairing-with-connection-string {:events [:syncing/input-connection-string-for-bootstrapping] :interceptors [(re-frame/inject-cofx :random-guid-generator)]} diff --git a/src/status_im2/contexts/syncing/scan_sync_code/view.cljs b/src/status_im2/contexts/syncing/scan_sync_code/view.cljs index 4a4102cde6..321773c6c3 100644 --- a/src/status_im2/contexts/syncing/scan_sync_code/view.cljs +++ b/src/status_im2/contexts/syncing/scan_sync_code/view.cljs @@ -13,10 +13,35 @@ [status-im2.contexts.syncing.scan-sync-code.style :as style] [utils.i18n :as i18n] [utils.re-frame :as rf] - [status-im2.contexts.syncing.utils :as sync-utils])) + [status-im2.contexts.syncing.utils :as sync-utils] + [status-im.utils.platform :as platform])) + +;; Android allow local network access by default. So, we need this check on iOS only. +(defonce preflight-check-passed? (reagent/atom (if platform/ios? false true))) (defonce camera-permission-granted? (reagent/atom false)) +(defn request-camera-permission + [] + (rf/dispatch + [:request-permissions + {:permissions [:camera] + :on-allowed #(reset! camera-permission-granted? true) + :on-denied #(rf/dispatch + [:toasts/upsert + {:icon :i/info + :icon-color colors/danger-50 + :override-theme :light + :text (i18n/label :t/camera-permission-denied)}])}])) + +(defn perform-preflight-check + "Performing the check for the first time + will trigger local network access permission in iOS. + This permission is required for local pairing + https://github.com/status-im/status-mobile/issues/16135" + [] + (rf/dispatch [:syncing/preflight-outbound-check #(reset! preflight-check-passed? %)])) + (defn- header [active-tab read-qr-once? title] [:<> @@ -59,27 +84,49 @@ (reset! active-tab id) (reset! read-qr-once? false))}]]]) -(defn- camera-permission-view - [request-camera-permission] - [rn/view {:style style/camera-permission-container} - [quo/text - {:size :paragraph-1 - :weight :medium - :style style/enable-camera-access-header} - (i18n/label :t/enable-access-to-camera)] - [quo/text - {:size :paragraph-2 - :weight :regular - :style style/enable-camera-access-sub-text} - (i18n/label :t/to-scan-a-qr-enable-your-camera)] - [quo/button - {:before :i/camera - :type :primary - :size 32 - :accessibility-label :request-camera-permission - :override-theme :dark - :on-press request-camera-permission} - (i18n/label :t/enable-camera)]]) +(defn get-labels-and-on-press-method + [] + (if @camera-permission-granted? + {:title-label-key :t/enable-access-to-local-network + :description-label-key :t/to-pair-with-your-other-device-in-the-network + :button-icon :i/world + :button-label :t/enable-network-access + :accessibility-label :perform-preflight-check + :on-press perform-preflight-check} + {:title-label-key :t/enable-access-to-camera + :description-label-key :t/to-scan-a-qr-enable-your-camera + :button-icon :i/camera + :button-label :t/enable-camera + :accessibility-label :request-camera-permission + :on-press request-camera-permission})) + +(defn- camera-and-local-network-access-permission-view + [] + (let [{:keys [title-label-key + description-label-key + button-icon + button-label + accessibility-label + on-press]} (get-labels-and-on-press-method)] + [rn/view {:style style/camera-permission-container} + [quo/text + {:size :paragraph-1 + :weight :medium + :style style/enable-camera-access-header} + (i18n/label title-label-key)] + [quo/text + {:size :paragraph-2 + :weight :regular + :style style/enable-camera-access-sub-text} + (i18n/label description-label-key)] + [quo/button + {:before button-icon + :type :primary + :size 32 + :accessibility-label accessibility-label + :override-theme :dark + :on-press on-press} + (i18n/label button-label)]])) (defn- qr-scan-hole-area [qr-view-finder] @@ -117,14 +164,16 @@ (i18n/label :t/ensure-qr-code-is-in-focus-to-scan)]]])) (defn- scan-qr-code-tab - [qr-view-finder request-camera-permission] + [qr-view-finder] [:<> [rn/view {:style style/scan-qr-code-container}] (when (empty? @qr-view-finder) [qr-scan-hole-area qr-view-finder]) - (if (and @camera-permission-granted? (boolean (not-empty @qr-view-finder))) + (if (and @preflight-check-passed? + @camera-permission-granted? + (boolean (not-empty @qr-view-finder))) [viewfinder @qr-view-finder] - [camera-permission-view request-camera-permission])]) + [camera-and-local-network-access-permission-view])]) (defn- enter-sync-code-tab [] @@ -185,21 +234,9 @@ (defn f-view [{:keys [title show-bottom-view? background]}] - (let [insets (safe-area/get-insets) - active-tab (reagent/atom 1) - qr-view-finder (reagent/atom {}) - request-camera-permission (fn [] - (rf/dispatch - [:request-permissions - {:permissions [:camera] - :on-allowed #(reset! camera-permission-granted? true) - :on-denied #(rf/dispatch - [:toasts/upsert - {:icon :i/info - :icon-color colors/danger-50 - :override-theme :light - :text (i18n/label - :t/camera-permission-denied)}])}]))] + (let [insets (safe-area/get-insets) + active-tab (reagent/atom 1) + qr-view-finder (reagent/atom {})] (fn [] (let [camera-ref (atom nil) read-qr-once? (atom false) @@ -215,7 +252,9 @@ 3000) (check-qr-code-data data))) scan-qr-code-tab? (= @active-tab 1) - show-camera? (and scan-qr-code-tab? @camera-permission-granted?) + show-camera? (and scan-qr-code-tab? + @camera-permission-granted? + @preflight-check-passed?) show-holes? (and show-camera? (boolean (not-empty @qr-view-finder)))] (rn/use-effect @@ -230,7 +269,7 @@ [rn/view {:style (style/root-container (:top insets))} [header active-tab read-qr-once? title] (case @active-tab - 1 [scan-qr-code-tab qr-view-finder request-camera-permission] + 1 [scan-qr-code-tab qr-view-finder] 2 [enter-sync-code-tab] nil) [rn/view {:style style/flex-spacer}] diff --git a/translations/en.json b/translations/en.json index f6efa1a80e..2081fa3eed 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2092,6 +2092,9 @@ "synchronise-your-data-across-your-devices": "Synchronise your data across your devices", "scan-sync-qr-code": "Scan QR code", "enter-sync-code": "Enter sync code", + "enable-access-to-local-network": "Enable access to local network", + "to-pair-with-your-other-device-in-the-network": "To pair with your other device in the network", + "enable-network-access": "Enable network access", "enable-access-to-camera": "Enable access to camera", "link-preview-loading-message": "Generating preview", "to-scan-a-qr-enable-your-camera": "To scan a QR, enable your camera",