[#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
This commit is contained in:
Ulises Manuel Cárdenas 2023-08-08 11:53:05 -06:00 committed by GitHub
parent ff1ae2f4aa
commit 79bf4bb8d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 236 additions and 135 deletions

View File

@ -89,3 +89,8 @@
(reset! constants {:top-bar-height (.-topBarHeight consts) (reset! constants {:top-bar-height (.-topBarHeight consts)
:bottom-tabs-height (.-bottomTabsHeight consts) :bottom-tabs-height (.-bottomTabsHeight consts)
:status-bar-height (.-statusBarHeight consts)}))) :status-bar-height (.-statusBarHeight consts)})))
(defn bind-component
[^js/Object this component-id]
(set! (. this -navigationEventListener)
(.. Navigation events (bindComponent this component-id))))

View File

@ -14,11 +14,13 @@
{:title (i18n/label :t/sign-in-by-syncing) {:title (i18n/label :t/sign-in-by-syncing)
:show-bottom-view? true :show-bottom-view? true
:background [background/view true] :background [background/view true]
:animated? false}]) :animated? false
:screen-name "sign-in"}])
(defn animated-view (defn animated-view
[] []
[scan-sync-code/view [scan-sync-code/view
{:title (i18n/label :t/sign-in-by-syncing) {:title (i18n/label :t/sign-in-by-syncing)
:show-bottom-view? true :show-bottom-view? true
:animated? true}]) :animated? true
:screen-name "sign-in-intro"}])

View File

@ -8,12 +8,7 @@
(defn pairing-progress (defn pairing-progress
[status] [status]
(cond (not= status :error))
(= status :error)
false
:else
true))
(defn page-title (defn page-title
[pairing-progress?] [pairing-progress?]

View File

@ -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))

View File

@ -14,8 +14,10 @@
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im2.common.device-permissions :as device-permissions] [status-im2.common.device-permissions :as device-permissions]
[status-im2.constants :as constants] [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.scan-sync-code.style :as style]
[status-im2.contexts.syncing.utils :as sync-utils] [status-im2.contexts.syncing.utils :as sync-utils]
[status-im2.navigation.util :as navigation.util]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf] [utils.re-frame :as rf]
@ -37,7 +39,7 @@
(rf/dispatch [:syncing/preflight-outbound-check #(reset! preflight-check-passed? %)])) (rf/dispatch [:syncing/preflight-outbound-check #(reset! preflight-check-passed? %)]))
(defn- header (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]) (let [subtitle-translate-x (reanimated/interpolate subtitle-opacity [0 1] [-13 0])
subtitle-translate-y (reanimated/interpolate subtitle-opacity [0 1] [-85 0]) subtitle-translate-y (reanimated/interpolate subtitle-opacity [0 1] [-85 0])
subtitle-scale (reanimated/interpolate subtitle-opacity [0 1] [0.9 1]) 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)} :data [{:id 1 :label (i18n/label :t/scan-sync-qr-code)}
{:id 2 :label (i18n/label :t/enter-sync-code)}] {:id 2 :label (i18n/label :t/enter-sync-code)}]
:on-change (fn [id] :on-change (fn [id]
(reset! active-tab id) (reset! active-tab id))}]]]))
(reset! read-qr-once? false))}]]]))
(defn get-labels-and-on-press-method (defn get-labels-and-on-press-method
[] []
@ -225,135 +226,114 @@
[insets translate-y] [insets translate-y]
[:f> f-bottom-view insets translate-y]) [:f> f-bottom-view insets translate-y])
(defn- check-qr-code-data (defn- check-qr-code-and-navigate
[event] [{:keys [event on-success-scan on-failed-scan]}]
(let [connection-string (string/trim (oops/oget event "nativeEvent.codeStringValue")) (let [connection-string (string/trim (oops/oget event "nativeEvent.codeStringValue"))
valid-connection-string? (sync-utils/valid-connection-string? connection-string)] 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? (if valid-connection-string?
(debounce/debounce-and-dispatch [:syncing/input-connection-string-for-bootstrapping (do
connection-string] (on-success-scan)
300) (debounce/debounce-and-dispatch
(rf/dispatch [:toasts/upsert [:syncing/input-connection-string-for-bootstrapping connection-string]
{:icon :i/info 300))
:icon-color colors/danger-50 (do
:theme :dark (on-failed-scan)
:text (i18n/label :t/error-this-is-not-a-sync-qr-code)}])))) (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 (defn- render-camera
[show-camera? torch-mode qr-view-finder camera-ref on-read-code] [{:keys [torch-mode qr-view-finder scan-code? set-qr-code-succeeded set-rescan-timeout]}]
(when (and show-camera? (:x qr-view-finder)) [:<>
[:<> [rn/view {:style style/camera-container}
[rn/view {:style style/camera-container} [camera-kit/camera
[camera-kit/camera {:style style/camera-style
{:ref #(reset! camera-ref %) :camera-type camera-kit/camera-type-back
:style style/camera-style :zoom-mode :off
:camera-type camera-kit/camera-type-back :torch-mode torch-mode
:zoom-mode :off :scan-barcode true
:torch-mode torch-mode :on-read-code #(when scan-code?
:scan-barcode true (check-qr-code-and-navigate {:event %
:on-read-code on-read-code}]] :on-success-scan set-qr-code-succeeded
[hole-view/hole-view :on-failed-scan set-rescan-timeout}))}]]
{:style style/hole [hole-view/hole-view
:holes [(assoc qr-view-finder :borderRadius 16)]} {:style style/hole
[blur/view :holes [(assoc qr-view-finder :borderRadius 16)]}
{:style style/absolute-fill [blur/view
:blur-amount 10 {:style style/absolute-fill
:blur-type :transparent :blur-amount 10
:overlay-color colors/neutral-80-opa-80 :blur-type :transparent
:background-color colors/neutral-80-opa-80}]]])) :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 (defn f-view
[{:keys [title show-bottom-view? background animated?]}] [_]
(let [insets (safe-area/get-insets) (let [insets (safe-area/get-insets)
active-tab (reagent/atom 1) active-tab (reagent/atom 1)
qr-view-finder (reagent/atom {}) qr-view-finder (reagent/atom {})
render-camera? (reagent/atom false) render-camera? (reagent/atom false)
torch? (reagent/atom false) torch? (reagent/atom false)
app-state-listener (atom nil)] scan-code? (reagent/atom true)
(fn [] set-rescan-timeout (fn []
(let [camera-ref (atom nil) (reset! scan-code? false)
read-qr-once? (atom false) (js/setTimeout #(reset! scan-code? true) 3000))]
torch-mode (if @torch? :on :off) (fn [{:keys [title show-bottom-view? background animated? qr-code-succeed?
flashlight-icon (if @torch? :i/flashlight-on :i/flashlight-off) set-qr-code-succeeded]}]
;; The below check is to prevent scanning of any QR code (let [torch-mode (if @torch? :on :off)
;; when the user is in syncing progress screen flashlight-icon (if @torch? :i/flashlight-on :i/flashlight-off)
user-in-syncing-progress-screen? (= (rf/sub [:view-id]) :syncing-progress) scan-qr-code-tab? (= @active-tab 1)
on-read-code (fn [data] show-camera? (and scan-qr-code-tab?
(when (and (not @read-qr-once?) @camera-permission-granted?
(not user-in-syncing-progress-screen?)) @preflight-check-passed?
(reset! read-qr-once? true) (boolean (not-empty @qr-view-finder)))
(js/setTimeout (fn [] camera-ready-to-scan? (and (or (not animated?) @render-camera?)
(reset! read-qr-once? false)) show-camera?
3000) (not qr-code-succeed?))
(check-qr-code-data data))) title-opacity (reanimated/use-shared-value (if animated? 0 1))
scan-qr-code-tab? (= @active-tab 1) subtitle-opacity (reanimated/use-shared-value (if animated? 0 1))
show-camera? (and scan-qr-code-tab? content-opacity (reanimated/use-shared-value (if animated? 0 1))
@camera-permission-granted? content-translate-y (reanimated/interpolate subtitle-opacity [0 1] [85 0])
@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])
bottom-view-translate-y (reanimated/use-shared-value bottom-view-translate-y (reanimated/use-shared-value
(if animated? (+ 42.2 (:bottom insets)) 0)) (if animated? (+ 42.2 (:bottom insets)) 0))
reset-animations-fn reset-animations-fn (reset-animations-and-navigate-fn
(fn [] {:render-camera? render-camera?
(reset! render-camera? false) :show-camera? show-camera?
(js/setTimeout :content-opacity content-opacity
(fn [] :subtitle-opacity subtitle-opacity
(rf/dispatch [:navigate-back]) :title-opacity title-opacity})]
(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))
(rn/use-effect (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? (when animated?
(reanimated/animate-shared-value-with-delay content-opacity (animation/animate-content content-opacity)
1 constants/onboarding-modal-animation-duration
:easing4
(/
constants/onboarding-modal-animation-delay
2))
(js/setTimeout #(reset! render-camera? true) (js/setTimeout #(reset! render-camera? true)
(+ constants/onboarding-modal-animation-duration (+ constants/onboarding-modal-animation-duration
constants/onboarding-modal-animation-delay constants/onboarding-modal-animation-delay
@ -365,12 +345,16 @@
#(reset! camera-permission-granted? false))))) #(reset! camera-permission-granted? false)))))
[:<> [:<>
background background
(when (or (not animated?) @render-camera?) (when camera-ready-to-scan?
[render-camera show-camera? torch-mode @qr-view-finder camera-ref on-read-code]) [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))} [rn/view {:style (style/root-container (:top insets))}
[:f> header [:f> header
{:active-tab active-tab {:active-tab active-tab
:read-qr-once? read-qr-once?
:title title :title title
:title-opacity title-opacity :title-opacity title-opacity
:subtitle-opacity subtitle-opacity :subtitle-opacity subtitle-opacity
@ -390,8 +374,10 @@
2 [enter-sync-code-tab] 2 [enter-sync-code-tab]
nil)] nil)]
[rn/view {:style style/flex-spacer}] [rn/view {:style style/flex-spacer}]
(when show-bottom-view? [bottom-view insets bottom-view-translate-y]) (when show-bottom-view?
(when (and (or (not animated?) @render-camera?) show-camera?) [bottom-view insets bottom-view-translate-y])
(when (and (or (not animated?) @render-camera?)
show-camera?)
[quo/button [quo/button
{:icon-only? true {:icon-only? true
:type :grey :type :grey
@ -403,5 +389,14 @@
flashlight-icon])]])))) flashlight-icon])]]))))
(defn view (defn view
[props] [{:keys [screen-name] :as _props}]
[:f> f-view 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])))))

View File

@ -7,6 +7,7 @@
(defn view (defn view
[] []
[scan-sync-code/view [scan-sync-code/view
{:title (i18n/label :t/scan-sync-code) {:title (i18n/label :t/scan-sync-code)
:background [rn/view :background [rn/view
{:style style/background} true]}]) {:style style/background} true]
:screen-name "scan-sync-code-page"}])

View File

@ -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)))