From 79bf4bb8d5e7b83b3c3d97602b70a9c4b9d17645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulises=20Manuel=20C=C3=A1rdenas?= <90291778+ulisesmac@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:53:05 -0600 Subject: [PATCH] [#16437] Fix sync QR code not recognized after trying again (#16746) * Add bind-component function to `react-native.navigation` * Create helper function to use react-native-navigation lifecycle methods * Fix sync QR code not recognized after trying again & refactor * Add atom to manage callbacks when qr code scan fails --- src/react_native/navigation.cljs | 5 + .../contexts/onboarding/sign_in/view.cljs | 6 +- .../onboarding/syncing/progress/view.cljs | 7 +- .../syncing/scan_sync_code/animation.cljs | 61 +++++ .../contexts/syncing/scan_sync_code/view.cljs | 243 +++++++++--------- .../syncing/scan_sync_code_page/view.cljs | 7 +- src/status_im2/navigation/util.cljs | 42 +++ 7 files changed, 236 insertions(+), 135 deletions(-) create mode 100644 src/status_im2/contexts/syncing/scan_sync_code/animation.cljs create mode 100644 src/status_im2/navigation/util.cljs diff --git a/src/react_native/navigation.cljs b/src/react_native/navigation.cljs index fbce207798..d5a385ce15 100644 --- a/src/react_native/navigation.cljs +++ b/src/react_native/navigation.cljs @@ -89,3 +89,8 @@ (reset! constants {:top-bar-height (.-topBarHeight consts) :bottom-tabs-height (.-bottomTabsHeight consts) :status-bar-height (.-statusBarHeight consts)}))) + +(defn bind-component + [^js/Object this component-id] + (set! (. this -navigationEventListener) + (.. Navigation events (bindComponent this component-id)))) diff --git a/src/status_im2/contexts/onboarding/sign_in/view.cljs b/src/status_im2/contexts/onboarding/sign_in/view.cljs index 17a685d9a8..4d52f29b8f 100644 --- a/src/status_im2/contexts/onboarding/sign_in/view.cljs +++ b/src/status_im2/contexts/onboarding/sign_in/view.cljs @@ -14,11 +14,13 @@ {:title (i18n/label :t/sign-in-by-syncing) :show-bottom-view? true :background [background/view true] - :animated? false}]) + :animated? false + :screen-name "sign-in"}]) (defn animated-view [] [scan-sync-code/view {:title (i18n/label :t/sign-in-by-syncing) :show-bottom-view? true - :animated? true}]) + :animated? true + :screen-name "sign-in-intro"}]) diff --git a/src/status_im2/contexts/onboarding/syncing/progress/view.cljs b/src/status_im2/contexts/onboarding/syncing/progress/view.cljs index ef04b54152..6c9f4c1be9 100644 --- a/src/status_im2/contexts/onboarding/syncing/progress/view.cljs +++ b/src/status_im2/contexts/onboarding/syncing/progress/view.cljs @@ -8,12 +8,7 @@ (defn pairing-progress [status] - (cond - (= status :error) - false - - :else - true)) + (not= status :error)) (defn page-title [pairing-progress?] diff --git a/src/status_im2/contexts/syncing/scan_sync_code/animation.cljs b/src/status_im2/contexts/syncing/scan_sync_code/animation.cljs new file mode 100644 index 0000000000..0c67c7ede1 --- /dev/null +++ b/src/status_im2/contexts/syncing/scan_sync_code/animation.cljs @@ -0,0 +1,61 @@ +(ns status-im2.contexts.syncing.scan-sync-code.animation + (:require [react-native.reanimated :as reanimated] + [status-im2.constants :as constants])) + +(defn animate-subtitle + [subtitle-opacity] + (reanimated/animate-shared-value-with-delay + subtitle-opacity + 1 + constants/onboarding-modal-animation-duration + :easing4 + (/ constants/onboarding-modal-animation-delay 2))) + +(defn animate-title + [title-opacity] + (reanimated/animate-shared-value-with-delay + title-opacity + 1 + 0 + :easing4 + (+ constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay))) + +(defn animate-bottom + [bottom-view-translate-y] + (reanimated/animate-delay + bottom-view-translate-y + 0 + (+ constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay) + 100)) + +(defn animate-content + [content-opacity] + (reanimated/animate-shared-value-with-delay + content-opacity + 1 + constants/onboarding-modal-animation-duration + :easing4 + (/ constants/onboarding-modal-animation-delay 2))) + +(defn reset-animations + [{:keys [content-opacity subtitle-opacity title-opacity]}] + (reanimated/animate-shared-value-with-timing + content-opacity + 0 + (/ constants/onboarding-modal-animation-duration 8) + :easing4) + + (reanimated/animate-shared-value-with-timing + subtitle-opacity + 0 + (- constants/onboarding-modal-animation-duration + constants/onboarding-modal-animation-delay) + :easing4) + + (reanimated/animate-shared-value-with-timing + title-opacity + 0 + 0 + :easing4)) 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 b5431abed2..8de42e5f77 100644 --- a/src/status_im2/contexts/syncing/scan_sync_code/view.cljs +++ b/src/status_im2/contexts/syncing/scan_sync_code/view.cljs @@ -14,8 +14,10 @@ [reagent.core :as reagent] [status-im2.common.device-permissions :as device-permissions] [status-im2.constants :as constants] + [status-im2.contexts.syncing.scan-sync-code.animation :as animation] [status-im2.contexts.syncing.scan-sync-code.style :as style] [status-im2.contexts.syncing.utils :as sync-utils] + [status-im2.navigation.util :as navigation.util] [utils.debounce :as debounce] [utils.i18n :as i18n] [utils.re-frame :as rf] @@ -37,7 +39,7 @@ (rf/dispatch [:syncing/preflight-outbound-check #(reset! preflight-check-passed? %)])) (defn- header - [{:keys [active-tab read-qr-once? title title-opacity subtitle-opacity reset-animations-fn animated?]}] + [{:keys [active-tab title title-opacity subtitle-opacity reset-animations-fn animated?]}] (let [subtitle-translate-x (reanimated/interpolate subtitle-opacity [0 1] [-13 0]) subtitle-translate-y (reanimated/interpolate subtitle-opacity [0 1] [-85 0]) subtitle-scale (reanimated/interpolate subtitle-opacity [0 1] [0.9 1]) @@ -106,8 +108,7 @@ :data [{:id 1 :label (i18n/label :t/scan-sync-qr-code)} {:id 2 :label (i18n/label :t/enter-sync-code)}] :on-change (fn [id] - (reset! active-tab id) - (reset! read-qr-once? false))}]]])) + (reset! active-tab id))}]]])) (defn get-labels-and-on-press-method [] @@ -225,135 +226,114 @@ [insets translate-y] [:f> f-bottom-view insets translate-y]) -(defn- check-qr-code-data - [event] +(defn- check-qr-code-and-navigate + [{:keys [event on-success-scan on-failed-scan]}] (let [connection-string (string/trim (oops/oget event "nativeEvent.codeStringValue")) valid-connection-string? (sync-utils/valid-connection-string? connection-string)] + ;; debounce-and-dispatch used because the QR code scanner performs callbacks too fast (if valid-connection-string? - (debounce/debounce-and-dispatch [:syncing/input-connection-string-for-bootstrapping - connection-string] - 300) - (rf/dispatch [:toasts/upsert - {:icon :i/info - :icon-color colors/danger-50 - :theme :dark - :text (i18n/label :t/error-this-is-not-a-sync-qr-code)}])))) + (do + (on-success-scan) + (debounce/debounce-and-dispatch + [:syncing/input-connection-string-for-bootstrapping connection-string] + 300)) + (do + (on-failed-scan) + (debounce/debounce-and-dispatch + [:toasts/upsert + {:icon :i/info + :icon-color colors/danger-50 + :theme :dark + :text (i18n/label :t/error-this-is-not-a-sync-qr-code)}] + 300))))) -(defn render-camera - [show-camera? torch-mode qr-view-finder camera-ref on-read-code] - (when (and show-camera? (:x qr-view-finder)) - [:<> - [rn/view {:style style/camera-container} - [camera-kit/camera - {:ref #(reset! camera-ref %) - :style style/camera-style - :camera-type camera-kit/camera-type-back - :zoom-mode :off - :torch-mode torch-mode - :scan-barcode true - :on-read-code on-read-code}]] - [hole-view/hole-view - {:style style/hole - :holes [(assoc qr-view-finder :borderRadius 16)]} - [blur/view - {:style style/absolute-fill - :blur-amount 10 - :blur-type :transparent - :overlay-color colors/neutral-80-opa-80 - :background-color colors/neutral-80-opa-80}]]])) +(defn- render-camera + [{:keys [torch-mode qr-view-finder scan-code? set-qr-code-succeeded set-rescan-timeout]}] + [:<> + [rn/view {:style style/camera-container} + [camera-kit/camera + {:style style/camera-style + :camera-type camera-kit/camera-type-back + :zoom-mode :off + :torch-mode torch-mode + :scan-barcode true + :on-read-code #(when scan-code? + (check-qr-code-and-navigate {:event % + :on-success-scan set-qr-code-succeeded + :on-failed-scan set-rescan-timeout}))}]] + [hole-view/hole-view + {:style style/hole + :holes [(assoc qr-view-finder :borderRadius 16)]} + [blur/view + {:style style/absolute-fill + :blur-amount 10 + :blur-type :transparent + :overlay-color colors/neutral-80-opa-80 + :background-color colors/neutral-80-opa-80}]]]) + +(defn- reset-animations-and-navigate-fn + [{:keys [render-camera? show-camera?] :as params}] + (letfn [(reset-fn [] + (rf/dispatch [:navigate-back]) + (when @dismiss-animations (@dismiss-animations)) + (animation/reset-animations params))] + (fn [] + (reset! render-camera? false) + (js/setTimeout reset-fn (if show-camera? 500 0))))) + +(defn- set-listener-torch-off-on-app-inactive + [torch-atm] + (let [set-torch-off-fn #(when (not= % "active") (reset! torch-atm false)) + app-state-listener (.addEventListener rn/app-state "change" set-torch-off-fn)] + #(.remove app-state-listener))) (defn f-view - [{:keys [title show-bottom-view? background animated?]}] + [_] (let [insets (safe-area/get-insets) active-tab (reagent/atom 1) qr-view-finder (reagent/atom {}) render-camera? (reagent/atom false) torch? (reagent/atom false) - app-state-listener (atom nil)] - (fn [] - (let [camera-ref (atom nil) - read-qr-once? (atom false) - torch-mode (if @torch? :on :off) - flashlight-icon (if @torch? :i/flashlight-on :i/flashlight-off) - ;; The below check is to prevent scanning of any QR code - ;; when the user is in syncing progress screen - user-in-syncing-progress-screen? (= (rf/sub [:view-id]) :syncing-progress) - on-read-code (fn [data] - (when (and (not @read-qr-once?) - (not user-in-syncing-progress-screen?)) - (reset! read-qr-once? true) - (js/setTimeout (fn [] - (reset! read-qr-once? false)) - 3000) - (check-qr-code-data data))) - scan-qr-code-tab? (= @active-tab 1) - show-camera? (and scan-qr-code-tab? - @camera-permission-granted? - @preflight-check-passed? - (boolean (not-empty @qr-view-finder))) - title-opacity (reanimated/use-shared-value (if animated? 0 1)) - subtitle-opacity (reanimated/use-shared-value (if animated? 0 1)) - content-opacity (reanimated/use-shared-value (if animated? 0 1)) - content-translate-y (reanimated/interpolate subtitle-opacity [0 1] [85 0]) + scan-code? (reagent/atom true) + set-rescan-timeout (fn [] + (reset! scan-code? false) + (js/setTimeout #(reset! scan-code? true) 3000))] + (fn [{:keys [title show-bottom-view? background animated? qr-code-succeed? + set-qr-code-succeeded]}] + (let [torch-mode (if @torch? :on :off) + flashlight-icon (if @torch? :i/flashlight-on :i/flashlight-off) + scan-qr-code-tab? (= @active-tab 1) + show-camera? (and scan-qr-code-tab? + @camera-permission-granted? + @preflight-check-passed? + (boolean (not-empty @qr-view-finder))) + camera-ready-to-scan? (and (or (not animated?) @render-camera?) + show-camera? + (not qr-code-succeed?)) + title-opacity (reanimated/use-shared-value (if animated? 0 1)) + subtitle-opacity (reanimated/use-shared-value (if animated? 0 1)) + content-opacity (reanimated/use-shared-value (if animated? 0 1)) + content-translate-y (reanimated/interpolate subtitle-opacity [0 1] [85 0]) bottom-view-translate-y (reanimated/use-shared-value (if animated? (+ 42.2 (:bottom insets)) 0)) - reset-animations-fn - (fn [] - (reset! render-camera? false) - (js/setTimeout - (fn [] - (rf/dispatch [:navigate-back]) - (when @dismiss-animations - (@dismiss-animations)) - (reanimated/animate-shared-value-with-timing - content-opacity - 0 - (/ constants/onboarding-modal-animation-duration 8) - :easing4) - (reanimated/animate-shared-value-with-timing - subtitle-opacity - 0 - (- constants/onboarding-modal-animation-duration - constants/onboarding-modal-animation-delay) - :easing4) - (reanimated/animate-shared-value-with-timing title-opacity - 0 - 0 - :easing4)) - (if show-camera? 500 0)))] - (rn/use-effect (fn [] - (reset! app-state-listener - (.addEventListener rn/app-state - "change" - #(when (and (not= % "active") @torch?) - (reset! torch? false)))) - #(.remove @app-state-listener))) - (when animated? - (reanimated/animate-shared-value-with-delay subtitle-opacity - 1 constants/onboarding-modal-animation-duration - :easing4 - (/ - constants/onboarding-modal-animation-delay - 2)) - (reanimated/animate-shared-value-with-delay title-opacity - 1 0 - :easing4 - (+ constants/onboarding-modal-animation-duration - constants/onboarding-modal-animation-delay)) - (reanimated/animate-delay bottom-view-translate-y - 0 - (+ constants/onboarding-modal-animation-duration - constants/onboarding-modal-animation-delay) - 100)) + reset-animations-fn (reset-animations-and-navigate-fn + {:render-camera? render-camera? + :show-camera? show-camera? + :content-opacity content-opacity + :subtitle-opacity subtitle-opacity + :title-opacity title-opacity})] (rn/use-effect - (fn [] + #(set-listener-torch-off-on-app-inactive torch?)) + + (when animated? + (animation/animate-subtitle subtitle-opacity) + (animation/animate-title title-opacity) + (animation/animate-bottom bottom-view-translate-y)) + + (rn/use-effect + (fn initialize-component [] (when animated? - (reanimated/animate-shared-value-with-delay content-opacity - 1 constants/onboarding-modal-animation-duration - :easing4 - (/ - constants/onboarding-modal-animation-delay - 2)) + (animation/animate-content content-opacity) (js/setTimeout #(reset! render-camera? true) (+ constants/onboarding-modal-animation-duration constants/onboarding-modal-animation-delay @@ -365,12 +345,16 @@ #(reset! camera-permission-granted? false))))) [:<> background - (when (or (not animated?) @render-camera?) - [render-camera show-camera? torch-mode @qr-view-finder camera-ref on-read-code]) + (when camera-ready-to-scan? + [render-camera + {:torch-mode torch-mode + :qr-view-finder @qr-view-finder + :scan-code? @scan-code? + :set-qr-code-succeeded set-qr-code-succeeded + :set-rescan-timeout set-rescan-timeout}]) [rn/view {:style (style/root-container (:top insets))} [:f> header {:active-tab active-tab - :read-qr-once? read-qr-once? :title title :title-opacity title-opacity :subtitle-opacity subtitle-opacity @@ -390,8 +374,10 @@ 2 [enter-sync-code-tab] nil)] [rn/view {:style style/flex-spacer}] - (when show-bottom-view? [bottom-view insets bottom-view-translate-y]) - (when (and (or (not animated?) @render-camera?) show-camera?) + (when show-bottom-view? + [bottom-view insets bottom-view-translate-y]) + (when (and (or (not animated?) @render-camera?) + show-camera?) [quo/button {:icon-only? true :type :grey @@ -403,5 +389,14 @@ flashlight-icon])]])))) (defn view - [props] - [:f> f-view props]) + [{:keys [screen-name] :as _props}] + (let [qr-code-succeed? (reagent/atom false)] + (navigation.util/create-class-and-bind + screen-name + {:component-did-appear (fn set-qr-code-failed [_this] + (reset! qr-code-succeed? false))} + (fn [props] + (let [new-pops (assoc props + :qr-code-succeed? @qr-code-succeed? + :set-qr-code-succeeded #(reset! qr-code-succeed? true))] + [:f> f-view new-pops]))))) diff --git a/src/status_im2/contexts/syncing/scan_sync_code_page/view.cljs b/src/status_im2/contexts/syncing/scan_sync_code_page/view.cljs index 56921c7b44..8e1b28c4d8 100644 --- a/src/status_im2/contexts/syncing/scan_sync_code_page/view.cljs +++ b/src/status_im2/contexts/syncing/scan_sync_code_page/view.cljs @@ -7,6 +7,7 @@ (defn view [] [scan-sync-code/view - {:title (i18n/label :t/scan-sync-code) - :background [rn/view - {:style style/background} true]}]) + {:title (i18n/label :t/scan-sync-code) + :background [rn/view + {:style style/background} true] + :screen-name "scan-sync-code-page"}]) diff --git a/src/status_im2/navigation/util.cljs b/src/status_im2/navigation/util.cljs new file mode 100644 index 0000000000..6fd2867b15 --- /dev/null +++ b/src/status_im2/navigation/util.cljs @@ -0,0 +1,42 @@ +(ns status-im2.navigation.util + (:require [react-native.navigation :as navigation] + [reagent.core :as reagent])) + +(defn create-class-and-bind + "Creates a React class that allows the use of life-cycle methods added by + react-native-navigation: + - componentWillAppear + - componentDidAppear + - componentDidDisappear + Receives: + - `component-id` - The component-id to subscribe registered in navigation + - `react-methods` - A map of React methods (kebab-case) -> function handler + - `reagent-render` - A regular reagent function that returns hiccup. + Example: + (defn view + [props & children] + ;; Bindings executed when component is created + (let [qr-code-succeed? (reagent/atom false)] + (create-class-and-bind + \"sign-in-intro\" ; navigation component-id of the screen to subscribe + {:component-did-appear (fn [this] + ;; Executed when component appeared to the screen + ) + :component-will-appear (fn [this] + ;; Executed when component will be shown to the screen + ) + :component-did-disappear (fn [this] + ;; Executed when component disappeared from the screen + )} + (fn [props & children] ; Must be the same signature as this `view` function + ;; Regular component call, e.g.: + [rn/view {:style {:padding-top 10}} + [:f> my-f-component-call (assoc props :on-press identity)] + children])))) + " + [component-id react-methods reagent-render] + (reagent/create-class + (assoc react-methods + :display-name (str component-id "-view") + :component-did-mount #(navigation/bind-component % component-id) + :reagent-render reagent-render)))