Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627bb19313 | ||
|
|
6f3ef8c76b | ||
|
|
86c5505c94 | ||
|
|
c1dcd7a764 | ||
|
|
17ebedd6b8 | ||
|
|
2e2d15adfb | ||
|
|
1d1d586223 | ||
|
|
88dd5e04bb | ||
|
|
8f8c8dede2 | ||
|
|
2e0643fabe | ||
|
|
ec4046e543 | ||
|
|
4a44161163 | ||
|
|
b9353b1ccd | ||
|
|
5ceb7c2215 | ||
|
|
2043e9353b | ||
|
|
7153e4da48 | ||
|
|
c7cba5b4d3 |
+12
-4
@@ -16,10 +16,18 @@
|
||||
:clj-kondo-config {:level :error}
|
||||
:cond-else {:level :error}
|
||||
:consistent-alias {:level :error
|
||||
:aliases {clojure.string string
|
||||
clojure.set set
|
||||
clojure.walk walk
|
||||
taoensso.timbre log}}
|
||||
:aliases {clojure.set set
|
||||
clojure.string string
|
||||
clojure.walk walk
|
||||
malli.core malli
|
||||
malli.dev.pretty malli.pretty
|
||||
malli.dev.virhe malli.virhe
|
||||
malli.error malli.error
|
||||
malli.generator malli.generator
|
||||
malli.transform malli.transform
|
||||
malli.util malli.util
|
||||
schema.core schema
|
||||
taoensso.timbre log}}
|
||||
:deprecated-namespace {:level :warning}
|
||||
:docstring-blank {:level :error}
|
||||
:equals-true {:level :error}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{:lint-as {malli.experimental/defn schema.core/defn}
|
||||
:linters {:unresolved-symbol {:exclude [(malli.core/=>)]}}}
|
||||
@@ -36,6 +36,7 @@
|
||||
"deftest-sub" :arg1-body
|
||||
"wait-for" :arg1-body
|
||||
"with-deps-check" :arg1-body
|
||||
"schema/=>" :arg1-body
|
||||
"->" [:noarg1-body
|
||||
{:list {:constant-pair? false :force-nl? false}
|
||||
:next-inner-restore [[:list :constant-pair?]]}]
|
||||
|
||||
@@ -311,13 +311,14 @@ endef
|
||||
lint: export TARGET := clojure
|
||||
lint: export CLJ_LINTER_PRINT_WARNINGS ?= false
|
||||
lint: ##@test Run code style checks
|
||||
@sh scripts/lint-re-frame-in-quo-components.sh && \
|
||||
sh scripts/lint-direct-require-component-outside-quo.sh && \
|
||||
@sh scripts/lint/re-frame-in-quo-components.sh && \
|
||||
sh scripts/lint/direct-require-component-outside-quo.sh && \
|
||||
sh scripts/lint/require-i18n-resource-first.sh && \
|
||||
clj-kondo --config .clj-kondo/config.edn --cache false --fail-level error --lint src $(if $(filter $(CLJ_LINTER_PRINT_WARNINGS),true),,| grep -v ': warning: ') && \
|
||||
ALL_CLOJURE_FILES=$(call find_all_clojure_files) && \
|
||||
scripts/lint_translations.clj && \
|
||||
scripts/lint/translations.clj && \
|
||||
zprint '{:search-config? true}' -sfc $$ALL_CLOJURE_FILES && \
|
||||
sh scripts/lint-trailing-newline.sh && \
|
||||
sh scripts/lint/trailing-newline.sh && \
|
||||
node_modules/.bin/prettier --write .
|
||||
|
||||
# NOTE: We run the linter twice because of https://github.com/kkinnear/zprint/issues/271
|
||||
@@ -327,7 +328,7 @@ lint-fix: ##@test Run code style checks and fix issues
|
||||
zprint '{:search-config? true}' -sw $$ALL_CLOJURE_FILES && \
|
||||
zprint '{:search-config? true}' -sw $$ALL_CLOJURE_FILES && \
|
||||
clojure-lsp --ns-exclude-regex ".*/src/status_im2/core\.cljs$$" clean-ns && \
|
||||
sh scripts/lint-trailing-newline.sh --fix && \
|
||||
sh scripts/lint/trailing-newline.sh --fix && \
|
||||
node_modules/.bin/prettier --write .
|
||||
|
||||
shadow-server: export TARGET := clojure
|
||||
|
||||
+48
-12
@@ -45,11 +45,20 @@
|
||||
},
|
||||
|
||||
{
|
||||
"path": "borkdude/edamame/1.1.17/edamame-1.1.17",
|
||||
"path": "borkdude/dynaload/0.3.5/dynaload-0.3.5",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "9087f7abf0104e0354d7db7fc4576608eac558f4",
|
||||
"sha256": "1n1872i240lakn4pzsag4grf7bv7lcsipmqllxd9m4k1zp3dgla1"
|
||||
"sha1": "accd696ba364b850b4d92e38f5a34d0e828a0ad1",
|
||||
"sha256": "0k62m1f29xfh3cp67w7kcvkp5aj35simi8kf95ycvkmgp76w11q8"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "borkdude/edamame/1.3.23/edamame-1.3.23",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "254d023e97ed438f0f44532b5a06d928d031ede4",
|
||||
"sha256": "0l7mxza2nimslhg0qh6jp7gmb8sd6l89fk5d1zvzq2xskscq2vly"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -81,20 +90,20 @@
|
||||
},
|
||||
|
||||
{
|
||||
"path": "cider/cider-nrepl/0.29.0/cider-nrepl-0.29.0",
|
||||
"path": "cider/cider-nrepl/0.25.3/cider-nrepl-0.25.3",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "45f6034b26a14138e74145b7a4059628c0fedcd1",
|
||||
"sha256": "1dy1l6y8cb8xiqq97a4lf8giyiicq4wfl4s2lxn5fb6614cjxqx2"
|
||||
"sha1": "5ae0efd9377a5e60c084bdaf4a2ce094f759ce23",
|
||||
"sha256": "0drxf9nm23i1pcgrkwbcr09msq37csilzww38709add0hz8spjhq"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "cider/piggieback/0.5.2/piggieback-0.5.2",
|
||||
"path": "cider/piggieback/0.4.1/piggieback-0.4.1",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "ecfd5c286a85db3f059e75c37fca5722d9e26f79",
|
||||
"sha256": "1ps9yf3cxmlm447hqkidjb5xry90n0wl3jk0jn28fagq31lzylkl"
|
||||
"sha1": "0a02a3e2ecd7a126ab60d8a44793342f20ced79b",
|
||||
"sha256": "142vl5np33akcrnn6pksi0rjfsmmi528villxsj6cwcndvybiw4m"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -539,6 +548,24 @@
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "metosin/malli/0.13.0/malli-0.13.0",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "286c52d26a3a9d613b5f692971bd19c581cdc4b0",
|
||||
"sha256": "0vaq9d6cln5j4ww302fwlgg7m647cf2j16gs2i1p1d4klbn4jddz"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "1bc8f3bba7a28de85f319b7d464aa8d955f44918",
|
||||
"sha256": "1j1rkwrs4wm8zk9v7ilgpnyav5sipdd2bmb83sazh392blw52wpk"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "net/cgrand/macrovich/0.2.1/macrovich-0.2.1",
|
||||
"host": "https://repo.clojars.org",
|
||||
@@ -728,6 +755,15 @@
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "org/clojure/test.check/1.1.1/test.check-1.1.1",
|
||||
"host": "https://repo1.maven.org/maven2",
|
||||
"jar": {
|
||||
"sha1": "f33d988fd57bc9c11af1952db81c10f319c91416",
|
||||
"sha256": "0y2hpkj7zl4yrpsl35ifpdaja5c72b8fpjcnmdgmld9c7cb1hlcl"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "org/clojure/tools.analyzer/1.1.0/tools.analyzer-1.1.0",
|
||||
"host": "https://repo1.maven.org/maven2",
|
||||
@@ -909,11 +945,11 @@
|
||||
},
|
||||
|
||||
{
|
||||
"path": "refactor-nrepl/refactor-nrepl/3.6.0/refactor-nrepl-3.6.0",
|
||||
"path": "refactor-nrepl/refactor-nrepl/2.5.0/refactor-nrepl-2.5.0",
|
||||
"host": "https://repo.clojars.org",
|
||||
"jar": {
|
||||
"sha1": "2b3bb82da53b5db9c2b2aa298417816b81d0ed97",
|
||||
"sha256": "1ysqabmlnghki6x0636zngxza2d83c85276wp9ma9wk183mkv52a"
|
||||
"sha1": "6bc3441afc94f7ca024e41a864ca75e05df7e207",
|
||||
"sha256": "0w8hax99y98l53mixxzx2ja0vcnhjv8dnsaz1zj3vqk775ns5w6i"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@ babashka/fs/0.2.16/fs-0.2.16.jar
|
||||
bidi/bidi/2.1.6/bidi-2.1.6.jar
|
||||
binaryage/env-config/0.2.2/env-config-0.2.2.jar
|
||||
binaryage/oops/0.7.2/oops-0.7.2.jar
|
||||
borkdude/edamame/1.1.17/edamame-1.1.17.jar
|
||||
borkdude/dynaload/0.3.5/dynaload-0.3.5.jar
|
||||
borkdude/edamame/1.3.23/edamame-1.3.23.jar
|
||||
borkdude/sci.impl.reflector/0.0.1/sci.impl.reflector-0.0.1.jar
|
||||
camel-snake-kebab/camel-snake-kebab/0.4.3/camel-snake-kebab-0.4.3.jar
|
||||
cheshire/cheshire/5.11.0/cheshire-5.11.0.jar
|
||||
cider/cider-nrepl/0.29.0/cider-nrepl-0.29.0.jar
|
||||
cider/piggieback/0.5.2/piggieback-0.5.2.jar
|
||||
cider/cider-nrepl/0.25.3/cider-nrepl-0.25.3.jar
|
||||
cider/piggieback/0.4.1/piggieback-0.4.1.jar
|
||||
clj-kondo/clj-kondo/2023.09.07/clj-kondo-2023.09.07.jar
|
||||
cljs-bean/cljs-bean/1.9.0/cljs-bean-1.9.0.jar
|
||||
clout/clout/2.1.2/clout-2.1.2.jar
|
||||
@@ -58,6 +59,8 @@ javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar
|
||||
javax/servlet/servlet-api/2.5/servlet-api-2.5.jar
|
||||
javax/xml/bind/jaxb-api/2.3.0/jaxb-api-2.3.0.jar
|
||||
medley/medley/0.8.2/medley-0.8.2.jar
|
||||
metosin/malli/0.13.0/malli-0.13.0.jar
|
||||
mvxcvi/arrangement/2.1.0/arrangement-2.1.0.jar
|
||||
net/cgrand/macrovich/0.2.1/macrovich-0.2.1.jar
|
||||
net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar
|
||||
nrepl/bencode/1.1.0/bencode-1.1.0.jar
|
||||
@@ -79,6 +82,7 @@ org/clojure/data.priority-map/1.1.0/data.priority-map-1.1.0.jar
|
||||
org/clojure/google-closure-library/0.0-20230227-c7c0a541/google-closure-library-0.0-20230227-c7c0a541.jar
|
||||
org/clojure/google-closure-library-third-party/0.0-20230227-c7c0a541/google-closure-library-third-party-0.0-20230227-c7c0a541.jar
|
||||
org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar
|
||||
org/clojure/test.check/1.1.1/test.check-1.1.1.jar
|
||||
org/clojure/tools.analyzer/1.1.0/tools.analyzer-1.1.0.jar
|
||||
org/clojure/tools.analyzer.jvm/1.2.2/tools.analyzer.jvm-1.2.2.jar
|
||||
org/clojure/tools.cli/1.0.206/tools.cli-1.0.206.jar
|
||||
@@ -99,7 +103,7 @@ org/wildfly/common/wildfly-common/1.5.2.Final/wildfly-common-1.5.2.Final.jar
|
||||
prismatic/schema/1.1.7/schema-1.1.7.jar
|
||||
reagent/reagent/1.2.0/reagent-1.2.0.jar
|
||||
re-com/re-com/2.8.0/re-com-2.8.0.jar
|
||||
refactor-nrepl/refactor-nrepl/3.6.0/refactor-nrepl-3.6.0.jar
|
||||
refactor-nrepl/refactor-nrepl/2.5.0/refactor-nrepl-2.5.0.jar
|
||||
re-frame/re-frame/1.3.0/re-frame-1.3.0.jar
|
||||
re-frisk-remote/re-frisk-remote/1.6.0/re-frisk-remote-1.6.0.jar
|
||||
re-frisk/sente/1.15.0/sente-1.15.0.jar
|
||||
|
||||
@@ -67,9 +67,12 @@ stdenv.mkDerivation {
|
||||
'';
|
||||
buildPhase = ''
|
||||
# Assemble CLASSPATH from available clojure dependencies.
|
||||
# We append 'src' so it can find the local sources.
|
||||
export CLASS_PATH="$(find ${deps.clojure} \
|
||||
-iname '*.jar' | tr '\n' ':')src"
|
||||
# We prepend 'src' so it can find the local sources and prioritize
|
||||
# our own namespaces over dependencies, given that indirect dependencies
|
||||
# can also cause naming conflicts (e.g. prismatic/schema already uses
|
||||
# namespace schema.core).
|
||||
export CLASS_PATH="src:$(find ${deps.clojure} \
|
||||
-iname '*.jar' | tr '\n' ':')"
|
||||
|
||||
# target must be one of the builds defined in shadow-cljs.edn
|
||||
java -cp "$CLASS_PATH" clojure.main \
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
if rg --quiet --multiline '^\(ns.*\n^\s*\(:require\n(^\s*(;|#_).*\n)*(^\s*\[status-im2\.setup\.i18n-resources\W)' src/status_im2/core.cljs; then
|
||||
exit 0
|
||||
elif [ $? -eq 1 ]; then
|
||||
echo "status-im2.setup.i18n-resources must be loaded first (be the first one in ns :require form) in status-im2.core"
|
||||
echo "For more info, check the comment here https://github.com/status-im/status-mobile/pull/17618#discussion_r1361275489"
|
||||
else
|
||||
exit $?
|
||||
fi
|
||||
+18
-2
@@ -9,6 +9,7 @@
|
||||
[cljs-bean "1.9.0"]
|
||||
[com.cognitect/transit-cljs "0.8.280"]
|
||||
[camel-snake-kebab "0.4.3"]
|
||||
[metosin/malli "0.13.0"]
|
||||
|
||||
;; Dev dependencies
|
||||
[refactor-nrepl "2.5.0"]
|
||||
@@ -50,6 +51,7 @@
|
||||
:after-load-async status-im2.setup.hot-reload/reload
|
||||
:build-notify status-im2.setup.hot-reload/build-notify
|
||||
:preloads [re-frisk-remote.preload
|
||||
status-im2.setup.schema-preload
|
||||
;; In order to use component test helpers in the REPL we
|
||||
;; need to preload namespaces that are not normally required
|
||||
;; by production code, such as
|
||||
@@ -71,6 +73,10 @@
|
||||
:warnings {:fn-deprecated false}
|
||||
:closure-defines {re-frame.trace/trace-enabled? true}
|
||||
:source-map false
|
||||
;; This seems to be necessary while using the REPL,
|
||||
;; otherwise sometimes you'll get weird errors when
|
||||
;; instrumenting functions.
|
||||
:static-fns false
|
||||
:infer-externs true}
|
||||
;; if you want to use a real device, set your local ip
|
||||
;; in the SHADOW_HOST env variable to make sure that
|
||||
@@ -104,13 +110,15 @@
|
||||
:output-dir "target/test"
|
||||
:optimizations :simple
|
||||
:target :node-test
|
||||
:dev {:devtools {:preloads [status-im2.setup.schema-preload]}}
|
||||
;; Uncomment line below to `make test-watch` a specific file
|
||||
;; :ns-regexp "status-im2.subs.messages-test$"
|
||||
:main status-im.test-runner/main
|
||||
;; set :ui-driven to true to let shadow-cljs inject node-repl
|
||||
:ui-driven true
|
||||
:closure-defines
|
||||
{status-im2.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
|
||||
{schema.core/throw-on-error? true
|
||||
status-im2.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
|
||||
status-im2.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN"
|
||||
status-im2.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY"
|
||||
status-im2.config/ALCHEMY_ARBITRUM_GOERLI_TOKEN #shadow/env "ALCHEMY_ARBITRUM_GOERLI_TOKEN"
|
||||
@@ -139,9 +147,17 @@
|
||||
:compiler-options {:optimizations :simple
|
||||
:source-map false}}
|
||||
:component-test {:target :npm-module
|
||||
:entries [quo.core-spec status-im2.core-spec]
|
||||
:entries [;; We need to tell shadow-cljs to compile
|
||||
;; the preloads namespace because it will
|
||||
;; be used directly by Jest in the option
|
||||
;; setupFilesAfterEnv.
|
||||
status-im2.setup.schema-preload
|
||||
|
||||
quo.core-spec
|
||||
status-im2.core-spec]
|
||||
:ns-regexp "component-spec$"
|
||||
:output-dir "component-spec"
|
||||
:closure-defines {schema.core/throw-on-error? true}
|
||||
:compiler-options {:warnings-as-errors false
|
||||
:static-fns false
|
||||
:infer-externs true}}}}
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
[quo.components.counter.step.view :as step]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
(defn render
|
||||
[component]
|
||||
(h/render-with-theme-provider component :dark))
|
||||
|
||||
(h/describe "step component"
|
||||
(h/test "default render of step component"
|
||||
(h/render [step/view {} nil])
|
||||
(render [step/view {} nil])
|
||||
(-> (h/expect (h/query-by-label-text :step-counter))
|
||||
(h/is-truthy)))
|
||||
|
||||
(h/test "renders step with a string value"
|
||||
(h/render [step/view {} "1"])
|
||||
(render [step/view {} "1"])
|
||||
(-> (h/expect (h/get-by-text "1"))
|
||||
(h/is-truthy)))
|
||||
|
||||
(h/test "renders step with an integer value"
|
||||
(h/render [step/view {} 1])
|
||||
(render [step/view {} 1])
|
||||
(-> (h/expect (h/get-by-text "1"))
|
||||
(h/is-truthy))))
|
||||
|
||||
@@ -2,10 +2,24 @@
|
||||
(:require
|
||||
[quo.components.counter.step.style :as style]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.theme :as theme]
|
||||
quo.theme
|
||||
[react-native.core :as rn]
|
||||
[schema.core :as schema]
|
||||
[utils.number]))
|
||||
|
||||
(def ?schema
|
||||
[:=>
|
||||
[:catn
|
||||
[:props
|
||||
[:map {:closed true}
|
||||
[:accessibility-label {:optional true} [:maybe :keyword]]
|
||||
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
|
||||
[:in-blur-view? {:optional true} [:maybe :boolean]]
|
||||
[:theme :schema.common/theme]
|
||||
[:type {:optional true} [:enum :active :complete :neutral]]]]
|
||||
[:value [:maybe [:or :string :int]]]]
|
||||
:any])
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [type accessibility-label theme in-blur-view? customization-color]} value]
|
||||
(let [type (or type :neutral)
|
||||
@@ -23,6 +37,9 @@
|
||||
[text/text
|
||||
{:weight :medium
|
||||
:size :label
|
||||
:style {:color (style/text-color type theme)}} label]]))
|
||||
:style {:color (style/text-color type theme)}}
|
||||
label]]))
|
||||
|
||||
(def view (theme/with-theme view-internal))
|
||||
(def view
|
||||
(quo.theme/with-theme
|
||||
(schema/instrument #'view-internal ?schema)))
|
||||
|
||||
@@ -3,22 +3,17 @@
|
||||
[quo.components.icon :as quo.icons]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as theme]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]))
|
||||
|
||||
(def themes
|
||||
{:light {:default colors/neutral-50
|
||||
:success colors/success-50
|
||||
:error colors/danger-50}
|
||||
:dark {:default colors/neutral-40
|
||||
:success colors/success-60
|
||||
:error colors/danger-60}})
|
||||
|
||||
(defn get-color
|
||||
[k]
|
||||
(get-in themes [(theme/get-theme) k]))
|
||||
[k theme]
|
||||
(case k
|
||||
:success (colors/resolve-color :success theme)
|
||||
:error (colors/resolve-color :danger theme)
|
||||
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme)))
|
||||
|
||||
(defn info-message
|
||||
(defn view-internal
|
||||
"[info-message opts \"message\"]
|
||||
opts
|
||||
{:type :default/:success/:error
|
||||
@@ -27,11 +22,11 @@
|
||||
:text-color colors/white ;; text color override
|
||||
:icon-color colors/white ;; icon color override
|
||||
:no-icon-color? false ;; disable tint color for icon"
|
||||
[{:keys [type size icon text-color icon-color no-icon-color? style]} message]
|
||||
[{:keys [type size theme icon text-color icon-color no-icon-color? style accessibility-label]} message]
|
||||
(let [weight (if (= size :default) :regular :medium)
|
||||
icon-size (if (= size :default) 16 12)
|
||||
size (if (= size :default) :paragraph-2 :label)
|
||||
text-color (or text-color (get-color type))
|
||||
text-color (or text-color (get-color type theme))
|
||||
icon-color (or icon-color text-color)]
|
||||
[rn/view
|
||||
{:style (merge {:flex-direction :row
|
||||
@@ -42,7 +37,10 @@
|
||||
:no-color no-icon-color?
|
||||
:size icon-size}]
|
||||
[text/text
|
||||
{:size size
|
||||
:weight weight
|
||||
:style {:color text-color
|
||||
:margin-horizontal 4}} message]]))
|
||||
{:accessibility-label accessibility-label
|
||||
:size size
|
||||
:weight weight
|
||||
:style {:color text-color
|
||||
:margin-horizontal 4}} message]]))
|
||||
|
||||
(def info-message (quo.theme/with-theme view-internal))
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
(h/is-truthy (h/get-by-text "test description")))
|
||||
|
||||
(h/test "renders step component when step-number is valid and type is step"
|
||||
(h/render [list/view
|
||||
{:type :step
|
||||
:step-number 1}])
|
||||
(h/render-with-theme-provider [list/view
|
||||
{:type :step
|
||||
:step-number 1}]
|
||||
:dark)
|
||||
(h/is-truthy (h/get-by-label-text :step-counter)))
|
||||
|
||||
(h/test "renders decription with a context tag component and description after the tag"
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@
|
||||
|
||||
;;;; Counter
|
||||
(def counter quo.components.counter.counter.view/view)
|
||||
(def step quo.components.counter.step.view/view)
|
||||
(def step #'quo.components.counter.step.view/view)
|
||||
|
||||
;;;; Dividers
|
||||
(def divider-label quo.components.dividers.divider-label.view/view)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Schemas
|
||||
|
||||
This document will grow to describe how we use Malli in the project and our
|
||||
conventions. It's still early days 🐪
|
||||
|
||||
## Guidelines
|
||||
### Use var quote `#'` when aliasing instrumented vars
|
||||
|
||||
It is common in this repository to have aliases to vars. For example, `view`
|
||||
referring to `var-internal`, or `quo.core/button` referring to
|
||||
`quo.components.buttons.button.view`.
|
||||
|
||||
If the original var being aliased is instrumented, the alias var MUST [var
|
||||
quote](https://clojure.org/guides/weird_characters#_var_quote) the original var.
|
||||
If you don't do this, the aliased var will not be instrumented.
|
||||
|
||||
```clojure
|
||||
;; bad, view-internal is instrumented, but both aliases don't use a var quote.
|
||||
(schema.core/=> view-internal ?schema)
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
(def button quo.components.buttons.button.view/button)
|
||||
|
||||
;; good
|
||||
(schema.core/=> view-internal ?schema)
|
||||
(def view (quo.theme/with-theme #'view-internal))
|
||||
(def button #'quo.components.buttons.button.view/button)
|
||||
```
|
||||
|
||||
### Prefix schema references with `?`
|
||||
|
||||
Prefix schema bindings and vars with a question mark `?`. This is the naming
|
||||
convention used by malli itself when functions receive instances of schemas and
|
||||
it's an unambiguous way to avoid naming clashes.
|
||||
|
||||
```clojure
|
||||
;; bad
|
||||
(def message-type [:enum ...])
|
||||
|
||||
(defn view
|
||||
[message-type] ; Shadows `message-type` schema
|
||||
(do-something message-type))
|
||||
|
||||
;; good
|
||||
(def ?message-type [:enum ...])
|
||||
|
||||
(defn view
|
||||
[message-type] ; Unambiguous naming strategy
|
||||
(do-something message-type))
|
||||
```
|
||||
|
||||
### Define schemas as functions when needed
|
||||
|
||||
Malli has many utility functions to manipulate schemas as data, and they will
|
||||
automatically check if the schemas were already defined in the registry.
|
||||
|
||||
For schemas we want to conveniently access from the global registry, like
|
||||
`:schema.common/theme`, they must be registered before Malli tries to use them.
|
||||
|
||||
```clojure
|
||||
;; bad, will fail if :schema.common/bar is not registered.
|
||||
(def ^:private ?foo
|
||||
(malli.util/select-keys :schema.common/bar [:id :name]))
|
||||
|
||||
;; good, execution will be delayed until the schema ?foo is correctly registered.
|
||||
(defn- ?foo
|
||||
[]
|
||||
(malli.util/select-keys :schema.common/bar [:id :name]))
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
(ns schema.common
|
||||
(:require
|
||||
[schema.registry :as registry]))
|
||||
|
||||
(def ^:private ?theme
|
||||
[:enum :light :dark])
|
||||
|
||||
(def ^:private ?customization-color
|
||||
[:or :string :keyword])
|
||||
|
||||
(defn register-schemas
|
||||
[]
|
||||
(registry/register ::theme ?theme)
|
||||
(registry/register ::customization-color ?customization-color))
|
||||
@@ -0,0 +1,17 @@
|
||||
(ns schema.core)
|
||||
|
||||
(defmacro =>
|
||||
"Similar to `malli.core/=>`, but instrumentation code will be completely removed
|
||||
in non-debug environments.
|
||||
|
||||
`value` is first transformed via `malli.core/schema` to make sure we fail fast
|
||||
and only register valid schemas."
|
||||
[sym value]
|
||||
`(if ^boolean js/goog.DEBUG
|
||||
(try
|
||||
(malli.core/=> ~sym (malli.core/schema ~value))
|
||||
(catch js/Error e#
|
||||
(taoensso.timbre/error "Failed to instrument function"
|
||||
{:symbol ~sym :error e#})
|
||||
~sym))
|
||||
~sym))
|
||||
@@ -0,0 +1,95 @@
|
||||
(ns schema.core
|
||||
(:require-macros schema.core)
|
||||
(:require
|
||||
[malli.core :as malli]
|
||||
[malli.dev.pretty :as malli.pretty]
|
||||
schema.state
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(goog-define throw-on-error? false)
|
||||
|
||||
(defn- ui-reporter
|
||||
"Prints to STDOUT and signals a schema error should be displayed on screen."
|
||||
[schema-id printer]
|
||||
(let [report (malli.pretty/reporter printer)]
|
||||
(fn [type data]
|
||||
(report type data)
|
||||
(swap! schema.state/errors conj schema-id))))
|
||||
|
||||
(defn- thrower
|
||||
"Similar to `malli.dev.pretty/thrower`, but this reporter uses js/Error instead
|
||||
of ex-info, otherwise invalid schema errors will be printed in one long and
|
||||
incomprehensible line in unit test failures."
|
||||
([] (thrower (malli.pretty/-printer)))
|
||||
([printer]
|
||||
(let [report (malli.pretty/reporter printer)]
|
||||
(fn [type data]
|
||||
(let [message (with-out-str (report type data))]
|
||||
(throw (js/Error. (str "\n" message))))))))
|
||||
|
||||
(defn reporter
|
||||
([]
|
||||
(reporter nil nil))
|
||||
([schema-id]
|
||||
(reporter schema-id nil))
|
||||
([schema-id opts]
|
||||
(let [printer (malli.pretty/-printer
|
||||
(merge {:width 60
|
||||
:print-length 6
|
||||
:print-level 3
|
||||
:print-meta false}
|
||||
opts))]
|
||||
(if throw-on-error?
|
||||
;; The thrower reporter should be used to short-circuit tests on schema errors.
|
||||
(thrower printer)
|
||||
(ui-reporter schema-id printer)))))
|
||||
|
||||
(defn- with-clear-schema-error
|
||||
"Clears current schema error on app if args passed to `f` are valid."
|
||||
[schema-id validate-input f]
|
||||
(fn [& args]
|
||||
(when (validate-input args)
|
||||
(swap! schema.state/errors disj schema-id))
|
||||
(apply f args)))
|
||||
|
||||
(defn instrument
|
||||
"Similar to `malli/-instrument`, but will automatically clear up visible schema
|
||||
errors if `f` is called with valid arguments.
|
||||
|
||||
We use a validator cached by `malli.core/validator`, so that validation is
|
||||
performed once.
|
||||
|
||||
If `?schema` is invalid, then behave like a nop, log the error and return `f`.
|
||||
|
||||
`schema-id` is optional, but should be passed when instrumenting anonymous
|
||||
functions. Consider using a namespaced keyword. For example:
|
||||
|
||||
(schema/instrument ::foo
|
||||
(fn [x] (inc x))
|
||||
[:=> [:cat :int] :int])
|
||||
"
|
||||
([f ?schema]
|
||||
(if ^boolean js/goog.DEBUG
|
||||
(let [schema-id (when (var? f) (symbol f))]
|
||||
(when-not schema-id
|
||||
(log/warn "Anonymous function instrumented without an explicit identifier."
|
||||
{:schema ?schema}))
|
||||
(instrument schema-id f ?schema))
|
||||
f))
|
||||
([schema-id f ?schema]
|
||||
(if ^boolean js/goog.DEBUG
|
||||
(try
|
||||
(let [?schema (malli/schema ?schema)
|
||||
{schema-input :input} (malli/-function-info ?schema)
|
||||
[validate-input _] (malli/-vmap malli/validator [schema-input])]
|
||||
(malli/-instrument {:schema ?schema
|
||||
:report (reporter schema-id
|
||||
{:title (str "Schema error at " schema-id)})}
|
||||
(with-clear-schema-error schema-id validate-input f)))
|
||||
(catch js/Error e
|
||||
(log/error "Failed to instrument function"
|
||||
{:schema-id schema-id
|
||||
:error e
|
||||
:function f})
|
||||
f))
|
||||
f)))
|
||||
@@ -0,0 +1,26 @@
|
||||
(ns schema.registry
|
||||
(:refer-clojure :exclude [merge])
|
||||
(:require
|
||||
[malli.core :as malli]
|
||||
malli.registry))
|
||||
|
||||
(defonce ^:private registry
|
||||
(atom (malli/default-schemas)))
|
||||
|
||||
(defn init-global-registry
|
||||
[]
|
||||
(malli.registry/set-default-registry! (malli.registry/mutable-registry registry)))
|
||||
|
||||
(defn register
|
||||
"Defines a new schema in mutable `registry`.
|
||||
|
||||
We normalize `?schema` by always registering it as a proper instance of
|
||||
`malli.core/Schema` to avoid inconsistencies down the road."
|
||||
[type ?schema]
|
||||
(swap! registry assoc type (malli/schema ?schema))
|
||||
?schema)
|
||||
|
||||
(defn merge
|
||||
[& schemas]
|
||||
(apply swap! registry cljs.core/merge schemas)
|
||||
schemas)
|
||||
@@ -0,0 +1,11 @@
|
||||
(ns schema.state
|
||||
(:require [reagent.core :as reagent]))
|
||||
|
||||
(def errors
|
||||
"Set of schema identifiers, usually namespaced keywords. When the set is empty,
|
||||
no schema errors will be displayed on the app. See `schema.view/view`."
|
||||
(reagent/atom #{}))
|
||||
|
||||
(defn clear-errors
|
||||
[]
|
||||
(reset! errors #{}))
|
||||
@@ -0,0 +1,28 @@
|
||||
(ns schema.style)
|
||||
|
||||
(defn container
|
||||
[{:keys [bottom-inset]}]
|
||||
{:align-items :center
|
||||
:flex-direction :row
|
||||
:background-color "#cc0000"
|
||||
:border-bottom-left-radius 8
|
||||
:border-top-left-radius 8
|
||||
:justify-content :center
|
||||
:padding-vertical 6
|
||||
:padding-right 16
|
||||
:position :absolute
|
||||
:bottom (+ 12 bottom-inset)
|
||||
:right 0
|
||||
:z-index 10000000})
|
||||
|
||||
(def icon
|
||||
{:margin-horizontal 8})
|
||||
|
||||
(def text
|
||||
{:font-family "Inter-SemiBold"
|
||||
:font-size 13
|
||||
:color "#ddd"})
|
||||
|
||||
(def text-suffix
|
||||
{:font-style :italic
|
||||
:font-size 9})
|
||||
@@ -0,0 +1,18 @@
|
||||
(ns schema.view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
schema.state
|
||||
[schema.style :as style]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(when (seq @schema.state/errors)
|
||||
[rn/pressable
|
||||
{:on-press schema.state/clear-errors
|
||||
:style (style/container {:bottom-inset (safe-area/get-bottom)})}
|
||||
[quo/icon :i/close {:size 12 :color "#ddd" :container-style style/icon}]
|
||||
[rn/text {:style style/text}
|
||||
"Schema error(s)"
|
||||
[rn/text {:style (merge style/text style/text-suffix)} " check logs"]]]))
|
||||
@@ -107,14 +107,15 @@
|
||||
[{:keys [db] :as cofx} message]
|
||||
(let [current-chat-id (:current-chat-id db)
|
||||
text (get-in message [:content :text])]
|
||||
{:db (-> db
|
||||
(assoc-in [:chat/inputs current-chat-id :metadata :editing-message]
|
||||
message)
|
||||
(assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil)
|
||||
(update-in [:chat/inputs current-chat-id :metadata]
|
||||
dissoc
|
||||
:sending-image))
|
||||
:dispatch [:mention/to-input-field text current-chat-id]}))
|
||||
{:db (-> db
|
||||
(assoc-in [:chat/inputs current-chat-id :metadata :editing-message]
|
||||
message)
|
||||
(assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil)
|
||||
(update-in [:chat/inputs current-chat-id :metadata]
|
||||
dissoc
|
||||
:sending-image))
|
||||
:dispatch-n [[:chat.ui/set-chat-input-text nil current-chat-id]
|
||||
[:mention/to-input-field text current-chat-id]]}))
|
||||
|
||||
(rf/defn show-contact-request-input
|
||||
"Sets reference to previous chat message and focuses on input"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
status-im.subs.root
|
||||
status-im2.setup.i18n-resources
|
||||
[status-im2.setup.interceptors :as interceptors]
|
||||
[status-im2.setup.schema :as schema]
|
||||
status-im2.subs.root
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
@@ -118,6 +119,7 @@
|
||||
[& args]
|
||||
(reset-test-data!)
|
||||
(interceptors/register-global-interceptors)
|
||||
(schema/setup!)
|
||||
(rf/set-mergeable-keys #{:filters/load-filters
|
||||
:pairing/set-installation-metadata
|
||||
:dispatch-n
|
||||
|
||||
@@ -225,7 +225,9 @@
|
||||
(.. ^js % -nativeEvent -data)])
|
||||
:on-load #(re-frame/dispatch [:browser/loading-started])
|
||||
:on-error #(re-frame/dispatch [:browser/error-occured])
|
||||
:injected-java-script-before-content-loaded (js-res/ethereum-provider (str network-id))}])]
|
||||
:injected-java-script-before-content-loaded (js-res/ethereum-provider (str network-id))
|
||||
;; https://github.com/status-im/status-mobile/issues/17854
|
||||
:allows-inline-media-playback true}])]
|
||||
[navigation
|
||||
{:url url-original
|
||||
:name name
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
:accessibility-label :appearance-settings-button
|
||||
:chevron true
|
||||
:on-press #(re-frame/dispatch [:navigate-to :quo-preview])}])
|
||||
(when config/quo-preview-enabled?
|
||||
[list.item/list-item
|
||||
{:icon :main-icons/appearance
|
||||
:title "Status IM Components"
|
||||
:accessibility-label :status-im-common-components
|
||||
:chevron true
|
||||
:on-press #(re-frame/dispatch [:navigate-to :status-im-preview])}])
|
||||
[list.item/list-item
|
||||
{:icon :main-icons/appearance
|
||||
:title (i18n/label :t/appearance)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
(def test-dir (.mkdtempSync fs test-dir-prefix))
|
||||
|
||||
(def initialized? (atom false))
|
||||
|
||||
(defn signal-received-callback
|
||||
[a]
|
||||
(re-frame/dispatch [:signals/signal-received a]))
|
||||
@@ -22,10 +24,12 @@
|
||||
;; We poll for signals, could not get callback working
|
||||
(defn init!
|
||||
[]
|
||||
(.setSignalEventCallback native-status)
|
||||
(js/setInterval (fn []
|
||||
(.pollSignal native-status signal-received-callback)
|
||||
100)))
|
||||
(when-not @initialized?
|
||||
(.setSignalEventCallback native-status)
|
||||
(reset! initialized? true)
|
||||
(js/setInterval (fn []
|
||||
(.pollSignal native-status signal-received-callback)
|
||||
100))))
|
||||
|
||||
(def status
|
||||
(clj->js
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
(ns status-im2.common.floating-button-page.component-spec
|
||||
(:require [quo.core :as quo]
|
||||
[status-im2.common.floating-button-page.view :as floating-button-page]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
(h/describe "floating button page"
|
||||
(h/test "renders with a header and standard button"
|
||||
(h/render [floating-button-page/view
|
||||
{:header [quo/page-nav
|
||||
{:type :title-description
|
||||
:title "floating button page"
|
||||
:description "press right icon to swap button type"
|
||||
:text-align :left
|
||||
:background :blur
|
||||
:icon-name :i/close}]
|
||||
:footer [quo/button {} "continue"]}])
|
||||
(h/is-truthy (h/get-by-text "continue"))
|
||||
(h/is-truthy (h/get-by-text "floating button page")))
|
||||
|
||||
(h/test "renders with a header and a slide button"
|
||||
(h/render [floating-button-page/view
|
||||
{:header [quo/page-nav
|
||||
{:type :title-description
|
||||
:title "floating button page"
|
||||
:description "press right icon to swap button type"
|
||||
:text-align :left
|
||||
:background :blur
|
||||
:icon-name :i/close}]
|
||||
:footer [quo/slide-button
|
||||
{:track-text "We gotta slide"
|
||||
:track-icon :face-id}]}])
|
||||
(h/is-truthy (h/get-by-text "We gotta slide"))))
|
||||
@@ -0,0 +1,17 @@
|
||||
(ns status-im2.common.floating-button-page.floating-container.style
|
||||
(:require [react-native.safe-area :as safe-area]))
|
||||
|
||||
(defn content-container
|
||||
[blur? keyboard-shown?]
|
||||
(let [margin-bottom (if keyboard-shown? 0 (safe-area/get-bottom))]
|
||||
(cond-> {:margin-top :auto
|
||||
:overflow :hidden
|
||||
:margin-bottom margin-bottom
|
||||
:padding-vertical 12
|
||||
:padding-horizontal 20}
|
||||
blur? (dissoc :padding-vertical :padding-horizontal))))
|
||||
|
||||
(def blur-inner-container
|
||||
{:background-color :transparent ; required, otherwise blur-view will shrink
|
||||
:padding-vertical 12
|
||||
:padding-horizontal 20})
|
||||
@@ -0,0 +1,25 @@
|
||||
(ns status-im2.common.floating-button-page.floating-container.view
|
||||
(:require [quo.theme :as quo.theme]
|
||||
[react-native.blur :as blur]
|
||||
[react-native.core :as rn]
|
||||
[status-im2.common.floating-button-page.floating-container.style :as style]))
|
||||
|
||||
(defn- blur-container
|
||||
[child theme]
|
||||
[blur/view
|
||||
{:blur-amount 12
|
||||
:blur-radius 12
|
||||
:blur-type (quo.theme/theme-value :light :dark theme)}
|
||||
[rn/view {:style style/blur-inner-container}
|
||||
child]])
|
||||
|
||||
(defn view-internal
|
||||
[{:keys [theme on-layout keyboard-shown? blur?]} child]
|
||||
[rn/view
|
||||
{:style (style/content-container blur? keyboard-shown?)
|
||||
:on-layout on-layout}
|
||||
(if blur?
|
||||
[blur-container child theme]
|
||||
child)])
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -0,0 +1,15 @@
|
||||
(ns status-im2.common.floating-button-page.style)
|
||||
|
||||
(def page-container
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0})
|
||||
|
||||
(def keyboard-avoiding-view
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0})
|
||||
@@ -0,0 +1,102 @@
|
||||
(ns status-im2.common.floating-button-page.view
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.floating-button-page.floating-container.view :as floating-container]
|
||||
[status-im2.common.floating-button-page.style :as style]))
|
||||
|
||||
(defn- show-background
|
||||
[{:keys [window-height keyboard-height footer-container-height content-scroll-y
|
||||
content-container-height header-height keyboard-shown?]}]
|
||||
(let [available-space (- window-height
|
||||
(safe-area/get-top)
|
||||
header-height
|
||||
keyboard-height ; Already contains the bottom safe area value
|
||||
footer-container-height)
|
||||
scroll-view-height (- content-container-height content-scroll-y)
|
||||
overlap? (< available-space scroll-view-height)]
|
||||
(and keyboard-shown? overlap?)))
|
||||
|
||||
(defn- set-height-on-layout
|
||||
[ratom]
|
||||
(fn [event]
|
||||
(let [height (oops/oget event "nativeEvent.layout.height")]
|
||||
(reset! ratom height))))
|
||||
|
||||
(defn- init-keyboard-listeners
|
||||
[{:keys [on-did-show]}]
|
||||
(let [keyboard-will-show? (reagent/atom false)
|
||||
keyboard-did-show? (reagent/atom false)
|
||||
add-listener (fn [listener callback]
|
||||
(oops/ocall rn/keyboard "addListener" listener callback))
|
||||
will-show-listener (add-listener "keyboardWillShow"
|
||||
#(reset! keyboard-will-show? true))
|
||||
did-show-listener (add-listener "keyboardDidShow"
|
||||
(fn [e]
|
||||
(reset! keyboard-did-show? true)
|
||||
(when on-did-show (on-did-show e))))
|
||||
will-hide-listener (add-listener "keyboardWillHide"
|
||||
#(reset! keyboard-will-show? false))
|
||||
did-hide-listener (add-listener "keyboardDidHide"
|
||||
#(reset! keyboard-did-show? false))
|
||||
remove-listeners (fn []
|
||||
(doseq [listener [will-show-listener will-hide-listener
|
||||
did-show-listener did-hide-listener]]
|
||||
(oops/ocall listener "remove")))]
|
||||
{:keyboard-will-show? keyboard-will-show?
|
||||
:keyboard-did-show? keyboard-did-show?
|
||||
:remove-listeners remove-listeners}))
|
||||
|
||||
(defn view
|
||||
[{:keys [header footer]} & children]
|
||||
(reagent/with-let [window-height (:height (rn/get-window))
|
||||
footer-container-height (reagent/atom 0)
|
||||
header-height (reagent/atom 0)
|
||||
content-container-height (reagent/atom 0)
|
||||
content-scroll-y (reagent/atom 0)
|
||||
keyboard-height (reagent/atom 0)
|
||||
{:keys [keyboard-will-show?
|
||||
keyboard-did-show?
|
||||
remove-listeners]} (init-keyboard-listeners
|
||||
{:on-did-show
|
||||
(fn [e]
|
||||
(reset! keyboard-height
|
||||
(oops/oget e "endCoordinates.height")))})
|
||||
set-header-height (set-height-on-layout header-height)
|
||||
set-content-container-height (set-height-on-layout content-container-height)
|
||||
set-footer-container-height (set-height-on-layout footer-container-height)
|
||||
set-content-y-scroll (fn [event]
|
||||
(reset! content-scroll-y
|
||||
(oops/oget event "nativeEvent.contentOffset.y")))]
|
||||
(let [keyboard-shown? (if platform/ios? @keyboard-will-show? @keyboard-did-show?)
|
||||
show-background? (show-background {:window-height window-height
|
||||
:footer-container-height @footer-container-height
|
||||
:keyboard-height @keyboard-height
|
||||
:content-scroll-y @content-scroll-y
|
||||
:content-container-height @content-container-height
|
||||
:header-height @header-height
|
||||
:keyboard-shown? keyboard-shown?})]
|
||||
|
||||
[rn/view {:style style/page-container}
|
||||
[rn/view {:on-layout set-header-height}
|
||||
header]
|
||||
[rn/scroll-view
|
||||
{:on-scroll set-content-y-scroll
|
||||
:scroll-event-throttle 64
|
||||
:content-container-style {:flex-grow 1}}
|
||||
(into [rn/view {:on-layout set-content-container-height}]
|
||||
children)]
|
||||
[rn/keyboard-avoiding-view
|
||||
{:style style/keyboard-avoiding-view
|
||||
:keyboard-vertical-offset (if platform/ios? (safe-area/get-top) 0)
|
||||
:pointer-events :box-none}
|
||||
[floating-container/view
|
||||
{:on-layout set-footer-container-height
|
||||
:keyboard-shown? keyboard-shown?
|
||||
:blur? show-background?}
|
||||
footer]]])
|
||||
(finally
|
||||
(remove-listeners))))
|
||||
@@ -78,13 +78,16 @@
|
||||
(fn [can-save?]
|
||||
(if can-save?
|
||||
(keychain/get-credentials
|
||||
(str key-uid "-auth")
|
||||
(str key-uid "-hashed" "-auth")
|
||||
#(callback (if % (oops/oget % "password") auth-method-none)))
|
||||
(callback nil))))))
|
||||
|
||||
(defn save-user-password!
|
||||
[key-uid password]
|
||||
(keychain/save-credentials key-uid key-uid (security/safe-unmask-data password) #()))
|
||||
(keychain/save-credentials key-uid
|
||||
key-uid
|
||||
(security/safe-unmask-data password)
|
||||
#()))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:keychain/get-user-password
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
:on-change-text #(reset! entered-password %)}]
|
||||
(when (not-empty error)
|
||||
[quo/info-message
|
||||
{:type :error
|
||||
:size :default
|
||||
:icon :i/info
|
||||
:style {:margin-top 8}}
|
||||
{:type :error
|
||||
:size :default
|
||||
:icon :i/info
|
||||
:containstyle {:margin-top 8}}
|
||||
(i18n/label :t/oops-wrong-password)])
|
||||
[quo/button
|
||||
{:container-style {:margin-bottom 12 :margin-top 40}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
(ns status-im2.common.standard-authentication.core
|
||||
(:require
|
||||
status-im2.common.standard-authentication.standard-auth.button.view
|
||||
status-im2.common.standard-authentication.standard-auth.slide-button.view))
|
||||
|
||||
(def button status-im2.common.standard-authentication.standard-auth.button.view/view)
|
||||
(def slide-button status-im2.common.standard-authentication.standard-auth.slide-button.view/view)
|
||||
+1
-41
@@ -1,10 +1,6 @@
|
||||
(ns status-im2.common.standard-authentication.standard-auth.view
|
||||
(ns status-im2.common.standard-authentication.standard-auth.authorize
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.touch-id :as biometric]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.standard-authentication.enter-password.view :as enter-password]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.i18n :as i18n]
|
||||
@@ -46,39 +42,3 @@
|
||||
{:on-enter-password on-enter-password
|
||||
:button-icon-left auth-button-icon-left
|
||||
:button-label auth-button-label}])}]))))))
|
||||
|
||||
(defn- view-internal
|
||||
[_]
|
||||
(let [reset-slider? (reagent/atom false)
|
||||
on-close #(reset! reset-slider? true)]
|
||||
(fn [{:keys [biometric-auth?
|
||||
track-text
|
||||
customization-color
|
||||
auth-button-label
|
||||
on-enter-password
|
||||
on-auth-success
|
||||
on-auth-fail
|
||||
auth-button-icon-left
|
||||
size
|
||||
theme
|
||||
blur?
|
||||
container-style]}]
|
||||
[rn/view {:style {:flex 1}}
|
||||
[quo/slide-button
|
||||
{:size size
|
||||
:container-style container-style
|
||||
:customization-color customization-color
|
||||
:on-reset (when @reset-slider? #(reset! reset-slider? false))
|
||||
:on-complete #(authorize {:on-close on-close
|
||||
:auth-button-icon-left auth-button-icon-left
|
||||
:theme theme
|
||||
:blur? blur?
|
||||
:on-enter-password on-enter-password
|
||||
:biometric-auth? biometric-auth?
|
||||
:on-auth-success on-auth-success
|
||||
:on-auth-fail on-auth-fail
|
||||
:auth-button-label auth-button-label})
|
||||
:track-icon (if biometric-auth? :i/face-id :password)
|
||||
:track-text track-text}]])))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -0,0 +1,44 @@
|
||||
(ns status-im2.common.standard-authentication.standard-auth.button.view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.standard-authentication.standard-auth.authorize :as authorize]))
|
||||
|
||||
(defn- view-internal
|
||||
[_]
|
||||
(let [reset-slider? (reagent/atom false)
|
||||
on-close #(reset! reset-slider? true)]
|
||||
(fn [{:keys [biometric-auth?
|
||||
customization-color
|
||||
auth-button-label
|
||||
on-enter-password
|
||||
on-auth-success
|
||||
on-press
|
||||
on-auth-fail
|
||||
auth-button-icon-left
|
||||
size
|
||||
button-label
|
||||
theme
|
||||
blur?
|
||||
container-style
|
||||
icon-left]}]
|
||||
[quo/button
|
||||
{:size size
|
||||
:container-style container-style
|
||||
:customization-color customization-color
|
||||
:icon-left icon-left
|
||||
:on-press (if on-press
|
||||
on-press
|
||||
#(authorize/authorize {:on-close on-close
|
||||
:auth-button-icon-left auth-button-icon-left
|
||||
:theme theme
|
||||
:blur? blur?
|
||||
:on-enter-password on-enter-password
|
||||
:biometric-auth? biometric-auth?
|
||||
:on-auth-success on-auth-success
|
||||
:on-auth-fail on-auth-fail
|
||||
:auth-button-label auth-button-label}))}
|
||||
button-label])))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -0,0 +1,43 @@
|
||||
(ns status-im2.common.standard-authentication.standard-auth.slide-button.view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.standard-authentication.standard-auth.authorize :as authorize]))
|
||||
|
||||
(defn- view-internal
|
||||
[_]
|
||||
(let [reset-slider? (reagent/atom false)
|
||||
on-close #(reset! reset-slider? true)]
|
||||
(fn [{:keys [biometric-auth?
|
||||
track-text
|
||||
customization-color
|
||||
auth-button-label
|
||||
on-enter-password
|
||||
on-auth-success
|
||||
on-auth-fail
|
||||
auth-button-icon-left
|
||||
size
|
||||
theme
|
||||
blur?
|
||||
container-style]}]
|
||||
[rn/view {:style {:flex 1}}
|
||||
[quo/slide-button
|
||||
{:size size
|
||||
:container-style container-style
|
||||
:customization-color customization-color
|
||||
:on-reset (when @reset-slider? #(reset! reset-slider? false))
|
||||
:on-complete #(authorize/authorize {:on-close on-close
|
||||
:auth-button-icon-left auth-button-icon-left
|
||||
:theme theme
|
||||
:blur? blur?
|
||||
:on-enter-password on-enter-password
|
||||
:biometric-auth? biometric-auth?
|
||||
:on-auth-success on-auth-success
|
||||
:on-auth-fail on-auth-fail
|
||||
:auth-button-label auth-button-label})
|
||||
:track-icon (if biometric-auth? :i/face-id :password)
|
||||
:track-text track-text}]])))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -11,7 +11,7 @@
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn edit-message
|
||||
[state]
|
||||
[{:keys [text-value input-ref]}]
|
||||
[rn/view
|
||||
{:style style/container
|
||||
:accessibility-label :edit-message}
|
||||
@@ -30,20 +30,18 @@
|
||||
{:size 24
|
||||
:icon-only? true
|
||||
:accessibility-label :edit-cancel-button
|
||||
:on-press (fn []
|
||||
(utils/cancel-edit-message state)
|
||||
(rf/dispatch [:chat.ui/cancel-message-edit]))
|
||||
:on-press #(utils/cancel-edit-message text-value input-ref)
|
||||
:type :outline}
|
||||
:i/close]])
|
||||
|
||||
(defn- f-view
|
||||
[state]
|
||||
[props]
|
||||
(let [edit (rf/sub [:chats/edit-message])
|
||||
height (reanimated/use-shared-value (if edit constants/edit-container-height 0))]
|
||||
(rn/use-effect #(reanimated/animate height (if edit constants/edit-container-height 0)) [edit])
|
||||
[reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})}
|
||||
(when edit [edit-message state])]))
|
||||
(when edit [edit-message props])]))
|
||||
|
||||
(defn view
|
||||
[state]
|
||||
[:f> f-view state])
|
||||
[props]
|
||||
[:f> f-view props])
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.keyboard :as kb]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
@@ -105,53 +106,62 @@
|
||||
|
||||
(defn use-edit
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value saved-cursor-position]}
|
||||
{:keys [edit]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(let [edit-text (get-in edit [:content :text])
|
||||
text-value-count (count @text-value)]
|
||||
(when (and edit @input-ref)
|
||||
;; A small setTimeout is necessary to ensure the statement is enqueued and will get executed
|
||||
;; ASAP.
|
||||
;; https://github.com/software-mansion/react-native-screens/issues/472
|
||||
(js/setTimeout #(.focus ^js @input-ref) 250)
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text edit-text}))
|
||||
(reset! text-value edit-text)
|
||||
(reset! saved-cursor-position (if (zero? text-value-count)
|
||||
(count edit-text)
|
||||
text-value-count)))))
|
||||
[(:message-id edit)]))
|
||||
{:keys [text-value saved-cursor-position cursor-position]}
|
||||
{:keys [edit input-with-mentions]}
|
||||
messages-list-on-layout-finished?]
|
||||
(let [mention? (some #(= :mention (first %)) (seq input-with-mentions))
|
||||
composer-just-opened? (not @messages-list-on-layout-finished?)]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(let [mention-text (reduce (fn [acc item]
|
||||
(str acc (second item)))
|
||||
""
|
||||
input-with-mentions)
|
||||
edit-text (cond
|
||||
mention? mention-text
|
||||
;; NOTE: using text-value for cases when the user
|
||||
;; leaves the app with an unfinished edit and re-opens
|
||||
;; the chat.
|
||||
(and (seq @text-value) composer-just-opened?)
|
||||
@text-value
|
||||
:else (get-in edit [:content :text]))
|
||||
selection-pos (count edit-text)
|
||||
inject-edit-text (fn []
|
||||
(reset! text-value edit-text)
|
||||
(reset! cursor-position selection-pos)
|
||||
(reset! saved-cursor-position selection-pos)
|
||||
(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:text edit-text}))))]
|
||||
|
||||
(when (and edit @input-ref)
|
||||
;; NOTE: A small setTimeout is necessary to ensure the focus is enqueued and is executed
|
||||
;; ASAP. Check https://github.com/software-mansion/react-native-screens/issues/472
|
||||
;;
|
||||
;; The nested setTimeout is necessary to avoid both `on-focus` and
|
||||
;; `on-content-size-change` handlers triggering the height animation simultaneously, as
|
||||
;; this causes a jump in the
|
||||
;; UI. This way, `on-focus` will trigger first without changing the height, after which
|
||||
;; `on-content-size-change` will animate the height of the input based on the injected
|
||||
;; text.
|
||||
(js/setTimeout #(do (when @messages-list-on-layout-finished? (.focus ^js @input-ref))
|
||||
(reagent/next-tick inject-edit-text))
|
||||
600))))
|
||||
[(:message-id edit)])))
|
||||
|
||||
(defn use-reply
|
||||
[{:keys [input-ref]}
|
||||
{:keys [container-opacity]}
|
||||
{:keys [reply]}]
|
||||
{:keys [reply]}
|
||||
messages-list-on-layout-finished?]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when reply
|
||||
(reanimated/animate container-opacity 1))
|
||||
(when (and reply @input-ref)
|
||||
(js/setTimeout #(.focus ^js @input-ref) 250)))
|
||||
(when (and reply @input-ref @messages-list-on-layout-finished?)
|
||||
(js/setTimeout #(.focus ^js @input-ref) 600)))
|
||||
[(:message-id reply)]))
|
||||
|
||||
(defn edit-mentions
|
||||
[{:keys [input-ref]} {:keys [text-value cursor-position]} {:keys [input-with-mentions]}]
|
||||
(rn/use-effect (fn []
|
||||
(let [input-text (reduce (fn [acc item]
|
||||
(str acc (second item)))
|
||||
""
|
||||
input-with-mentions)]
|
||||
(reset! text-value input-text)
|
||||
(reset! cursor-position (count input-text))
|
||||
(js/setTimeout #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start (count input-text)
|
||||
:end (count
|
||||
input-text)}})))
|
||||
300)))
|
||||
[(some #(= :mention (first %)) (seq input-with-mentions))]))
|
||||
|
||||
(defn update-input-mention
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value]}
|
||||
|
||||
@@ -43,8 +43,7 @@
|
||||
(reset! emoji-kb-extra-height nil))
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])
|
||||
(when @input-ref
|
||||
(.blur ^js @input-ref)))
|
||||
(utils/blur-input input-ref))
|
||||
|
||||
(defn bounce-back
|
||||
[{:keys [height saved-height opacity background-y]}
|
||||
@@ -96,13 +95,13 @@
|
||||
max-height
|
||||
bounded-height
|
||||
saved-height))
|
||||
(when @input-ref ; sheet at min-height, collapse keyboard
|
||||
(.blur ^js @input-ref)))))))
|
||||
; sheet at min-height, collapse keyboard
|
||||
(utils/blur-input input-ref))))))
|
||||
(gesture/on-end (fn []
|
||||
(let [diff (- (reanimated/get-shared-value height)
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
(if @gesture-enabled?
|
||||
(if (>= diff 0)
|
||||
(if (and @expanding? (>= diff 0))
|
||||
(if (> diff constants/drag-threshold)
|
||||
(maximize state animations dimensions)
|
||||
(bounce-back animations dimensions starting-opacity))
|
||||
|
||||
@@ -23,12 +23,14 @@
|
||||
show-floating-scroll-down-button?]
|
||||
(reset! focused? true)
|
||||
(rf/dispatch [:chat.ui/set-input-focused true])
|
||||
(reanimated/animate height (reanimated/get-shared-value last-height))
|
||||
(reanimated/set-shared-value saved-height (reanimated/get-shared-value last-height))
|
||||
(reanimated/animate container-opacity 1)
|
||||
(when (> (reanimated/get-shared-value last-height) (* constants/background-threshold max-height))
|
||||
(reanimated/animate opacity 1)
|
||||
(reanimated/set-shared-value background-y 0))
|
||||
(let [last-height-value (reanimated/get-shared-value last-height)]
|
||||
(reanimated/animate height last-height-value)
|
||||
(reanimated/set-shared-value saved-height last-height-value)
|
||||
(reanimated/animate container-opacity 1)
|
||||
(when (> last-height-value (* constants/background-threshold max-height))
|
||||
(reanimated/animate opacity 1)
|
||||
(reanimated/set-shared-value background-y 0)))
|
||||
|
||||
(js/setTimeout #(reset! lock-selection? false) 300)
|
||||
(when (and (not-empty @text-value) @input-ref)
|
||||
(.setNativeProps ^js @input-ref
|
||||
@@ -72,7 +74,7 @@
|
||||
"Save new text height, expand composer if possible, show background overlay if needed"
|
||||
[event
|
||||
{:keys [maximized? lock-layout? text-value]}
|
||||
{:keys [height saved-height opacity background-y]}
|
||||
{:keys [height saved-height last-height opacity background-y]}
|
||||
{:keys [content-height window-height max-height]}
|
||||
keyboard-shown]
|
||||
(when keyboard-shown
|
||||
@@ -87,8 +89,9 @@
|
||||
max-height)
|
||||
new-height (min new-height max-height)]
|
||||
(reset! content-height content-size)
|
||||
(when (utils/update-height? content-size height max-height maximized?)
|
||||
(when (utils/update-height? content-size height max-height)
|
||||
(reanimated/animate height new-height)
|
||||
(reanimated/set-shared-value last-height new-height)
|
||||
(reanimated/set-shared-value saved-height new-height))
|
||||
(when (= new-height max-height)
|
||||
(reset! maximized? true)
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
[react-native.async-storage :as async-storage]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]))
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]))
|
||||
|
||||
(defn get-kb-height
|
||||
[curr-height default-height]
|
||||
@@ -20,18 +21,24 @@
|
||||
(async-storage/set-item! :kb-default-height (str height)))))
|
||||
|
||||
(defn handle-emoji-kb-ios
|
||||
"Opening emoji KB on iOS while maximized will cause a flicker up and down. This method handles that."
|
||||
"Opening emoji KB on iOS will cause a flicker up and down due to height differences.
|
||||
This method handles that by adding the extra difference between the keyboards. When the input is
|
||||
expanded to a point where the added difference will make the composer go beyond the screen causing a flicker,
|
||||
we're subtracting the difference so it only reaches the allowed max-height. We're not animating these
|
||||
changes to make it appear seamless during transitions between keyboard types when maximized."
|
||||
[event
|
||||
{:keys [emoji-kb-extra-height]}
|
||||
{:keys [text-value]}
|
||||
{:keys [text-value kb-height]}
|
||||
{:keys [height saved-height]}
|
||||
{:keys [max-height]}]
|
||||
(let [start-h (oops/oget event "startCoordinates.height")
|
||||
end-h (oops/oget event "endCoordinates.height")
|
||||
diff (- end-h start-h)
|
||||
max-height-diff (- max-height diff)
|
||||
curr-text @text-value]
|
||||
(if (> (reanimated/get-shared-value height) max-height-diff)
|
||||
(let [start-h (oops/oget event "startCoordinates.height")
|
||||
end-h (oops/oget event "endCoordinates.height")
|
||||
diff (- end-h start-h)
|
||||
max-height-diff (- max-height diff)
|
||||
curr-text @text-value
|
||||
bigger-than-default-kb? (> end-h @kb-height)
|
||||
almost-expanded? (> (reanimated/get-shared-value height) max-height-diff)]
|
||||
(if (and almost-expanded? bigger-than-default-kb? (pos? diff))
|
||||
(do
|
||||
(reanimated/set-shared-value height (- (reanimated/get-shared-value height) diff))
|
||||
(reanimated/set-shared-value saved-height (- (reanimated/get-shared-value saved-height) diff))
|
||||
@@ -58,8 +65,8 @@
|
||||
#(handle-emoji-kb-ios % props state animations dimensions)))
|
||||
(reset! keyboard-hide-listener (.addListener rn/keyboard
|
||||
"keyboardDidHide"
|
||||
#(when (and platform/android? @input-ref)
|
||||
(.blur ^js @input-ref)))))
|
||||
#(when platform/android?
|
||||
(utils/blur-input input-ref)))))
|
||||
|
||||
(defn handle-refocus-emoji-kb-ios
|
||||
[{:keys [saved-emoji-kb-extra-height]}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[status-im2.constants :as constant]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.reply.style :as style]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
[utils.ens.stateofus :as stateofus]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
@@ -85,7 +86,7 @@
|
||||
(defn quoted-message
|
||||
[{:keys [from content-type contentType parsed-text content deleted? deleted-for-me?
|
||||
album-images-count]}
|
||||
in-chat-input? pin? recording-audio?]
|
||||
in-chat-input? pin? recording-audio? input-ref]
|
||||
(let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from])
|
||||
current-public-key (rf/sub [:multiaccount/public-key])
|
||||
content-type (or content-type contentType)
|
||||
@@ -136,7 +137,7 @@
|
||||
{:icon-only? true
|
||||
:size 24
|
||||
:accessibility-label :reply-cancel-button
|
||||
:on-press #(rf/dispatch [:chat.ui/cancel-message-reply])
|
||||
:on-press #(utils/cancel-reply-message input-ref)
|
||||
:type :outline}
|
||||
:i/close])
|
||||
(when (and in-chat-input? recording-audio?)
|
||||
@@ -148,13 +149,13 @@
|
||||
:style style/gradient}])]))
|
||||
|
||||
(defn- f-view
|
||||
[recording?]
|
||||
[recording? input-ref]
|
||||
(let [reply (rf/sub [:chats/reply-message])
|
||||
height (reanimated/use-shared-value (if reply constants/reply-container-height 0))]
|
||||
(rn/use-effect #(reanimated/animate height (if reply constants/reply-container-height 0)) [reply])
|
||||
[reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})}
|
||||
(when reply [quoted-message reply true false recording?])]))
|
||||
(when reply [quoted-message reply true false recording? input-ref])]))
|
||||
|
||||
(defn view
|
||||
[{:keys [recording?]}]
|
||||
[:f> f-view @recording?])
|
||||
[{:keys [recording?]} input-ref]
|
||||
[:f> f-view @recording? input-ref])
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
(max min-v (min v max-v)))
|
||||
|
||||
(defn update-height?
|
||||
[content-size height max-height maximized?]
|
||||
(when-not @maximized?
|
||||
(let [diff (Math/abs (- content-size (reanimated/get-shared-value height)))]
|
||||
(and (not= (reanimated/get-shared-value height) max-height)
|
||||
(> diff constants/content-change-threshold)))))
|
||||
[content-size height max-height]
|
||||
(let [diff (Math/abs (- content-size (reanimated/get-shared-value height)))]
|
||||
(and (not= (reanimated/get-shared-value height) max-height)
|
||||
(> diff constants/content-change-threshold))))
|
||||
|
||||
(defn show-top-gradient?
|
||||
[y lines max-lines gradient-opacity focused?]
|
||||
@@ -100,10 +99,28 @@
|
||||
(not reply?)
|
||||
(not audio?)))
|
||||
|
||||
(defn blur-input
|
||||
[input-ref]
|
||||
(when @input-ref
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(.blur ^js @input-ref)))
|
||||
|
||||
(defn cancel-reply-message
|
||||
[input-ref]
|
||||
(js/setTimeout #(blur-input input-ref) 100)
|
||||
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height])
|
||||
(rf/dispatch [:chat.ui/cancel-message-reply]))
|
||||
|
||||
(defn cancel-edit-message
|
||||
[{:keys [text-value]}]
|
||||
[text-value input-ref]
|
||||
(reset! text-value "")
|
||||
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height]))
|
||||
;; NOTE: adding a timeout to assure the input is blurred on the next tick
|
||||
;; after the `text-value` was cleared. Otherwise the height will be calculated
|
||||
;; with the old `text-value`, leading to wrong composer height after blur.
|
||||
(js/setTimeout #(blur-input input-ref) 100)
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text ""}))
|
||||
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height])
|
||||
(rf/dispatch [:chat.ui/cancel-message-edit]))
|
||||
|
||||
(defn count-lines
|
||||
[s]
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
blur-height
|
||||
opacity
|
||||
background-y
|
||||
theme]} props state]
|
||||
theme
|
||||
messages-list-on-layout-finished?]} props state]
|
||||
(let [{:keys [chat-screen-loaded?]
|
||||
:as subscriptions} (utils/init-subs)
|
||||
content-height (reagent/atom (or (:input-content-height ; Actual text height
|
||||
@@ -73,10 +74,9 @@
|
||||
animations
|
||||
dimensions
|
||||
subscriptions)
|
||||
(effects/use-edit props state subscriptions)
|
||||
(effects/use-reply props animations subscriptions)
|
||||
(effects/use-edit props state subscriptions messages-list-on-layout-finished?)
|
||||
(effects/use-reply props animations subscriptions messages-list-on-layout-finished?)
|
||||
(effects/update-input-mention props state subscriptions)
|
||||
(effects/edit-mentions props state subscriptions)
|
||||
(effects/link-previews props state animations subscriptions)
|
||||
(effects/use-images props state animations subscriptions)
|
||||
[:<>
|
||||
@@ -98,8 +98,10 @@
|
||||
[sub-view/bar]
|
||||
(when chat-screen-loaded?
|
||||
[:<>
|
||||
[reply/view state]
|
||||
[edit/view state]])
|
||||
[reply/view state (:input-ref props)]
|
||||
[edit/view
|
||||
{:text-value (:text-value state)
|
||||
:input-ref (:input-ref props)}]])
|
||||
[reanimated/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press (fn []
|
||||
@@ -147,11 +149,13 @@
|
||||
subscriptions]]]]]))
|
||||
|
||||
(defn composer
|
||||
[{:keys [insets scroll-to-bottom-fn show-floating-scroll-down-button?]}]
|
||||
[{:keys [insets scroll-to-bottom-fn show-floating-scroll-down-button?
|
||||
messages-list-on-layout-finished?]}]
|
||||
(let [window-height (:height (rn/get-window))
|
||||
theme (quo.theme/use-theme-value)
|
||||
opacity (reanimated/use-shared-value 0)
|
||||
background-y (reanimated/use-shared-value (- window-height)) ; Y position of background overlay
|
||||
background-y (reanimated/use-shared-value (- window-height)) ; Y position of background
|
||||
; overlay
|
||||
blur-height (reanimated/use-shared-value (+ constants/composer-default-height
|
||||
(:bottom insets)))
|
||||
extra-params {:insets insets
|
||||
@@ -161,7 +165,8 @@
|
||||
:blur-height blur-height
|
||||
:opacity opacity
|
||||
:background-y background-y
|
||||
:theme theme}
|
||||
:theme theme
|
||||
:messages-list-on-layout-finished? messages-list-on-layout-finished?}
|
||||
props (utils/init-non-reactive-state)
|
||||
state (utils/init-reactive-state)]
|
||||
[rn/view (when platform/ios? {:style {:z-index 1}})
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
|
||||
(defn f-messages-list-content
|
||||
[{:keys [chat insets scroll-y content-height cover-bg-color keyboard-shown? inner-state-atoms
|
||||
big-name-visible? animate-topbar-opacity? composer-active?
|
||||
big-name-visible? animate-topbar-opacity? composer-active? messages-list-on-layout-finished?
|
||||
on-end-reached? animate-topbar-name?]}]
|
||||
(rn/use-effect (fn []
|
||||
(if (and (not @on-end-reached?)
|
||||
@@ -412,6 +412,15 @@
|
||||
;;TODO(rasom) https://github.com/facebook/react-native/issues/30034
|
||||
:inverted (when platform/ios? true)
|
||||
:on-layout (fn [e]
|
||||
;; FIXME: the 1s timeout is to assure all effects with
|
||||
;; timeouts that depend on the value are considered.
|
||||
;; Hacky, but we're heavily relying on timeouts in the
|
||||
;; composer and need to react to differently (e.g.
|
||||
;; inside effects/use-edit) when the chat has just
|
||||
;; opened and the subsequent times.
|
||||
(js/setTimeout #(reset! messages-list-on-layout-finished?
|
||||
true)
|
||||
1000)
|
||||
(let [layout-height (oops/oget e
|
||||
"nativeEvent.layout.height")]
|
||||
(reset! messages-view-height layout-height)))
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
(defn f-chat
|
||||
[{:keys [show-floating-scroll-down-button? animate-topbar-name?
|
||||
big-name-visible? animate-topbar-opacity? on-end-reached?]
|
||||
big-name-visible? animate-topbar-opacity? on-end-reached? messages-list-on-layout-finished?]
|
||||
:as inner-state-atoms}]
|
||||
(let [insets (safe-area/get-insets)
|
||||
scroll-y (reanimated/use-shared-value 0)
|
||||
@@ -53,18 +53,19 @@
|
||||
:keyboard-vertical-offset (- (:bottom insets))}
|
||||
|
||||
[list.view/message-list-content-view
|
||||
{:chat chat
|
||||
:insets insets
|
||||
:scroll-y scroll-y
|
||||
:content-height content-height
|
||||
:cover-bg-color :turquoise
|
||||
:keyboard-shown? keyboard-shown
|
||||
:inner-state-atoms inner-state-atoms
|
||||
:animate-topbar-name? animate-topbar-name?
|
||||
:big-name-visible? big-name-visible?
|
||||
:animate-topbar-opacity? animate-topbar-opacity?
|
||||
:composer-active? focused?
|
||||
:on-end-reached? on-end-reached?}]
|
||||
{:chat chat
|
||||
:insets insets
|
||||
:scroll-y scroll-y
|
||||
:content-height content-height
|
||||
:cover-bg-color :turquoise
|
||||
:keyboard-shown? keyboard-shown
|
||||
:inner-state-atoms inner-state-atoms
|
||||
:animate-topbar-name? animate-topbar-name?
|
||||
:big-name-visible? big-name-visible?
|
||||
:animate-topbar-opacity? animate-topbar-opacity?
|
||||
:composer-active? focused?
|
||||
:on-end-reached? on-end-reached?
|
||||
:messages-list-on-layout-finished? messages-list-on-layout-finished?}]
|
||||
|
||||
[messages.navigation/navigation-view
|
||||
{:scroll-y scroll-y
|
||||
@@ -86,7 +87,8 @@
|
||||
[:f> composer.view/composer
|
||||
{:insets insets
|
||||
:scroll-to-bottom-fn list.view/scroll-to-bottom
|
||||
:show-floating-scroll-down-button? show-floating-scroll-down-button?}]
|
||||
:show-floating-scroll-down-button? show-floating-scroll-down-button?
|
||||
:messages-list-on-layout-finished? messages-list-on-layout-finished?}]
|
||||
[contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]))]))
|
||||
|
||||
(defn chat
|
||||
@@ -99,5 +101,6 @@
|
||||
:animate-topbar-name? (reagent/atom false)
|
||||
:big-name-visible? (reagent/atom :initial-render)
|
||||
:animate-topbar-opacity? (reagent/atom false)
|
||||
:on-end-reached? (reagent/atom false)}]
|
||||
:on-end-reached? (reagent/atom false)
|
||||
:messages-list-on-layout-finished? (reagent/atom false)}]
|
||||
[:f> f-chat inner-state-atoms]))
|
||||
|
||||
@@ -42,26 +42,31 @@
|
||||
community-id %])
|
||||
:on-error #(log/error "failed to request to join community" community-id %)}]})
|
||||
|
||||
(defn request-to-join
|
||||
[{:keys [db]} [{:keys [community-id password]}]]
|
||||
(let [pub-key (get-in db [:profile/profile :public-key])
|
||||
addresses-to-reveal []]
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "wakuext_generateJoiningCommunityRequestsForSigning"
|
||||
:params [pub-key community-id addresses-to-reveal]
|
||||
:on-success [:communities/sign-data community-id password]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]}))
|
||||
|
||||
;; Event to be called to request to join a community.
|
||||
;; This event will generate the data to be signed and then call the sign-data event.
|
||||
;; This is the only event that should be called from the UI.
|
||||
(rf/reg-event-fx :communities/request-to-join
|
||||
(fn [{:keys [db]} [{:keys [community-id password]}]]
|
||||
(let [pub-key (get-in db [:profile/profile :public-key])
|
||||
addresses-to-reveal []]
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "wakuext_generateJoiningCommunityRequestsForSigning"
|
||||
:params [pub-key community-id addresses-to-reveal]
|
||||
:on-success [:communities/sign-data community-id password]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]})))
|
||||
(rf/reg-event-fx :communities/request-to-join request-to-join)
|
||||
|
||||
(rf/reg-event-fx :communities/sign-data
|
||||
(fn [_ [community-id password sign-params]]
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "wakuext_signData"
|
||||
:params [(map #(assoc % :password password) sign-params)]
|
||||
:on-success [:communities/request-to-join-with-signatures community-id]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]}))
|
||||
(defn sign-data
|
||||
[_ [community-id password sign-params]]
|
||||
(let [addresses-to-reveal (map :account sign-params)]
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "wakuext_signData"
|
||||
:params [(map #(assoc % :password password) sign-params)]
|
||||
:on-success [:communities/request-to-join-with-signatures community-id addresses-to-reveal]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]}))
|
||||
|
||||
(rf/reg-event-fx :communities/sign-data sign-data)
|
||||
|
||||
(rf/reg-event-fx :communities/requested-to-join-error
|
||||
(fn [{:keys [db]} [community-id error]]
|
||||
@@ -71,11 +76,19 @@
|
||||
:event :communities/requested-to-join-error})
|
||||
{:db (assoc-in db [:password-authentication :error] error)}))
|
||||
|
||||
(rf/reg-event-fx :communities/request-to-join-with-signatures
|
||||
(fn [_ [community-id signatures]]
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "wakuext_requestToJoinCommunity"
|
||||
:params [{:communityId community-id :signatures signatures}]
|
||||
:js-response true
|
||||
:on-success [:communities/requested-to-join]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]}))
|
||||
(defn request-to-join-with-signatures
|
||||
[_ [community-id addresses-to-reveal signatures]]
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "wakuext_requestToJoinCommunity"
|
||||
:params [{:communityId community-id
|
||||
:signatures signatures
|
||||
:addressesToReveal addresses-to-reveal
|
||||
;; NOTE: At least one airdrop address is required.
|
||||
;; This is a temporary solution while the address
|
||||
;; selection feature is not implemented in mobile.
|
||||
:airdropAddress (first addresses-to-reveal)}]
|
||||
:js-response true
|
||||
:on-success [:communities/requested-to-join]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]})
|
||||
|
||||
(rf/reg-event-fx :communities/request-to-join-with-signatures request-to-join-with-signatures)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
(ns status-im2.contexts.communities.overview.events-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[native-module.core :as native-module]
|
||||
[status-im2.contexts.communities.overview.events :as sut]))
|
||||
|
||||
(def password (native-module/sha3 "password123"))
|
||||
(def community-id "0x99")
|
||||
(def account-pub-key "0x1")
|
||||
|
||||
(deftest request-to-join-test
|
||||
(let [cofx {:db {:profile/profile {:public-key account-pub-key}}}
|
||||
expected {:fx [[:json-rpc/call
|
||||
[{:method "wakuext_generateJoiningCommunityRequestsForSigning"
|
||||
:params [account-pub-key community-id []]
|
||||
:on-success [:communities/sign-data community-id password]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]}]
|
||||
(is (= expected
|
||||
(sut/request-to-join cofx
|
||||
[{:community-id community-id
|
||||
:password password}])))))
|
||||
|
||||
(deftest sign-data-test
|
||||
(let [cofx {:db {}}
|
||||
sign-params [{:data "123" :account account-pub-key}
|
||||
{:data "456" :account "0x2"}]
|
||||
addresses-to-reveal [account-pub-key "0x2"]
|
||||
expected {:fx
|
||||
[[:json-rpc/call
|
||||
[{:method "wakuext_signData"
|
||||
:params [[{:data "123" :account account-pub-key :password password}
|
||||
{:data "456" :account "0x2" :password password}]]
|
||||
:on-success [:communities/request-to-join-with-signatures
|
||||
community-id addresses-to-reveal]
|
||||
:on-error [:communities/requested-to-join-error community-id]}]]]}]
|
||||
(is (= expected
|
||||
(sut/sign-data cofx [community-id password sign-params])))))
|
||||
|
||||
(deftest request-to-join-with-signatures-test
|
||||
(let [cofx {:db {}}
|
||||
addresses-to-reveal [account-pub-key "0x2"]
|
||||
signatures ["11111" "222222"]
|
||||
expected {:fx [[:json-rpc/call
|
||||
[{:method "wakuext_requestToJoinCommunity"
|
||||
:params [{:communityId community-id
|
||||
:signatures signatures
|
||||
:addressesToReveal addresses-to-reveal
|
||||
:airdropAddress "0x1"}]
|
||||
:js-response true
|
||||
:on-success [:communities/requested-to-join]
|
||||
:on-error [:communities/requested-to-join-error
|
||||
community-id]}]]]}]
|
||||
(is (= expected
|
||||
(sut/request-to-join-with-signatures cofx [community-id addresses-to-reveal signatures])))))
|
||||
@@ -255,7 +255,7 @@
|
||||
(defn community-content
|
||||
[community]
|
||||
(rf/dispatch [:communities/check-all-community-channels-permissions (:id community)])
|
||||
(fn [{:keys [name description joined images tags color id] :as community}
|
||||
(fn [{:keys [name description joined images tags color id token-permissions] :as community}
|
||||
pending?
|
||||
{:keys [on-category-layout
|
||||
collapsed?
|
||||
@@ -273,12 +273,14 @@
|
||||
:last-item-style style/last-community-tag
|
||||
:container-style style/community-tag-container}])
|
||||
[join-community community pending?]]
|
||||
[channel-list-component
|
||||
{:on-category-layout on-category-layout
|
||||
:community-id id
|
||||
:community-color color
|
||||
:on-first-channel-height-changed on-first-channel-height-changed}
|
||||
(add-handlers-to-categorized-chats id chats-by-category joined)]])))
|
||||
(when (or (and (seq token-permissions) joined)
|
||||
(empty? token-permissions))
|
||||
[channel-list-component
|
||||
{:on-category-layout on-category-layout
|
||||
:community-id id
|
||||
:community-color color
|
||||
:on-first-channel-height-changed on-first-channel-height-changed}
|
||||
(add-handlers-to-categorized-chats id chats-by-category joined)])])))
|
||||
|
||||
(defn sticky-category-header
|
||||
[_]
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
(ns status-im2.contexts.onboarding.enable-biometrics.view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im2.common.biometric.events :as biometric]
|
||||
[status-im2.common.parallax.view :as parallax]
|
||||
[status-im2.common.parallax.whitelist :as whitelist]
|
||||
[status-im2.common.resources :as resources]
|
||||
[status-im2.common.standard-authentication.core :as standard-auth]
|
||||
[status-im2.contexts.onboarding.enable-biometrics.style :as style]
|
||||
[status-im2.navigation.state :as state]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
|
||||
(defn page-title
|
||||
[]
|
||||
@@ -21,22 +26,29 @@
|
||||
:description-accessibility-label :enable-biometrics-sub-title}])
|
||||
|
||||
(defn enable-biometrics-buttons
|
||||
[insets]
|
||||
[insets theme]
|
||||
(let [supported-biometric-type (rf/sub [:biometric/supported-type])
|
||||
bio-type-label (biometric/get-label-by-type supported-biometric-type)
|
||||
profile-color (:color (rf/sub [:onboarding-2/profile]))]
|
||||
profile-color (or (:color (rf/sub [:onboarding-2/profile]))
|
||||
(rf/sub [:profile/customization-color]))
|
||||
syncing-results? (= :syncing-results @state/root-id)]
|
||||
[rn/view {:style (style/buttons insets)}
|
||||
[quo/button
|
||||
{:accessibility-label :enable-biometrics-button
|
||||
:on-press #(rf/dispatch [:onboarding-2/enable-biometrics])
|
||||
:icon-left :i/face-id
|
||||
:customization-color profile-color}
|
||||
(i18n/label :t/biometric-enable-button {:bio-type-label bio-type-label})]
|
||||
(merge
|
||||
{:size 40
|
||||
:accessibility-label :enable-biometrics-button
|
||||
:icon-left :i/face-id
|
||||
:customization-color profile-color
|
||||
:button-label (i18n/label :t/biometric-enable-button {:bio-type-label bio-type-label})}
|
||||
{:on-press #(rf/dispatch [:onboarding-2/sync-enable-biometrics])})]
|
||||
[quo/button
|
||||
{:accessibility-label :maybe-later-button
|
||||
:background :blur
|
||||
:type :grey
|
||||
:on-press #(rf/dispatch [:onboarding-2/create-account-and-login])
|
||||
:on-press #(rf/dispatch (if syncing-results?
|
||||
[:navigate-to-within-stack
|
||||
[:enable-notifications :enable-biometrics]]
|
||||
[:onboarding-2/create-account-and-login]))
|
||||
:container-style {:margin-top 12}}
|
||||
(i18n/label :t/maybe-later)]]))
|
||||
|
||||
@@ -55,12 +67,19 @@
|
||||
:style (style/page-illustration width)
|
||||
:source (resources/get-image :biometrics)}]))
|
||||
|
||||
(defn enable-biometrics
|
||||
[]
|
||||
(defn f-enable-biometrics
|
||||
[{:keys [theme]}]
|
||||
(let [insets (safe-area/get-insets)]
|
||||
[rn/view {:style (style/page-container insets)}
|
||||
[page-title]
|
||||
(if whitelist/whitelisted?
|
||||
[enable-biometrics-parallax]
|
||||
[enable-biometrics-simple])
|
||||
[enable-biometrics-buttons insets]]))
|
||||
[enable-biometrics-buttons insets theme]]))
|
||||
|
||||
|
||||
(defn- internale-enable-biometrics
|
||||
[params]
|
||||
[:f> f-enable-biometrics params])
|
||||
|
||||
(def view (quo.theme/with-theme internale-enable-biometrics))
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
[quo/page-nav
|
||||
{:background :blur
|
||||
:icon-name :i/arrow-left
|
||||
:on-press #(rf/dispatch [:navigate-back-within-stack :new-to-status])}]
|
||||
:on-press #(rf/dispatch [:navigate-back-within-stack :enable-biometrics])}]
|
||||
[page-title]
|
||||
[rn/view {:style style/page-illustration}
|
||||
[quo/text
|
||||
|
||||
@@ -35,11 +35,29 @@
|
||||
{:biometric/authenticate {:on-success #(rf/dispatch [:onboarding-2/biometrics-done])
|
||||
:on-fail #(rf/dispatch [:onboarding-2/biometrics-fail %])}})
|
||||
|
||||
(rf/defn sync-enable-biometrics
|
||||
{:events [:onboarding-2/sync-enable-biometrics]}
|
||||
[{:keys [db]} password]
|
||||
{:db (-> db
|
||||
(assoc-in [:onboarding-2/profile :syncing?] true))
|
||||
:biometric/authenticate {:on-success #(rf/dispatch [:onboarding-2/biometrics-done])
|
||||
:on-fail #(rf/dispatch [:onboarding-2/biometrics-fail %])}})
|
||||
|
||||
(rf/defn navigate-to-enable-notifications
|
||||
{:events [:onboarding-2/navigate-to-enable-notifications]}
|
||||
[{:keys [db]}]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:db (dissoc db :onboarding-2/profile)
|
||||
:dispatch [:navigate-to-within-stack [:enable-notifications :enable-biometrics]]}))
|
||||
|
||||
(rf/defn biometrics-done
|
||||
{:events [:onboarding-2/biometrics-done]}
|
||||
[{:keys [db]}]
|
||||
{:db (assoc-in db [:onboarding-2/profile :auth-method] constants/auth-method-biometric)
|
||||
:dispatch [:onboarding-2/create-account-and-login]})
|
||||
(let [syncing? (get-in db [:onboarding-2/profile :syncing?])]
|
||||
{:db (assoc-in db [:onboarding-2/profile :auth-method] constants/auth-method-biometric)
|
||||
:dispatch (if syncing?
|
||||
[:onboarding-2/finalize-setup]
|
||||
[:onboarding-2/create-account-and-login])}))
|
||||
|
||||
(rf/defn biometrics-fail
|
||||
{:events [:onboarding-2/biometrics-fail]}
|
||||
@@ -83,6 +101,14 @@
|
||||
[:navigate-to-within-stack [:enable-biometrics :new-to-status]]
|
||||
[:onboarding-2/create-account-and-login])}))
|
||||
|
||||
(rf/defn navigate-to-enable-biometrics
|
||||
{:events [:onboarding-2/navigate-to-enable-biometrics]}
|
||||
[{:keys [db]}]
|
||||
(let [supported-type (:biometric/supported-type db)]
|
||||
{:dispatch (if supported-type
|
||||
[:open-modal :enable-biometrics]
|
||||
[:open-modal :enable-notifications])}))
|
||||
|
||||
(rf/defn seed-phrase-entered
|
||||
{:events [:onboarding-2/seed-phrase-entered]}
|
||||
[_ seed-phrase on-error]
|
||||
@@ -118,16 +144,20 @@
|
||||
(rf/defn onboarding-new-account-finalize-setup
|
||||
{:events [:onboarding-2/finalize-setup]}
|
||||
[{:keys [db]}]
|
||||
(let [masked-password (get-in db [:onboarding-2/profile :password])
|
||||
(let [syncing? (get-in db [:onboarding-2/profile :syncing?])
|
||||
masked-password (get-in db [:onboarding-2/profile :password])
|
||||
key-uid (get-in db [:profile/profile :key-uid])
|
||||
biometric-enabled? (= (get-in db [:onboarding-2/profile :auth-method])
|
||||
constants/auth-method-biometric)]
|
||||
(cond-> {:db (assoc db :onboarding-2/generated-keys? true)}
|
||||
biometric-enabled?
|
||||
(assoc :keychain/save-password-and-auth-method
|
||||
{:key-uid key-uid
|
||||
{:key-uid (if syncing? (str key-uid "-hashed") key-uid)
|
||||
:masked-password masked-password
|
||||
:on-success #(log/debug "successfully saved biometric")
|
||||
:on-success (fn []
|
||||
(if syncing?
|
||||
(rf/dispatch [:onboarding-2/navigate-to-enable-notifications])
|
||||
(log/error "successfully saved biometrics")))
|
||||
:on-error #(log/error "failed to save biometrics"
|
||||
{:key-uid key-uid
|
||||
:error %})}))))
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
{:on-press (fn []
|
||||
(when on-press
|
||||
(on-press))
|
||||
(rf/dispatch [:open-modal :enable-notifications]))
|
||||
(rf/dispatch [:onboarding-2/navigate-to-enable-biometrics]))
|
||||
:accessibility-label :continue-button
|
||||
:customization-color profile-color
|
||||
:container-style style/continue-button}
|
||||
|
||||
@@ -42,11 +42,19 @@
|
||||
{:db (assoc-in db [:profile/login :processing] true)
|
||||
::login [key-uid (native-module/sha3 (security/safe-unmask-data password))]}))
|
||||
|
||||
(rf/defn biometry-login
|
||||
{:events [:profile.login/biometry-login]}
|
||||
[{:keys [db]}]
|
||||
(let [{:keys [key-uid password]} (:profile/login db)]
|
||||
{:db (assoc-in db [:profile/login :processing] true)
|
||||
::login [key-uid (security/safe-unmask-data password)]}))
|
||||
|
||||
(rf/defn login-local-paired-user
|
||||
{:events [:profile.login/local-paired-user]}
|
||||
[{:keys [db]}]
|
||||
(let [{:keys [key-uid password]} (get-in db [:syncing :profile])]
|
||||
{::login [key-uid password]}))
|
||||
{:db (assoc-in db [:onboarding-2/profile :password] (security/mask-data password))
|
||||
::login [key-uid password]}))
|
||||
|
||||
(rf/defn redirect-to-root
|
||||
[{:keys [db] :as cofx}]
|
||||
@@ -158,14 +166,25 @@
|
||||
{:on-success #(rf/dispatch [:profile.login/biometric-success])
|
||||
:on-faile #(rf/dispatch [:profile.login/biometric-auth-fail])}})))
|
||||
|
||||
(rf/defn biometric-auth-success
|
||||
{:events [:profile.login/biometric-success]}
|
||||
;; TODO store the password in the hash keychain
|
||||
(rf/defn backwards-compatible-biometric-auth-success
|
||||
{:events [:profile.login/backwards-compatible-biometric-success]}
|
||||
[{:keys [db] :as cofx}]
|
||||
(let [key-uid (get-in db [:profile/login :key-uid])]
|
||||
(keychain/get-user-password cofx
|
||||
key-uid
|
||||
#(rf/dispatch [:profile.login/get-user-password-success %]))))
|
||||
|
||||
(rf/defn biometric-auth-success
|
||||
{:events [:profile.login/biometric-success]}
|
||||
[{:keys [db] :as cofx}]
|
||||
(let [key-uid (get-in db [:profile/login :key-uid])]
|
||||
(keychain/get-user-password cofx
|
||||
(str key-uid "-hashed")
|
||||
#(if (nil? %)
|
||||
(rf/dispatch [:profile.login/backwards-compatible-biometric-success])
|
||||
(rf/dispatch [:profile.login/get-user-password-success %])))))
|
||||
|
||||
;; result of :keychain/get-auth-method above
|
||||
(rf/defn get-user-password-success
|
||||
{:events [:profile.login/get-user-password-success]}
|
||||
@@ -175,7 +194,7 @@
|
||||
cofx
|
||||
{:db (assoc-in db [:profile/login :password] password)}
|
||||
(navigation/init-root :progress)
|
||||
(login))))
|
||||
(biometry-login))))
|
||||
|
||||
(rf/defn biometric-auth-fail
|
||||
{:events [:profile.login/biometric-auth-fail]}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [theme]}]
|
||||
[{:keys [theme title]}]
|
||||
(let [logged-in? (rf/sub [:multiaccount/logged-in?])
|
||||
has-profiles? (boolean (rf/sub [:profile/profiles-overview]))
|
||||
root (if has-profiles? :profiles :intro)
|
||||
light? (= theme :light)]
|
||||
[quo/page-nav
|
||||
{:type :title
|
||||
:title "quo components preview"
|
||||
:title title
|
||||
:text-align :left
|
||||
:icon-name :i/close
|
||||
:right-side [{:icon-name (if light? :i/dark :i/light)
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
:descriptor descriptor
|
||||
:blur? (:in-blur-view? @state)
|
||||
:show-blur-background? (:in-blur-view? @state)}
|
||||
[quo/step @state (:value @state)]])))
|
||||
[quo/step (dissoc @state :value) (:value @state)]])))
|
||||
|
||||
@@ -487,7 +487,7 @@
|
||||
(defn- main-screen
|
||||
[]
|
||||
[:<>
|
||||
[common/navigation-bar]
|
||||
[common/navigation-bar {:title "Quo components preview"}]
|
||||
[rn/scroll-view {:style (style/main)}
|
||||
(for [category (sort screens-categories)]
|
||||
^{:key (first category)}
|
||||
|
||||
@@ -317,10 +317,10 @@
|
||||
children)])
|
||||
|
||||
(defn- f-preview-container
|
||||
[{:keys [state descriptor blur? blur-dark-only?
|
||||
[{:keys [title state descriptor blur? blur-dark-only?
|
||||
component-container-style
|
||||
blur-container-style blur-view-props blur-height show-blur-background?]
|
||||
:or {blur-height 200}}
|
||||
:or {blur-height 200 title "quo component"}}
|
||||
& children]
|
||||
(let [theme (quo.theme/use-theme-value)]
|
||||
(rn/use-effect (fn []
|
||||
@@ -332,7 +332,7 @@
|
||||
[rn/view
|
||||
{:style {:top (safe-area/get-top)
|
||||
:flex 1}}
|
||||
[common/navigation-bar]
|
||||
[common/navigation-bar {:title title}]
|
||||
[rn/scroll-view
|
||||
{:style (style/panel-basic)
|
||||
:shows-vertical-scroll-indicator false}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
(ns status-im2.contexts.status-im-preview.common.floating-button-page.style
|
||||
(:require [quo.foundations.colors :as colors]
|
||||
[react-native.safe-area :as safe-area]))
|
||||
|
||||
(defn container
|
||||
[]
|
||||
{:flex 1
|
||||
:margin-top (safe-area/get-top)})
|
||||
|
||||
(def background-image
|
||||
{:position :absolute
|
||||
:top (- (safe-area/get-top))
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 200})
|
||||
|
||||
(defn page-content
|
||||
[height]
|
||||
{:flex 1
|
||||
:height height
|
||||
:overflow :hidden
|
||||
:background-color (colors/resolve-color :army 30)})
|
||||
@@ -0,0 +1,55 @@
|
||||
(ns status-im2.contexts.status-im-preview.common.floating-button-page.view
|
||||
(:require [quo.core :as quo]
|
||||
[re-frame.core :as rf]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.floating-button-page.view :as floating-button-page]
|
||||
[status-im2.common.resources :as resources]
|
||||
[status-im2.contexts.status-im-preview.common.floating-button-page.style :as style]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [content-height (reagent/atom 450)
|
||||
slide? (reagent/atom false)]
|
||||
(fn []
|
||||
[rn/view {:style (style/container)}
|
||||
(when-not @slide?
|
||||
[rn/image
|
||||
{:style style/background-image
|
||||
:source (resources/get-mock-image :dark-blur-bg)}])
|
||||
[floating-button-page/view
|
||||
{:header [quo/page-nav
|
||||
{:type :title-description
|
||||
:title "floating button page"
|
||||
:description "press right icon to swap button type"
|
||||
:text-align :left
|
||||
:right-side [{:icon-name :i/swap
|
||||
:on-press #(swap! slide? not)}]
|
||||
:background :blur
|
||||
:icon-name :i/close
|
||||
:on-press #(rf/dispatch [:navigate-back])}]
|
||||
:footer (if @slide?
|
||||
[quo/slide-button
|
||||
{:track-text "We gotta slide"
|
||||
:track-icon :face-id
|
||||
:container-style {:z-index 2}
|
||||
:customization-color :blue
|
||||
:on-complete #(js/alert "button slid")}
|
||||
"Save address"]
|
||||
[quo/button
|
||||
{:container-style {:z-index 2}
|
||||
:on-press #(js/alert "button pressed")}
|
||||
"Save address"])}
|
||||
[rn/view {:style (style/page-content @content-height)}
|
||||
[quo/text {:size :heading-1} "Page Content"]
|
||||
[quo/input
|
||||
{:auto-focus true
|
||||
:value ""}]
|
||||
[quo/button
|
||||
{:type :outline
|
||||
:on-press #(swap! content-height (fn [v] (+ v 10)))}
|
||||
"increase height"]
|
||||
[quo/button
|
||||
{:type :outline
|
||||
:on-press #(swap! content-height (fn [v] (- v 10)))}
|
||||
"decrease height"]]]])))
|
||||
@@ -0,0 +1,59 @@
|
||||
(ns status-im2.contexts.status-im-preview.main
|
||||
(:refer-clojure :exclude [filter])
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.quo-preview.common :as common]
|
||||
[status-im2.contexts.status-im-preview.common.floating-button-page.view :as floating-button-page]
|
||||
[status-im2.contexts.status-im-preview.style :as style]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def screens-categories
|
||||
{:common [{:name :floating-button-page
|
||||
:component floating-button-page/view}]})
|
||||
|
||||
(defn- category-view
|
||||
[]
|
||||
(let [open? (reagent/atom false)
|
||||
on-press #(swap! open? not)]
|
||||
(fn [category]
|
||||
[rn/view {:style {:margin-vertical 8}}
|
||||
[quo/dropdown
|
||||
{:type :grey
|
||||
:state (if @open? :active :default)
|
||||
:on-press on-press}
|
||||
(name (key category))]
|
||||
(when @open?
|
||||
(for [{category-name :name} (val category)]
|
||||
^{:key category-name}
|
||||
[quo/button
|
||||
{:type :outline
|
||||
:container-style {:margin-vertical 8}
|
||||
:on-press #(rf/dispatch [:navigate-to category-name])}
|
||||
(name category-name)]))])))
|
||||
|
||||
(defn- main-screen
|
||||
[]
|
||||
[:<>
|
||||
[common/navigation-bar {:title "Status IM components"}]
|
||||
[rn/scroll-view {:style (style/main)}
|
||||
(for [category (sort screens-categories)]
|
||||
^{:key (first category)}
|
||||
[category-view category])]])
|
||||
|
||||
(def screens
|
||||
(->> screens-categories
|
||||
(map val)
|
||||
flatten
|
||||
(map (fn [subcategory]
|
||||
(update-in subcategory
|
||||
[:options :topBar]
|
||||
merge
|
||||
{:visible false})))))
|
||||
|
||||
(def main-screens
|
||||
[{:name :status-im-preview
|
||||
:options {:topBar {:visible false}
|
||||
:insets {:top? true}}
|
||||
:component main-screen}])
|
||||
@@ -0,0 +1,10 @@
|
||||
(ns status-im2.contexts.status-im-preview.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]))
|
||||
|
||||
(defn main
|
||||
[]
|
||||
{:flex 1
|
||||
:padding-bottom 8
|
||||
:padding-horizontal 16
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-90)})
|
||||
@@ -8,7 +8,7 @@
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.qr-codes.view :as qr-codes]
|
||||
[status-im2.common.resources :as resources]
|
||||
[status-im2.common.standard-authentication.standard-auth.view :as standard-auth]
|
||||
[status-im2.common.standard-authentication.core :as standard-auth]
|
||||
[status-im2.contexts.syncing.setup-syncing.style :as style]
|
||||
[status-im2.contexts.syncing.utils :as sync-utils]
|
||||
[utils.datetime :as datetime]
|
||||
@@ -119,7 +119,7 @@
|
||||
(i18n/label :t/copy-qr)]])
|
||||
(when-not (sync-utils/valid-connection-string? @code)
|
||||
[rn/view {:style style/standard-auth}
|
||||
[standard-auth/view
|
||||
[standard-auth/slide-button
|
||||
{:blur? true
|
||||
:size :size-40
|
||||
:track-text (i18n/label :t/slide-to-reveal-code)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
(ns status-im2.contexts.wallet.add-address-to-watch.component-spec
|
||||
(:require
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im2.contexts.wallet.add-address-to-watch.view :as add-address-to-watch]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
(defn setup-subs
|
||||
[subscriptions]
|
||||
(doseq [keyval subscriptions]
|
||||
(re-frame/reg-sub
|
||||
(key keyval)
|
||||
(fn [_] (val keyval)))))
|
||||
|
||||
(h/describe "select address for watch only account"
|
||||
(h/test "validation messages show for already used addressed"
|
||||
(setup-subs {:wallet/scanned-address nil
|
||||
:wallet/addresses (set
|
||||
["0x12E838Ae1f769147b12956485dc56e57138f3AC8"
|
||||
"0x22E838Ae1f769147b12956485dc56e57138f3AC8"])
|
||||
:profile/customization-color :blue})
|
||||
(h/render [add-address-to-watch/view])
|
||||
(h/is-falsy (h/query-by-label-text :error-message))
|
||||
(h/fire-event :change-text
|
||||
(h/get-by-label-text :add-address-to-watch)
|
||||
"0x12E838Ae1f769147b12956485dc56e57138f3AC8")
|
||||
(h/is-truthy (h/get-by-translation-text :address-already-in-use))))
|
||||
|
||||
(h/test "validation messages show for invalid address"
|
||||
(setup-subs {:wallet/scanned-address nil
|
||||
:wallet/addresses (set
|
||||
["0x12E838Ae1f769147b12956485dc56e57138f3AC8"
|
||||
"0x22E838Ae1f769147b12956485dc56e57138f3AC8"])
|
||||
:profile/customization-color :blue})
|
||||
(h/render [add-address-to-watch/view])
|
||||
(h/is-falsy (h/query-by-label-text :error-message))
|
||||
(h/fire-event :change-text (h/get-by-label-text :add-address-to-watch) "0x12E838Ae1f769147b")
|
||||
(h/is-truthy (h/get-by-translation-text :invalid-address)))
|
||||
|
||||
@@ -3,18 +3,7 @@
|
||||
(def container
|
||||
{:flex 1})
|
||||
|
||||
(def input
|
||||
{:margin-right 12
|
||||
:flex 1})
|
||||
|
||||
(def data-item
|
||||
{:margin-horizontal 20
|
||||
:padding-vertical 8
|
||||
:padding-horizontal 12})
|
||||
|
||||
(defn button-container
|
||||
[bottom]
|
||||
{:position :absolute
|
||||
:bottom (+ bottom 12)
|
||||
:left 20
|
||||
:right 20})
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[clojure.string :as string]
|
||||
[quo.core :as quo]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[re-frame.core :as re-frame]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
@@ -14,13 +13,12 @@
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn- view-internal
|
||||
(defn view
|
||||
[]
|
||||
(let [{:keys [address]} (rf/sub [:get-screen-params])
|
||||
number-of-accounts (count (rf/sub [:profile/wallet-accounts]))
|
||||
account-name (reagent/atom (i18n/label :t/default-account-name
|
||||
{:number (inc number-of-accounts)}))
|
||||
address-title (i18n/label :t/watch-address)
|
||||
account-color (reagent/atom (rand-nth colors/account-colors))
|
||||
account-emoji (reagent/atom (emoji-picker.utils/random-emoji))
|
||||
on-change-name #(reset! account-name %)
|
||||
@@ -40,8 +38,9 @@
|
||||
:on-change-name on-change-name
|
||||
:on-change-color on-change-color
|
||||
:on-change-emoji on-change-emoji
|
||||
:watch-only? true
|
||||
:bottom-action? true
|
||||
:bottom-action-label :t/create-account
|
||||
:bottom-action-label :t/add-watched-address
|
||||
:bottom-action-props {:customization-color @account-color
|
||||
:disabled? (string/blank? @account-name)
|
||||
:on-press #(re-frame/dispatch [:navigate-to
|
||||
@@ -51,11 +50,9 @@
|
||||
:right-icon :i/advanced
|
||||
:icon-right? true
|
||||
:emoji @account-emoji
|
||||
:title address-title
|
||||
:title (i18n/label :t/watched-address)
|
||||
:subtitle address
|
||||
:status :default
|
||||
:size :default
|
||||
:container-style style/data-item
|
||||
:on-press #(js/alert "To be implemented")}]]])))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
|
||||
@@ -5,13 +5,24 @@
|
||||
:margin-top 12
|
||||
:margin-bottom 20})
|
||||
|
||||
(def input-container
|
||||
{:flex-direction :row
|
||||
:padding-horizontal 20
|
||||
:align-items :flex-end})
|
||||
|
||||
(def button-container
|
||||
{:position :absolute
|
||||
:bottom 22
|
||||
:left 20
|
||||
:right 20})
|
||||
|
||||
(def scan
|
||||
{:align-self
|
||||
:flex-end})
|
||||
|
||||
(def input
|
||||
{:flex 1
|
||||
:margin-right 12})
|
||||
|
||||
(def input-container
|
||||
{:flex-direction :row
|
||||
:margin-horizontal 20})
|
||||
|
||||
(def info-message
|
||||
{:margin-top 8
|
||||
:margin-left 20})
|
||||
|
||||
@@ -2,51 +2,114 @@
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[re-frame.core :as re-frame]
|
||||
[react-native.clipboard :as clipboard]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.wallet.add-address-to-watch.style :as style]
|
||||
[status-im2.contexts.wallet.common.validation :as validation]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn view-internal
|
||||
(defn validate-message
|
||||
[addresses]
|
||||
(fn [s]
|
||||
(cond
|
||||
(or (= s nil) (= s "")) nil
|
||||
(contains? addresses s) (i18n/label :t/address-already-in-use)
|
||||
(not (or (validation/eth-address? s)
|
||||
(validation/ens-name? s))) (i18n/label :t/invalid-address)
|
||||
:else nil)))
|
||||
|
||||
(defn- address-input
|
||||
[{:keys [input-value validation-msg validate
|
||||
clear-input]}]
|
||||
(let [scanned-address (rf/sub [:wallet/scanned-address])
|
||||
empty-input? (and (string/blank? @input-value)
|
||||
(string/blank? scanned-address))
|
||||
|
||||
on-change-text (fn [new-text]
|
||||
(reset! validation-msg (validate new-text))
|
||||
(reset! input-value new-text)
|
||||
(when (and scanned-address (not= scanned-address new-text))
|
||||
(rf/dispatch [:wallet/clean-scanned-address])))
|
||||
paste-on-input #(clipboard/get-string
|
||||
(fn [clipboard-text]
|
||||
(on-change-text clipboard-text)))]
|
||||
(rn/use-effect (fn []
|
||||
(when-not (string/blank? scanned-address)
|
||||
(on-change-text scanned-address)))
|
||||
[scanned-address])
|
||||
[rn/view
|
||||
{:style style/input-container}
|
||||
[quo/input
|
||||
{:accessibility-label :add-address-to-watch
|
||||
:placeholder (i18n/label :t/address-placeholder)
|
||||
:container-style style/input
|
||||
:label (i18n/label :t/eth-or-ens)
|
||||
:auto-capitalize :none
|
||||
:multiline? true
|
||||
:on-clear clear-input
|
||||
:return-key-type :done
|
||||
:clearable? (not empty-input?)
|
||||
:on-change-text on-change-text
|
||||
:button (when empty-input?
|
||||
{:on-press paste-on-input
|
||||
:text (i18n/label :t/paste)})
|
||||
:value @input-value}]
|
||||
[quo/button
|
||||
{:type :outline
|
||||
:on-press (fn []
|
||||
(rn/dismiss-keyboard!)
|
||||
(rf/dispatch [:open-modal :scan-address]))
|
||||
:container-style style/scan
|
||||
:size 40
|
||||
:icon-only? true}
|
||||
:i/scan]]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [input-value (reagent/atom "")
|
||||
(let [addresses (rf/sub [:wallet/addresses])
|
||||
input-value (reagent/atom nil)
|
||||
validate (validate-message (set addresses))
|
||||
validation-msg (reagent/atom (validate
|
||||
@input-value))
|
||||
clear-input (fn []
|
||||
(reset! input-value nil)
|
||||
(reset! validation-msg nil)
|
||||
(rf/dispatch [:wallet/clean-scanned-address]))
|
||||
customization-color (rf/sub [:profile/customization-color])]
|
||||
(rf/dispatch [:wallet/clean-scanned-address])
|
||||
(fn []
|
||||
[rn/view
|
||||
{:style {:flex 1}}
|
||||
[quo/page-nav
|
||||
{:type :no-title
|
||||
:icon-name :i/close
|
||||
:on-press #(rf/dispatch [:navigate-back])}]
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:wallet/clean-scanned-address])
|
||||
(rf/dispatch [:navigate-back]))}]
|
||||
[quo/text-combinations
|
||||
{:container-style style/header-container
|
||||
:title (i18n/label :t/add-address)
|
||||
:description (i18n/label :t/enter-eth)}]
|
||||
[rn/view {:style style/input-container}
|
||||
[quo/input
|
||||
{:label (i18n/label :t/eth-or-ens)
|
||||
:button {:on-press (fn [] (clipboard/get-string #(reset! input-value %)))
|
||||
:text (i18n/label :t/paste)}
|
||||
:placeholder (str "0x123abc... " (string/lower-case (i18n/label :t/or)) " bob.eth")
|
||||
:container-style {:margin-right 12
|
||||
:flex 1}
|
||||
:weight :monospace
|
||||
:on-change-text #(reset! input-value %)
|
||||
:default-value @input-value}]
|
||||
[quo/button
|
||||
{:icon-only? true
|
||||
:type :outline} :i/scan]]
|
||||
[:f> address-input
|
||||
{:input-value input-value
|
||||
:validate validate
|
||||
:validation-msg validation-msg
|
||||
:clear-input clear-input}]
|
||||
(when @validation-msg
|
||||
[quo/info-message
|
||||
{:accessibility-label :error-message
|
||||
:size :default
|
||||
:icon :i/info
|
||||
:type :error
|
||||
:style style/info-message}
|
||||
@validation-msg])
|
||||
[quo/button
|
||||
{:customization-color customization-color
|
||||
:disabled? (clojure.string/blank? @input-value)
|
||||
:on-press #(re-frame/dispatch [:navigate-to
|
||||
:confirm-address-to-watch
|
||||
{:address @input-value}])
|
||||
:disabled? (string/blank? @input-value)
|
||||
:on-press #(rf/dispatch [:navigate-to
|
||||
:confirm-address-to-watch
|
||||
{:address @input-value}])
|
||||
:container-style style/button-container}
|
||||
(i18n/label :t/continue)]])))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
(ns status-im2.contexts.wallet.common.screen-base.create-or-edit-account.view
|
||||
(:require [quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im2.constants :as constants]
|
||||
@@ -8,12 +7,12 @@
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn- view-internal
|
||||
(defn view
|
||||
[{:keys [margin-top? page-nav-right-side account-name account-color account-emoji on-change-name
|
||||
on-change-color
|
||||
on-change-emoji on-focus on-blur section-label bottom-action?
|
||||
bottom-action-label bottom-action-props
|
||||
custom-bottom-action]} & children]
|
||||
custom-bottom-action watch-only?]} & children]
|
||||
(let [{:keys [top bottom]} (safe-area/get-insets)
|
||||
margin-top (if (false? margin-top?) 0 top)
|
||||
{window-width :width} (rn/get-window)]
|
||||
@@ -36,7 +35,7 @@
|
||||
{:customization-color account-color
|
||||
:size 80
|
||||
:emoji account-emoji
|
||||
:type :default}]
|
||||
:type (if watch-only? :watch-only :default)}]
|
||||
[quo/button
|
||||
{:size 32
|
||||
:type :grey
|
||||
@@ -81,5 +80,3 @@
|
||||
:type :primary}
|
||||
bottom-action-props)
|
||||
(i18n/label bottom-action-label)])])]))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
(ns status-im2.contexts.wallet.common.sheets.account-options.view
|
||||
(:require [quo.core :as quo]
|
||||
[quo.foundations.colors :as colors]
|
||||
quo.theme
|
||||
[react-native.clipboard :as clipboard]
|
||||
[react-native.core :as rn]
|
||||
[status-im2.contexts.wallet.common.sheets.account-options.style :as style]
|
||||
[status-im2.contexts.wallet.common.temp :as temp]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(defn- view-internal
|
||||
[{:keys [theme]}]
|
||||
(let [{:keys [name color emoji address]} (rf/sub [:wallet/current-viewing-account])]
|
||||
[:<>
|
||||
[quo/drawer-top
|
||||
@@ -26,7 +29,13 @@
|
||||
:on-press #(rf/dispatch [:navigate-to :wallet-edit-account])}
|
||||
{:icon :i/copy
|
||||
:accessibility-label :copy-address
|
||||
:label (i18n/label :t/copy-address)}
|
||||
:label (i18n/label :t/copy-address)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:toasts/upsert
|
||||
{:icon :i/correct
|
||||
:icon-color (colors/resolve-color :success theme)
|
||||
:text (i18n/label :t/address-copied)}])
|
||||
(clipboard/set-string address))}
|
||||
{:icon :i/share
|
||||
:accessibility-label :share-account
|
||||
:label (i18n/label :t/share-account)}
|
||||
@@ -42,3 +51,5 @@
|
||||
{:data temp/other-accounts
|
||||
:render-fn (fn [account] [quo/account-item {:account-props account}])
|
||||
:style {:margin-horizontal 8}}]]))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
(ns status-im2.contexts.wallet.common.validation
|
||||
(:require [status-im2.constants :as constants]))
|
||||
|
||||
(defn ens-name? [s] (re-find constants/regx-ens s))
|
||||
(defn eth-address? [s] (re-find constants/regx-address s))
|
||||
@@ -7,7 +7,7 @@
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.standard-authentication.standard-auth.view :as standard-auth]
|
||||
[status-im2.common.standard-authentication.core :as standard-auth]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.emoji-picker.utils :as emoji-picker.utils]
|
||||
[status-im2.contexts.wallet.common.utils :as utils]
|
||||
@@ -114,7 +114,7 @@
|
||||
{:list-type :settings
|
||||
:label (i18n/label :t/origin)
|
||||
:data (get-keypair-data primary-name @derivation-path @account-color)}]
|
||||
[standard-auth/view
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-create-account)
|
||||
:customization-color @account-color
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
(fn []
|
||||
(let [scanned-address (rf/sub [:wallet/scanned-address])
|
||||
send-address (rf/sub [:wallet/send-address])
|
||||
valid-ens-or-address? (boolean (rf/sub [:wallet/valid-ens-or-address?]))]
|
||||
valid-ens-or-address? (rf/sub [:wallet/valid-ens-or-address?])]
|
||||
[quo/address-input
|
||||
{:on-focus #(reset! input-focused? true)
|
||||
:on-blur #(reset! input-focused? false)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
(ns status-im2.core-spec
|
||||
(:require
|
||||
[status-im2.common.floating-button-page.component-spec]
|
||||
[status-im2.contexts.chat.messages.content.audio.component-spec]
|
||||
[status-im2.contexts.communities.actions.community-options.component-spec]
|
||||
[status-im2.contexts.wallet.add-address-to-watch.component-spec]
|
||||
[status-im2.contexts.wallet.create-account.edit-derivation-path.component-spec]))
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
(ns status-im2.integration-test.chat
|
||||
(ns status-im2.integration-test.chat-test
|
||||
(:require
|
||||
[cljs.test :refer [deftest is]]
|
||||
[day8.re-frame.test :as rf-test]
|
||||
@@ -16,7 +16,7 @@
|
||||
"0x0402905bed83f0bbf993cee8239012ccb1a8bc86907ead834c1e38476a0eda71414eed0e25f525f270592a2eebb01c9119a4ed6429ba114e51f5cb0a28dae1adfd")
|
||||
|
||||
(deftest one-to-one-chat-test
|
||||
(h/log-headline one-to-one-chat-test)
|
||||
(h/log-headline :one-to-one-chat-test)
|
||||
(rf-test/run-test-async
|
||||
(h/with-app-initialized
|
||||
(h/with-account
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
(ns status-im2.integration-test.community
|
||||
(ns status-im2.integration-test.community-test
|
||||
(:require [cljs.test :refer [deftest]]
|
||||
[day8.re-frame.test :as rf-test]
|
||||
[re-frame.core :as rf]
|
||||
+2
-5
@@ -1,4 +1,4 @@
|
||||
(ns status-im2.integration-test.core
|
||||
(ns status-im2.integration-test.core-test
|
||||
(:require
|
||||
[cljs.test :refer [deftest]]
|
||||
[day8.re-frame.test :as rf-test]
|
||||
@@ -8,17 +8,14 @@
|
||||
status-im.subs.root
|
||||
[status-im.utils.test :as utils.test]
|
||||
status-im2.events
|
||||
status-im2.integration-test.chat
|
||||
status-im2.integration-test.wallet
|
||||
status-im2.navigation.core
|
||||
status-im2.subs.root
|
||||
[test-helpers.integration :as h]))
|
||||
|
||||
(utils.test/init!)
|
||||
|
||||
(deftest initialize-app-test
|
||||
(h/log-headline :initialize-app-test)
|
||||
(rf-test/run-test-async
|
||||
(utils.test/init!)
|
||||
(rf/dispatch [:app-started])
|
||||
(rf-test/wait-for
|
||||
;; use initialize-view because it has the longest avg. time and
|
||||
+8
-4
@@ -1,6 +1,6 @@
|
||||
(ns status-im2.integration-test.wallet
|
||||
(ns status-im2.integration-test.wallet-test
|
||||
(:require
|
||||
[cljs.test :refer [deftest is]]
|
||||
[cljs.test :refer [is]]
|
||||
[clojure.string :as string]
|
||||
[day8.re-frame.test :as rf-test]
|
||||
[re-frame.core :as rf]
|
||||
@@ -12,7 +12,9 @@
|
||||
status-im2.subs.root
|
||||
[test-helpers.integration :as h]))
|
||||
|
||||
(deftest create-wallet-account-test
|
||||
;; Workaround to skip test. Switch to `deftest` when test is fixed.
|
||||
(defn create-wallet-account-test
|
||||
[]
|
||||
(h/log-headline :create-wallet-account-test)
|
||||
(rf-test/run-test-async
|
||||
(h/with-app-initialized
|
||||
@@ -24,7 +26,9 @@
|
||||
(h/logout)
|
||||
(rf-test/wait-for [::logout/logout-method]))))))
|
||||
|
||||
(deftest back-up-seed-phrase-test
|
||||
;; Workaround to skip test. Switch to `deftest` when test is fixed.
|
||||
(defn back-up-seed-phrase-test
|
||||
[]
|
||||
(h/log-headline :back-up-seed-phrase-test)
|
||||
(rf-test/run-test-async
|
||||
(h/with-app-initialized
|
||||
@@ -32,6 +32,7 @@
|
||||
[status-im2.contexts.shell.activity-center.view :as activity-center]
|
||||
[status-im2.contexts.shell.jump-to.view :as shell]
|
||||
[status-im2.contexts.shell.share.view :as share]
|
||||
[status-im2.contexts.status-im-preview.main :as status-im-preview]
|
||||
[status-im2.contexts.syncing.find-sync-code.view :as find-sync-code]
|
||||
[status-im2.contexts.syncing.how-to-pair.view :as how-to-pair]
|
||||
[status-im2.contexts.syncing.scan-sync-code-page.view :as scan-sync-code-page]
|
||||
@@ -46,7 +47,7 @@
|
||||
[status-im2.contexts.wallet.create-account.select-keypair.view :as wallet-select-keypair]
|
||||
[status-im2.contexts.wallet.create-account.view :as wallet-create-account]
|
||||
[status-im2.contexts.wallet.edit-account.view :as wallet-edit-account]
|
||||
[status-im2.contexts.wallet.saved-address.view :as wallet-saved-address]
|
||||
[status-im2.contexts.wallet.saved-addresses.view :as wallet-saved-addresses]
|
||||
[status-im2.contexts.wallet.scan-account.view :as scan-address]
|
||||
[status-im2.contexts.wallet.send.select-address.view :as wallet-select-address]
|
||||
[status-im2.navigation.options :as options]
|
||||
@@ -161,11 +162,15 @@
|
||||
:component create-password/create-password}
|
||||
|
||||
{:name :enable-biometrics
|
||||
:options {:theme :dark
|
||||
:layout options/onboarding-transparent-layout
|
||||
:animations transitions/push-animations-for-transparent-background
|
||||
:popGesture false}
|
||||
:component enable-biometrics/enable-biometrics}
|
||||
:options {:theme :dark
|
||||
:layout options/onboarding-transparent-layout
|
||||
:animations (merge transitions/new-to-status-modal-animations
|
||||
transitions/push-animations-for-transparent-background)
|
||||
:popGesture false
|
||||
:modalPresentationStyle :overCurrentContext
|
||||
:hardwareBackButton {:dismissModalOnPress false
|
||||
:popStackOnPress false}}
|
||||
:component enable-biometrics/view}
|
||||
|
||||
{:name :generating-keys
|
||||
:options {:theme :dark
|
||||
@@ -279,8 +284,8 @@
|
||||
:options {:insets {:top? true}}
|
||||
:component wallet-create-account/view}
|
||||
|
||||
{:name :wallet-saved-address
|
||||
:component wallet-saved-address/view}
|
||||
{:name :wallet-saved-addresses
|
||||
:component wallet-saved-addresses/view}
|
||||
|
||||
{:name :wallet-select-address
|
||||
:options {:modalPresentationStyle :overCurrentContext}
|
||||
@@ -301,4 +306,11 @@
|
||||
quo.preview/screens)
|
||||
|
||||
(when config/quo-preview-enabled?
|
||||
quo.preview/main-screens)))
|
||||
quo.preview/main-screens)
|
||||
|
||||
(when config/quo-preview-enabled?
|
||||
status-im-preview/screens)
|
||||
|
||||
(when config/quo-preview-enabled?
|
||||
status-im-preview/main-screens)))
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
schema.view
|
||||
[status-im.bottom-sheet.sheets :as bottom-sheets-old]
|
||||
[status-im.ui.screens.popover.views :as popover]
|
||||
[status-im.ui.screens.profile.visibility-status.views :as visibility-status-views]
|
||||
@@ -71,7 +72,9 @@
|
||||
[bottom-sheet-screen/view {:content component}]
|
||||
[component])]
|
||||
(when js/goog.DEBUG
|
||||
[reloader/reload-view])]))))
|
||||
[:<>
|
||||
[reloader/reload-view]
|
||||
[schema.view/view]])]))))
|
||||
|
||||
(def bottom-sheet
|
||||
(reagent/reactify-component
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
(:require
|
||||
["react-native" :refer (DevSettings LogBox)]
|
||||
[react-native.platform :as platform]
|
||||
[status-im2.setup.schema :as schema]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
;; Ignore all logs, because there are lots of temporary warnings when developing and hot reloading
|
||||
@@ -45,10 +46,11 @@
|
||||
:group-chats/extract-membership-signature
|
||||
:utils/dispatch-later
|
||||
:json-rpc/call})
|
||||
|
||||
(when (and js/goog.DEBUG platform/ios? DevSettings)
|
||||
;;on Android this method doesn't work
|
||||
(when-let [nm (.-_nativeModule DevSettings)]
|
||||
;;there is a bug in RN, so we have to enable it first and then disable
|
||||
(.setHotLoadingEnabled ^js nm true)
|
||||
(js/setTimeout #(.setHotLoadingEnabled ^js nm false) 1000))))
|
||||
(when ^:boolean js/goog.DEBUG
|
||||
(schema/setup!)
|
||||
(when (and platform/ios? DevSettings)
|
||||
;;on Android this method doesn't work
|
||||
(when-let [nm (.-_nativeModule DevSettings)]
|
||||
;;there is a bug in RN, so we have to enable it first and then disable
|
||||
(.setHotLoadingEnabled ^js nm true)
|
||||
(js/setTimeout #(.setHotLoadingEnabled ^js nm false) 1000)))))
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
(:require
|
||||
[re-frame.core :as re-frame]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]))
|
||||
[reagent.core :as reagent]
|
||||
schema.state
|
||||
[status-im2.setup.schema :as schema]))
|
||||
|
||||
(defonce cnt (reagent/atom 0))
|
||||
(defonce reload-locked? (atom false))
|
||||
@@ -19,6 +21,8 @@
|
||||
(reset! visible true)
|
||||
(reset! label "reloading UI")
|
||||
(re-frame/clear-subscription-cache!)
|
||||
(schema/setup!)
|
||||
(schema.state/clear-errors)
|
||||
(swap! cnt inc))
|
||||
|
||||
(defn before-reload
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
(ns status-im2.setup.schema
|
||||
(:require
|
||||
[malli.core :as malli]
|
||||
[malli.dev.cljs :as malli.dev]
|
||||
[malli.dev.pretty :as malli.pretty]
|
||||
[malli.dev.virhe :as malli.virhe]
|
||||
malli.error
|
||||
malli.instrument
|
||||
malli.util
|
||||
schema.common
|
||||
[schema.core :as schema]
|
||||
schema.registry
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
;;;; Formatters
|
||||
;; These formatters replace the original ones provided by Malli. They are more
|
||||
;; compact (less line breaks) and don't show the "More Information" section.
|
||||
|
||||
(defn block
|
||||
"Same as `malli.dev.pretty/-block`, but adds only one line break between `text`
|
||||
and `body`."
|
||||
[text body printer]
|
||||
[:group (malli.virhe/-text text printer) :break [:align 2 body]])
|
||||
|
||||
(defmethod malli.virhe/-format ::malli/explain
|
||||
[_ _ {:keys [schema] :as explanation} printer]
|
||||
{:body
|
||||
[:group
|
||||
(block "Value:" (malli.virhe/-visit (malli.error/error-value explanation printer) printer) printer)
|
||||
:break :break
|
||||
(block "Errors:" (malli.virhe/-visit (malli.error/humanize explanation) printer) printer)
|
||||
:break :break
|
||||
(block "Schema:" (malli.virhe/-visit schema printer) printer)]})
|
||||
|
||||
(defmethod malli.virhe/-format ::malli/invalid-input
|
||||
[_ _ {:keys [args input fn-name]} printer]
|
||||
{:body
|
||||
(cond-> [:group
|
||||
(block "Invalid function arguments:" (malli.virhe/-visit args printer) printer)
|
||||
:break :break]
|
||||
fn-name
|
||||
(conj (block "Function Var:" (malli.virhe/-visit fn-name printer) printer)
|
||||
:break
|
||||
:break)
|
||||
:else
|
||||
(conj (block "Input Schema:" (malli.virhe/-visit input printer) printer)
|
||||
:break
|
||||
:break
|
||||
(block "Errors:" (malli.pretty/-explain input args printer) printer)))})
|
||||
|
||||
(defmethod malli.virhe/-format ::malli/invalid-output
|
||||
[_ _ {:keys [value args output fn-name]} printer]
|
||||
{:body
|
||||
(cond-> [:group
|
||||
(block "Invalid function return value:" (malli.virhe/-visit value printer) printer)
|
||||
:break :break]
|
||||
fn-name
|
||||
(conj (block "Function Var:" (malli.virhe/-visit fn-name printer) printer)
|
||||
:break
|
||||
:break)
|
||||
:else
|
||||
(conj (block "Function arguments:" (malli.virhe/-visit args printer) printer)
|
||||
:break
|
||||
:break
|
||||
(block "Output Schema:" (malli.virhe/-visit output printer) printer)
|
||||
:break
|
||||
:break
|
||||
(block "Errors:" (malli.pretty/-explain output value printer) printer)))})
|
||||
|
||||
(defn register-schemas
|
||||
"Register all global schemas in `schema.registry/registry`.
|
||||
|
||||
Since keys in a map are unique, remember to qualify keywords. Prefer to add to
|
||||
the global registry schemas for domain entities (e.g. message, chat,
|
||||
notification, etc) or unambiguously useful schemas, like
|
||||
`:schema.common/theme`."
|
||||
[]
|
||||
(schema.registry/merge (malli.util/schemas))
|
||||
(schema.common/register-schemas))
|
||||
|
||||
(defn setup!
|
||||
"Configure Malli and initializes instrumentation.
|
||||
|
||||
After evaluating an s-exp in the REPL that changes a function schema you'll
|
||||
need to either save the file where the schema is defined and hot reload or
|
||||
manually call `setup!`, otherwise you won't see any changes. It is safe and
|
||||
even expected you will call `setup!` multiple times in REPLs."
|
||||
[]
|
||||
(try
|
||||
(schema.registry/init-global-registry)
|
||||
(register-schemas)
|
||||
|
||||
;; In theory not necessary, but sometimes in a REPL session the dev needs to
|
||||
;; call unstrument! manually.
|
||||
(malli.instrument/unstrument!)
|
||||
|
||||
(malli.dev/start! {:report (schema/reporter)})
|
||||
(println "Schemas initialized.")
|
||||
|
||||
;; It is relatively easy to write invalid schemas, but we don't want to
|
||||
;; block the app from initializing if such errors happen, at least not until
|
||||
;; Malli matures in the project.
|
||||
(catch js/Error e
|
||||
(log/error "Failed to initialize schemas" {:error e}))))
|
||||
@@ -0,0 +1,6 @@
|
||||
(ns status-im2.setup.schema-preload
|
||||
":dev/always is needed so that the compiler doesn't cache this file."
|
||||
{:dev/always true}
|
||||
(:require [status-im2.setup.schema :as schema]))
|
||||
|
||||
(schema/setup!)
|
||||
@@ -21,6 +21,14 @@
|
||||
vals
|
||||
(sort-by :position)))
|
||||
|
||||
(rf/reg-sub
|
||||
:wallet/addresses
|
||||
:<- [:wallet]
|
||||
:-> #(->> %
|
||||
:accounts
|
||||
keys
|
||||
set))
|
||||
|
||||
(rf/reg-sub
|
||||
:wallet/balances
|
||||
:<- [:wallet/accounts]
|
||||
|
||||
@@ -167,3 +167,17 @@
|
||||
:balance 3250
|
||||
:tokens tokens-0x1}
|
||||
(rf/sub [sub-name])))))
|
||||
|
||||
|
||||
(h/deftest-sub :wallet/addresses
|
||||
[sub-name]
|
||||
(testing "returns all addresses"
|
||||
(swap! rf-db/app-db
|
||||
#(-> %
|
||||
(assoc-in [:wallet :accounts] accounts)
|
||||
(assoc-in [:wallet :current-viewing-account-address] "0x1")))
|
||||
|
||||
(is
|
||||
(= (set ["0x1" "0x2"])
|
||||
(rf/sub [sub-name])))))
|
||||
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
|
||||
(defmacro with-app-initialized
|
||||
[& body]
|
||||
`(if (app-initialized)
|
||||
(do ~@body)
|
||||
(do
|
||||
(rf/dispatch [:app-started])
|
||||
(rf-test/wait-for
|
||||
[:profile/get-profiles-overview-success]
|
||||
~@body))))
|
||||
`(do
|
||||
(status-im.utils.test/init!)
|
||||
(if (test-helpers.integration/app-initialized)
|
||||
(do ~@body)
|
||||
(do
|
||||
(rf/dispatch [:app-started])
|
||||
(rf-test/wait-for [:profile/get-profiles-overview-success]
|
||||
~@body)))))
|
||||
|
||||
(defmacro with-account
|
||||
[& body]
|
||||
`(if (messenger-started)
|
||||
`(if (test-helpers.integration/messenger-started)
|
||||
(do ~@body)
|
||||
(do
|
||||
(create-multiaccount!)
|
||||
(rf-test/wait-for
|
||||
[:status-im.transport.core/messenger-started]
|
||||
(assert-messenger-started)
|
||||
(test-helpers.integration/create-multiaccount!)
|
||||
(rf-test/wait-for [:status-im.transport.core/messenger-started]
|
||||
(test-helpers.integration/assert-messenger-started)
|
||||
~@body))))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
(ns test-helpers.integration
|
||||
(:require-macros [test-helpers.integration])
|
||||
(:require
|
||||
[cljs.test :refer [is]]
|
||||
[native-module.core :as native-module]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
(:require
|
||||
["bignumber.js" :as BigNumber]
|
||||
[clojure.string :as string]
|
||||
[schema.core :as schema]
|
||||
[utils.i18n :as i18n]))
|
||||
|
||||
;; The BigNumber version included in web3 sometimes hangs when dividing large
|
||||
@@ -237,3 +238,7 @@
|
||||
|
||||
:else
|
||||
(str amount))))
|
||||
|
||||
(schema/=> format-amount
|
||||
[:=> [:cat [:maybe :int]]
|
||||
[:maybe :string]])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import urllib.request
|
||||
import requests
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
@@ -161,6 +161,57 @@ def is_uploaded():
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _upload_time_limit(seconds):
|
||||
def signal_handler(signum, frame):
|
||||
raise TimeoutError("Apk upload took more than %s seconds" % seconds)
|
||||
|
||||
signal.signal(signal.SIGALRM, signal_handler)
|
||||
signal.alarm(seconds)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
class UploadApkException(Exception):
|
||||
pass
|
||||
|
||||
def _upload_and_check_response(apk_file_path):
|
||||
with _upload_time_limit(600):
|
||||
with open(apk_file_path, 'rb') as f:
|
||||
resp = sauce.storage._session.request('post', '/v1/storage/upload', files={'payload': f})
|
||||
|
||||
try:
|
||||
if resp['item']['name'] != test_suite_data.apk_name:
|
||||
raise UploadApkException("Incorrect apk was uploaded to Sauce storage, response:\n%s" % resp)
|
||||
except KeyError:
|
||||
raise UploadApkException("Error when uploading apk to Sauce storage, response:\n%s" % resp)
|
||||
|
||||
def _upload_and_check_response_with_retries(apk_file_path, retries=3):
|
||||
for _ in range(retries):
|
||||
try:
|
||||
_upload_and_check_response(apk_file_path)
|
||||
break
|
||||
except (ConnectionError, RemoteDisconnected):
|
||||
sleep(10)
|
||||
|
||||
def _download_apk(url):
|
||||
# Absolute path adde to handle CI runs.
|
||||
apk_path = os.path.join(os.path.dirname(__file__), test_suite_data.apk_name)
|
||||
|
||||
print('Downloading: %s' % url)
|
||||
try:
|
||||
resp = requests.get(url)
|
||||
resp.raise_for_status()
|
||||
except requests.RequestException as err:
|
||||
print(resp.text)
|
||||
raise err
|
||||
|
||||
with open(apk_path, 'wb') as f:
|
||||
f.write(resp.content)
|
||||
|
||||
return apk_path
|
||||
|
||||
def pytest_configure(config):
|
||||
global option
|
||||
option = config.option
|
||||
@@ -180,75 +231,49 @@ def pytest_configure(config):
|
||||
apibase = 'eu-central-1.saucelabs.com'
|
||||
else:
|
||||
raise NotImplementedError("Unknown SauceLabs datacenter")
|
||||
|
||||
global sauce
|
||||
sauce = SauceLab('https://api.' + apibase + '/', sauce_username, sauce_access_key)
|
||||
if config.getoption('log_steps'):
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
if config.getoption('env') != 'api':
|
||||
test_suite_data.apk_name = ([i for i in [i for i in config.getoption('apk').split('/')
|
||||
if '.apk' in i]])[0]
|
||||
if is_master(config):
|
||||
pr_number = config.getoption('pr_number')
|
||||
if config.getoption('testrail_report'):
|
||||
if pr_number:
|
||||
run_number = len(testrail_report.get_runs(pr_number)) + 1
|
||||
run_name = 'PR-%s run #%s' % (pr_number, run_number)
|
||||
else:
|
||||
run_name = test_suite_data.apk_name
|
||||
testrail_report.add_run(run_name)
|
||||
if pr_number:
|
||||
from github import Github
|
||||
repo = Github(github_token).get_user('status-im').get_repo('status-mobile')
|
||||
pull = repo.get_pull(int(pr_number))
|
||||
pull.get_commits()[0].create_status(state='pending', context='Mobile e2e tests',
|
||||
description='e2e tests are running')
|
||||
if config.getoption('env') == 'sauce':
|
||||
if not is_uploaded():
|
||||
def _upload_and_check_response(apk_file_path):
|
||||
@contextmanager
|
||||
def _upload_time_limit(seconds):
|
||||
def signal_handler(signum, frame):
|
||||
raise TimeoutError("Apk upload took more than %s seconds" % seconds)
|
||||
if config.getoption('env') == 'api':
|
||||
return
|
||||
|
||||
signal.signal(signal.SIGALRM, signal_handler)
|
||||
signal.alarm(seconds)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
test_suite_data.apk_name = ([i for i in [i for i in config.getoption('apk').split('/')
|
||||
if '.apk' in i]])[0]
|
||||
if not is_master(config):
|
||||
return
|
||||
|
||||
with _upload_time_limit(600):
|
||||
class UploadApkException(Exception):
|
||||
pass
|
||||
pr_number = config.getoption('pr_number')
|
||||
if config.getoption('testrail_report'):
|
||||
if pr_number:
|
||||
run_number = len(testrail_report.get_runs(pr_number)) + 1
|
||||
run_name = 'PR-%s run #%s' % (pr_number, run_number)
|
||||
else:
|
||||
run_name = test_suite_data.apk_name
|
||||
testrail_report.add_run(run_name)
|
||||
|
||||
with open(apk_file_path, 'rb') as f:
|
||||
resp = sauce.storage._session.request('post', '/v1/storage/upload',
|
||||
files={'payload': f})
|
||||
try:
|
||||
if resp['item']['name'] != test_suite_data.apk_name:
|
||||
raise UploadApkException(
|
||||
"Incorrect apk was uploaded to Sauce storage, response:\n%s" % resp)
|
||||
except KeyError:
|
||||
raise UploadApkException(
|
||||
"Error when uploading apk to Sauce storage, response:\n%s" % resp)
|
||||
if pr_number:
|
||||
from github import Github
|
||||
repo = Github(github_token).get_user('status-im').get_repo('status-mobile')
|
||||
pull = repo.get_pull(int(pr_number))
|
||||
pull.get_commits()[0].create_status(
|
||||
state='pending',
|
||||
context='Mobile e2e tests',
|
||||
description='e2e tests are running'
|
||||
)
|
||||
|
||||
if 'http' in config.getoption('apk'):
|
||||
# it works with just a file_name, but I've added full path because not sure how it'll behave on the remote run (Jenkins)
|
||||
file_path, to_remove = os.path.join(os.path.dirname(__file__), test_suite_data.apk_name), True
|
||||
urllib.request.urlretrieve(config.getoption('apk'),
|
||||
filename=file_path) # if url is not valid it raises an error
|
||||
else:
|
||||
file_path, to_remove = config.getoption('apk'), False
|
||||
if config.getoption('env') == 'sauce' and not is_uploaded():
|
||||
apk_src = config.getoption('apk')
|
||||
if apk_src.startswith('http'):
|
||||
apk_path = _download_apk(apk_src)
|
||||
else:
|
||||
apk_path = apk_src
|
||||
|
||||
for _ in range(3):
|
||||
try:
|
||||
_upload_and_check_response(apk_file_path=file_path)
|
||||
break
|
||||
except (ConnectionError, RemoteDisconnected):
|
||||
sleep(10)
|
||||
if to_remove:
|
||||
os.remove(file_path)
|
||||
_upload_and_check_response_with_retries(apk_path)
|
||||
if apk_src.startswith('http'):
|
||||
os.remove(apk_path)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
|
||||
@@ -159,16 +159,13 @@ class TestOneToOneChatMultipleSharedDevicesNewUi(MultipleSharedDeviceTestCase):
|
||||
self.errors.append("Link message reaction is not shown for the sender")
|
||||
|
||||
self.home_2.just_fyi("Check 'Open in Status' option")
|
||||
# url_to_open = 'http://status.im' # ToDo: enable when a bug with Status link is fixed
|
||||
url_to_open = 'https://coinmarketcap.com/'
|
||||
url_to_open = 'http://status.app'
|
||||
self.chat_1.send_message(url_to_open)
|
||||
chat_element = self.chat_2.chat_element_by_text(url_to_open)
|
||||
if chat_element.is_element_displayed(120):
|
||||
chat_element.click_on_link_inside_message_body()
|
||||
web_view = self.chat_2.open_in_status_button.click()
|
||||
# if not web_view.element_by_text('Private, Secure Communication').is_element_displayed(60):
|
||||
if not web_view.element_by_text_part(
|
||||
"Today's Cryptocurrency Prices by Market Cap").is_element_displayed(30):
|
||||
if not web_view.element_by_text('Make the jump to web3').is_element_displayed(60):
|
||||
self.errors.append('URL was not opened from 1-1 chat')
|
||||
else:
|
||||
self.errors.append("Message with URL was not received")
|
||||
@@ -293,7 +290,7 @@ class TestOneToOneChatMultipleSharedDevicesNewUi(MultipleSharedDeviceTestCase):
|
||||
sent_time_variants = self.chat_2.convert_device_time_to_chat_timestamp()
|
||||
if timestamp not in sent_time_variants:
|
||||
self.errors.append(
|
||||
'Timestamp on message %s does not correspond expected [%s]' % (timestamp, *sent_time_variants))
|
||||
'Timestamp on message %s does not correspond expected %s' % (timestamp, sent_time_variants))
|
||||
except NoSuchElementException:
|
||||
self.errors.append("No timestamp on message %s" % messages[0])
|
||||
for message in [messages[1], messages[2]]:
|
||||
|
||||
@@ -33,6 +33,8 @@ class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase):
|
||||
self.channel = self.community_view.get_channel(self.channel_name).click()
|
||||
|
||||
@marks.testrail_id(703503)
|
||||
@marks.xfail(reason="Curated communities not loading, https://github.com/status-im/status-mobile/issues/17852",
|
||||
run=False)
|
||||
def test_community_discovery(self):
|
||||
self.home.navigate_back_to_home_view()
|
||||
self.home.communities_tab.click()
|
||||
@@ -190,8 +192,6 @@ class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase):
|
||||
self.home.just_fyi("Restore user with predefined communities and contacts")
|
||||
self.sign_in.recover_access(passphrase=waku_user.seed, second_user=True)
|
||||
|
||||
self.home.just_fyi("Restore user with predefined communities and contacts")
|
||||
|
||||
self.home.just_fyi("Check contacts/blocked users")
|
||||
self.home.chats_tab.click()
|
||||
self.home.contacts_tab.click()
|
||||
@@ -200,10 +200,9 @@ class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase):
|
||||
self.errors.append(
|
||||
"Incorrect contacts number restored: %s instead of %s" % (contacts_number, len(waku_user.contacts)))
|
||||
else:
|
||||
for i in range(contacts_number):
|
||||
key = waku_user.contacts[i]
|
||||
if not self.home.element_by_text(key).is_element_displayed(30):
|
||||
self.errors.append('%s was not restored as a contact from waku backup!' % key)
|
||||
for contact in waku_user.contacts:
|
||||
if not self.home.element_by_text(contact).is_element_displayed(30):
|
||||
self.errors.append('%s was not restored as a contact from waku backup!' % contact)
|
||||
# Disabled for simple check as sometimes from waku-backup users restored with 3-random names
|
||||
# self.home.click_system_back_button_until_element_is_shown()
|
||||
# contact_row = self.home.contact_details_row(index=i + 1)
|
||||
@@ -248,7 +247,7 @@ class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase):
|
||||
|
||||
self.home.just_fyi("Check that can login with different user")
|
||||
self.home.reopen_app(sign_in=False)
|
||||
self.sign_in.show_profiles_button.click()
|
||||
self.sign_in.show_profiles_button.wait_and_click()
|
||||
self.sign_in.element_by_text(self.username).click()
|
||||
self.sign_in.sign_in()
|
||||
self.home.communities_tab.click()
|
||||
@@ -257,7 +256,7 @@ class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase):
|
||||
|
||||
self.home.just_fyi("Check that can remove user from logged out state")
|
||||
self.home.reopen_app(sign_in=False)
|
||||
self.sign_in.show_profiles_button.click()
|
||||
self.sign_in.show_profiles_button.wait_and_click()
|
||||
user_card = self.sign_in.get_user(username=self.username)
|
||||
user_card.open_user_options()
|
||||
self.sign_in.remove_profile_button.click()
|
||||
@@ -267,7 +266,7 @@ class TestCommunityOneDeviceMerged(MultipleSharedDeviceTestCase):
|
||||
|
||||
self.home.just_fyi("Check that removed user is not shown in the list anymore")
|
||||
self.home.reopen_app(sign_in=False)
|
||||
self.sign_in.show_profiles_button.click()
|
||||
self.sign_in.show_profiles_button.wait_and_click()
|
||||
if self.sign_in.element_by_text(self.username).is_element_displayed():
|
||||
self.errors.append("Removed user is re-appeared after relogin!")
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@ communities = {
|
||||
|
||||
#contacts = ['Test_contact', 'MyCustomNickname'] # enable back when https://github.com/status-im/status-mobile/issues/15500 is fixed
|
||||
|
||||
contacts = ['zQ3...rLSpUs', 'zQ3...mxAZUU']
|
||||
contacts = ['zQ3...LSpUs', 'zQ3...xAZUU']
|
||||
blocked_user = 'Clear Flat Milkweedbug'
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
module.exports = {
|
||||
preset: 'react-native',
|
||||
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '../test/jest/jestSetup.js'],
|
||||
setupFilesAfterEnv: [
|
||||
'@testing-library/jest-native/extend-expect',
|
||||
'../component-spec/status_im2.setup.schema_preload.js',
|
||||
'../test/jest/jestSetup.js',
|
||||
],
|
||||
setupFiles: [],
|
||||
testPathIgnorePatterns: [],
|
||||
moduleNameMapper: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user