Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4127fdae9e | ||
|
|
99074382bc | ||
|
|
84ad0d03fb | ||
|
|
c4b27cad05 | ||
|
|
7c231f1fc3 | ||
|
|
bd68f60524 | ||
|
|
b8b77d0d03 | ||
|
|
5d21482506 | ||
|
|
66d10bd116 | ||
|
|
1ab55787dd | ||
|
|
77b97199ff | ||
|
|
5059a4bea4 | ||
|
|
146c766892 | ||
|
|
a95f2a0f52 | ||
|
|
38dbd6687e | ||
|
|
4633fc22ed | ||
|
|
7e31e028fa | ||
|
|
067b2d5afb | ||
|
|
e4ba0d3062 | ||
|
|
11e49c0cc3 |
@@ -44,22 +44,37 @@
|
||||
[categ]
|
||||
(reduce-kv #(assoc %1 (name %2) %3) {} categ))
|
||||
|
||||
(defn role-permission?
|
||||
[token-permission]
|
||||
(contains? constants/community-role-permissions (:type token-permission)))
|
||||
|
||||
(defn membership-permission?
|
||||
[token-permission]
|
||||
(= (:type token-permission) constants/community-token-permission-become-member))
|
||||
|
||||
(defn <-rpc
|
||||
[c]
|
||||
(-> c
|
||||
(set/rename-keys {:canRequestAccess :can-request-access?
|
||||
:canManageUsers :can-manage-users?
|
||||
:canDeleteMessageForEveryone :can-delete-message-for-everyone?
|
||||
:canJoin :can-join?
|
||||
:requestedToJoinAt :requested-to-join-at
|
||||
:isMember :is-member?
|
||||
:adminSettings :admin-settings
|
||||
:tokenPermissions :token-permissions
|
||||
:communityTokensMetadata :tokens-metadata
|
||||
:introMessage :intro-message
|
||||
:muteTill :muted-till
|
||||
:lastOpenedAt :last-opened-at
|
||||
:joinedAt :joined-at})
|
||||
(set/rename-keys
|
||||
{:canRequestAccess :can-request-access?
|
||||
:canManageUsers :can-manage-users?
|
||||
:canDeleteMessageForEveryone :can-delete-message-for-everyone?
|
||||
;; This flag is misleading based on its name alone
|
||||
;; because it should not be used to decide if the user
|
||||
;; is *allowed* to join. Allowance is based on token
|
||||
;; permissions. Still, the flag can be used to know
|
||||
;; whether or not the user will have to wait until an
|
||||
;; admin approves a join request.
|
||||
:canJoin :can-join?
|
||||
:requestedToJoinAt :requested-to-join-at
|
||||
:isMember :is-member?
|
||||
:adminSettings :admin-settings
|
||||
:tokenPermissions :token-permissions
|
||||
:communityTokensMetadata :tokens-metadata
|
||||
:introMessage :intro-message
|
||||
:muteTill :muted-till
|
||||
:lastOpenedAt :last-opened-at
|
||||
:joinedAt :joined-at})
|
||||
(update :admin-settings
|
||||
set/rename-keys
|
||||
{:pinMessageAllMembersEnabled :pin-message-all-members-enabled?})
|
||||
@@ -67,9 +82,16 @@
|
||||
(update :chats <-chats-rpc)
|
||||
(update :token-permissions seq)
|
||||
(update :categories <-categories-rpc)
|
||||
(assoc :role-permissions?
|
||||
(->> c
|
||||
:tokenPermissions
|
||||
vals
|
||||
(some role-permission?)))
|
||||
(assoc :membership-permissions?
|
||||
(some #(= (:type %) constants/community-token-permission-become-member)
|
||||
(vals (:tokenPermissions c))))
|
||||
(->> c
|
||||
:tokenPermissions
|
||||
vals
|
||||
(some membership-permission?)))
|
||||
(assoc :token-images
|
||||
(reduce (fn [acc {sym :symbol image :image}]
|
||||
(assoc acc sym image))
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
[react-native.core :as rn]
|
||||
[react-native.permissions :as permissions]
|
||||
[react-native.platform :as platform]
|
||||
[status-im.common.biometric.events :as biometric]
|
||||
status-im.common.serialization
|
||||
status-im.common.standard-authentication.events
|
||||
[status-im.common.theme.core :as theme]
|
||||
@@ -125,7 +124,7 @@
|
||||
:content (i18n/label :t/biometric-auth-confirm-message)
|
||||
:confirm-button-text (i18n/label :t/biometric-auth-confirm-try-again)
|
||||
:cancel-button-text (i18n/label :t/biometric-auth-confirm-logout)
|
||||
:on-accept #(biometric/authenticate nil {:on-fail on-biometric-auth-fail})
|
||||
:on-accept #(rf/dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}])
|
||||
:on-cancel #(re-frame/dispatch [:multiaccounts.logout.ui/logout-confirmed])})))
|
||||
|
||||
(rf/defn on-return-from-background
|
||||
@@ -148,7 +147,7 @@
|
||||
#(when-let [chat-id (:current-chat-id db)]
|
||||
{:dispatch [:chat/mark-all-as-read chat-id]})
|
||||
#(when requires-bio-auth
|
||||
(biometric/authenticate % {:on-fail on-biometric-auth-fail})))))
|
||||
{:dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}]}))))
|
||||
|
||||
(rf/defn on-going-in-background
|
||||
[{:keys [db now]}]
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [title title-icon type theme description blur? community-name community-logo button-icon
|
||||
account-name emoji
|
||||
account-name emoji context-tag-type
|
||||
on-button-press
|
||||
on-button-long-press
|
||||
button-disabled? account-avatar-emoji account-avatar-type customization-color icon-avatar
|
||||
@@ -214,6 +214,7 @@
|
||||
:description description
|
||||
:community-name community-name
|
||||
:community-logo community-logo
|
||||
:context-tag-type context-tag-type
|
||||
:customization-color customization-color
|
||||
:account-name account-name
|
||||
:emoji emoji}]]
|
||||
|
||||
@@ -3,62 +3,69 @@
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.platform :as platform]))
|
||||
[react-native.platform :as platform]
|
||||
[utils.number]))
|
||||
|
||||
;; Set radius coefficient based on Android Version.
|
||||
;; A good way to know is to check if shadow color is supported i.e API level 28 (Android 9) above
|
||||
(def ^:private shadow-radius-coefficient (if platform/is-shadow-color-supported? 0.8 0.1))
|
||||
|
||||
(defn- get-shadow-color
|
||||
"Calculates the shadow color based on a given color and opacity, with an optional coefficient.
|
||||
- Arity [color opacity]: Uses a default coefficient value of 5 (empirically determined) to compute the shadow color.
|
||||
- Arity [color opacity coefficient]: Uses the specified coefficient to compute the shadow color.
|
||||
On Android platforms, the shadow color's alpha is adjusted based on the product of opacity and
|
||||
coefficient, constrained between 0 and 1. Returns a map with :shadow-color and :shadow-opacity keys."
|
||||
([color opacity]
|
||||
(get-shadow-color color opacity 5))
|
||||
([color opacity coefficient]
|
||||
{:shadow-color (if platform/android?
|
||||
(colors/alpha color (utils.number/value-in-range (* opacity coefficient) 0 1))
|
||||
color)
|
||||
:shadow-opacity opacity}))
|
||||
|
||||
(defn- get-shadow-radius
|
||||
"Computes shadow radius and elevation properties based on a given radius, with an optional coefficient.
|
||||
- Arity [radius]: Uses a default coefficient of 0.8 for API level 28 and above and 0.1 otherwise (empirically determined) to compute the elevation.
|
||||
- Arity [radius coefficient]: Uses the specified coefficient to compute the elevation.
|
||||
Returns a map with :elevation, computed as the product of radius and coefficient, and :shadow-radius keys."
|
||||
([radius]
|
||||
(get-shadow-radius radius shadow-radius-coefficient))
|
||||
([radius coefficient]
|
||||
{:elevation (* radius coefficient)
|
||||
:shadow-radius radius}))
|
||||
|
||||
(def ^:private shadows
|
||||
(let [dark-normal {1 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.5))
|
||||
:shadow-offset {:width 0 :height 4}
|
||||
:elevation 3
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 20}
|
||||
2 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.64))
|
||||
:shadow-offset {:width 0 :height 4}
|
||||
:elevation 4
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 20}
|
||||
3 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.64))
|
||||
:shadow-offset {:width 0 :height 12}
|
||||
:elevation 8
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 20}
|
||||
4 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.72))
|
||||
:shadow-offset {:width 0 :height 16}
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 20
|
||||
:elevation 15}}
|
||||
(let [dark-normal {1 (merge {:shadow-offset {:width 0 :height 4}}
|
||||
(get-shadow-color colors/neutral-100 0.5)
|
||||
(get-shadow-radius 20))
|
||||
2 (merge {:shadow-offset {:width 0 :height 4}}
|
||||
(get-shadow-color colors/neutral-100 0.64)
|
||||
(get-shadow-radius 20))
|
||||
3 (merge {:shadow-offset {:width 0 :height 12}}
|
||||
(get-shadow-color colors/neutral-100 0.64)
|
||||
(get-shadow-radius 20))
|
||||
4 (merge {:shadow-offset {:width 0 :height 16}}
|
||||
(get-shadow-color colors/neutral-100 0.72)
|
||||
(get-shadow-radius 20))}
|
||||
dark-normal-inverted (-> dark-normal
|
||||
(update-in [:soft :shadow-offset :height] -)
|
||||
(update-in [:medium :shadow-offset :height] -)
|
||||
(update-in [:intense :shadow-offset :height] -)
|
||||
(update-in [:strong :shadow-offset :height] -))
|
||||
light-normal {1 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.04))
|
||||
:shadow-offset {:width 0 :height 4}
|
||||
:elevation 1
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 16}
|
||||
2 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.08))
|
||||
:shadow-offset {:width 0 :height 4}
|
||||
:elevation 6
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 16}
|
||||
3 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.12))
|
||||
:shadow-offset {:width 0 :height 12}
|
||||
:elevation 8
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 16}
|
||||
4 {:shadow-color (when platform/ios?
|
||||
(colors/alpha colors/neutral-100 0.16))
|
||||
:shadow-offset {:width 0 :height 16}
|
||||
:shadow-opacity 1
|
||||
:shadow-radius 16
|
||||
:elevation 13}}
|
||||
light-normal {1 (merge {:shadow-offset {:width 0 :height 4}}
|
||||
(get-shadow-color colors/neutral-100 0.04)
|
||||
(get-shadow-radius 16))
|
||||
|
||||
2 (merge {:shadow-offset {:width 0 :height 4}}
|
||||
(get-shadow-color colors/neutral-100 0.08)
|
||||
(get-shadow-radius 16))
|
||||
3 (merge {:shadow-offset {:width 0 :height 12}}
|
||||
(get-shadow-color colors/neutral-100 0.12)
|
||||
(get-shadow-radius 16))
|
||||
4 (merge {:shadow-offset {:width 0 :height 16}}
|
||||
(get-shadow-color colors/neutral-100 0.16)
|
||||
(get-shadow-radius 16))}
|
||||
light-normal-inverted (-> light-normal
|
||||
(update-in [:soft :shadow-offset :height] -)
|
||||
(update-in [:medium :shadow-offset :height] -)
|
||||
|
||||
@@ -14,3 +14,5 @@
|
||||
(def low-device? (and android? (< version 29)))
|
||||
|
||||
(def is-below-android-13? (and android? (< version 33)))
|
||||
|
||||
(def is-shadow-color-supported? (and android? (> version 27)))
|
||||
|
||||
@@ -28,10 +28,15 @@
|
||||
[:on-success [:or fn? [:cat keyword? [:* :any]]]]
|
||||
[:on-error [:or fn? [:cat keyword? [:* :any]]]]]])
|
||||
|
||||
(def ^:private ?error
|
||||
[:fn {:error/message "schema.common/error should be of type js/Error"}
|
||||
(fn [v] (instance? js/Error v))])
|
||||
|
||||
(defn register-schemas
|
||||
[]
|
||||
(registry/register ::theme ?theme)
|
||||
(registry/register ::customization-color ?customization-color)
|
||||
(registry/register ::public-key ?public-key)
|
||||
(registry/register ::image-source ?image-source)
|
||||
(registry/register ::rpc-call ?rpc-call))
|
||||
(registry/register ::rpc-call ?rpc-call)
|
||||
(registry/register ::error ?error))
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
(def ^:private ?cofx
|
||||
[:map])
|
||||
|
||||
(def ^:private ?event-fx
|
||||
[:maybe
|
||||
[:map {:closed true}
|
||||
[:db {:optional true} [:maybe map?]]
|
||||
[:fx {:optional true}
|
||||
[:maybe
|
||||
[:vector {:optional true}
|
||||
[:vector :any]]]]]])
|
||||
|
||||
(defn register-schemas
|
||||
[]
|
||||
(registry/register ::event-fx ?event-fx)
|
||||
(registry/register ::cofx ?cofx))
|
||||
|
||||
@@ -1,120 +1,124 @@
|
||||
(ns status-im.common.biometric.events
|
||||
(:require
|
||||
[native-module.core :as native-module]
|
||||
[re-frame.core :as re-frame]
|
||||
[react-native.biometrics :as biometrics]
|
||||
[react-native.platform :as platform]
|
||||
[schema.core :as schema]
|
||||
[status-im.common.biometric.events-schema :as events-schema]
|
||||
[status-im.common.biometric.utils :as utils]
|
||||
[status-im.common.keychain.events :as keychain]
|
||||
[status-im.constants :as constants]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def android-device-blacklisted?
|
||||
(and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand")))
|
||||
|
||||
(defn get-label-by-type
|
||||
[biometric-type]
|
||||
(condp = biometric-type
|
||||
constants/biometrics-type-android (i18n/label :t/biometric-fingerprint)
|
||||
constants/biometrics-type-face-id (i18n/label :t/biometric-faceid)
|
||||
(i18n/label :t/biometric-touchid)))
|
||||
|
||||
(defn get-icon-by-type
|
||||
[biometric-type]
|
||||
(condp = biometric-type
|
||||
constants/biometrics-type-face-id :i/face-id
|
||||
:i/touch-id))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:biometric/get-supported-biometric-type
|
||||
(rf/reg-fx
|
||||
:biometric/get-supported-type
|
||||
(fn []
|
||||
;;NOTE: if we can't save user password, we can't use biometric
|
||||
(keychain/can-save-user-password?
|
||||
(fn [can-save?]
|
||||
(when (and can-save? (not android-device-blacklisted?))
|
||||
(when (and can-save? (not utils/android-device-blacklisted?))
|
||||
(-> (biometrics/get-supported-type)
|
||||
(.then (fn [type]
|
||||
(rf/dispatch [:biometric/get-supported-biometric-type-success type])))))))))
|
||||
(rf/dispatch [:biometric/set-supported-type type])))))))))
|
||||
|
||||
(rf/defn get-supported-biometric-auth-success
|
||||
{:events [:biometric/get-supported-biometric-type-success]}
|
||||
[{:keys [db]} supported-type]
|
||||
(defn set-supported-type
|
||||
[{:keys [db]} [supported-type]]
|
||||
{:db (assoc db :biometric/supported-type supported-type)})
|
||||
|
||||
(rf/defn show-message
|
||||
{:events [:biometric/show-message]}
|
||||
[_ error]
|
||||
(let [code (ex-cause error)
|
||||
content (if (#{::biometrics/not-enrolled
|
||||
(schema/=> set-supported-type events-schema/?set-supported-type)
|
||||
(rf/reg-event-fx :biometric/set-supported-type set-supported-type)
|
||||
|
||||
(defn show-message
|
||||
[_ [code]]
|
||||
(let [content (if (#{::biometrics/not-enrolled
|
||||
::biometrics/not-available}
|
||||
code)
|
||||
(i18n/label :t/grant-face-id-permissions)
|
||||
(i18n/label :t/biometric-auth-error {:code code}))]
|
||||
{:effects.utils/show-popup
|
||||
{:title (i18n/label :t/biometric-auth-login-error-title)
|
||||
:content content}}))
|
||||
{:fx [[:effects.utils/show-popup
|
||||
{:title (i18n/label :t/biometric-auth-login-error-title)
|
||||
:content content}]]}))
|
||||
|
||||
(schema/=> show-message events-schema/?show-message)
|
||||
(rf/reg-event-fx :biometric/show-message show-message)
|
||||
|
||||
(re-frame/reg-fx
|
||||
(rf/reg-fx
|
||||
:biometric/authenticate
|
||||
(fn [{:keys [on-success on-fail prompt-message]}]
|
||||
(fn [{:keys [on-success on-fail on-cancel prompt-message on-done]}]
|
||||
(-> (biometrics/authenticate
|
||||
{:prompt-message (or prompt-message (i18n/label :t/biometric-auth-reason-login))
|
||||
:fallback-prompt-message (i18n/label
|
||||
:t/biometric-auth-login-ios-fallback-label)
|
||||
:cancel-button-text (i18n/label :t/cancel)})
|
||||
(.then (fn [not-canceled?]
|
||||
(when (and on-success not-canceled?)
|
||||
(on-success))))
|
||||
(utils/handle-cb on-done)
|
||||
(if not-canceled?
|
||||
(utils/handle-cb on-success)
|
||||
(utils/handle-cb on-cancel))))
|
||||
(.catch (fn [err]
|
||||
(when on-fail
|
||||
(on-fail err)))))))
|
||||
(utils/handle-cb on-done)
|
||||
(utils/handle-cb on-fail err))))))
|
||||
|
||||
(rf/defn authenticate
|
||||
{:events [:biometric/authenticate]}
|
||||
[_ opts]
|
||||
{:biometric/authenticate opts})
|
||||
(defn authenticate
|
||||
[{:keys [db]} [opts]]
|
||||
(let [pending? (get db :biometric/auth-pending?)]
|
||||
;;NOTE: prompting biometric check while another one is pending triggers error
|
||||
(when-not pending?
|
||||
{:db (assoc db :biometric/auth-pending? true)
|
||||
:fx [[:biometric/authenticate
|
||||
(assoc opts :on-done [:set :biometric/auth-pending? false])]]})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:biometric/on-enable-success
|
||||
(fn [{:keys [db]} [password]]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:db (assoc db :auth-method constants/auth-method-biometric)
|
||||
:dispatch [:keychain/save-password-and-auth-method
|
||||
{:key-uid key-uid
|
||||
:masked-password password}]})))
|
||||
(schema/=> authenticate events-schema/?authenticate)
|
||||
(rf/reg-event-fx :biometric/authenticate authenticate)
|
||||
|
||||
(rf/reg-event-fx
|
||||
:biometric/enable
|
||||
(fn [_ [password]]
|
||||
{:dispatch [:biometric/authenticate
|
||||
{:on-success #(rf/dispatch [:biometric/on-enable-success password])
|
||||
:on-fail #(rf/dispatch [:biometric/show-message %])}]}))
|
||||
(defn enable-biometrics
|
||||
[{:keys [db]} [password]]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:db (assoc db :auth-method constants/auth-method-biometric)
|
||||
:fx [[:dispatch
|
||||
[:keychain/save-password-and-auth-method
|
||||
{:key-uid key-uid
|
||||
:masked-password password}]]]}))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:biometric/disable
|
||||
(fn [{:keys [db]}]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:db (assoc db :auth-method constants/auth-method-none)
|
||||
:keychain/clear-user-password key-uid})))
|
||||
(schema/=> enable-biometrics events-schema/?enable-biometrics)
|
||||
(rf/reg-event-fx :biometric/enable enable-biometrics)
|
||||
|
||||
(rf/reg-fx
|
||||
:biometric/check-if-available
|
||||
(fn [[key-uid callback]]
|
||||
(keychain/can-save-user-password?
|
||||
(fn [can-save?]
|
||||
(when can-save?
|
||||
(-> (biometrics/get-available)
|
||||
(.then (fn [available?]
|
||||
(when-not available?
|
||||
(throw (js/Error. "biometric-not-available")))))
|
||||
(.then #(keychain/get-auth-method! key-uid))
|
||||
(.then (fn [auth-method]
|
||||
(when auth-method (callback auth-method))))
|
||||
(.catch (fn [err]
|
||||
(when-not (= (.-message err) "biometric-not-available")
|
||||
(log/error "Failed to check if biometrics is available"
|
||||
{:error err
|
||||
:key-uid key-uid
|
||||
:event :profile.login/check-biometric}))))))))))
|
||||
(defn disable-biometrics
|
||||
[{:keys [db]}]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:db (assoc db :auth-method constants/auth-method-none)
|
||||
:fx [[:keychain/clear-user-password key-uid]]}))
|
||||
|
||||
(schema/=> disable-biometrics events-schema/?disable-biometrics)
|
||||
(rf/reg-event-fx :biometric/disable disable-biometrics)
|
||||
|
||||
(defn check-if-biometrics-available
|
||||
[{:keys [key-uid on-success on-fail]}]
|
||||
(keychain/can-save-user-password?
|
||||
(fn [can-save?]
|
||||
(if-not can-save?
|
||||
(utils/handle-cb on-fail
|
||||
(ex-info "cannot-save-user-password"
|
||||
{:fx :biometric/check-if-available}))
|
||||
(-> (biometrics/get-available)
|
||||
(.then (fn [available?]
|
||||
(when-not available?
|
||||
(throw (js/Error. "biometric-not-available")))))
|
||||
(.then #(keychain/get-auth-method! key-uid))
|
||||
(.then (fn [auth-method]
|
||||
(when auth-method
|
||||
(utils/handle-cb on-success auth-method))))
|
||||
(.catch (fn [err]
|
||||
(let [message (.-message err)]
|
||||
(utils/handle-cb on-fail
|
||||
(ex-info message
|
||||
{:err err
|
||||
:fx :biometric/check-if-available}))
|
||||
(when-not (= message "biometric-not-available")
|
||||
(log/error "Failed to check if biometrics is available"
|
||||
{:error err
|
||||
:key-uid key-uid
|
||||
:event :profile.login/check-biometric}))))))))))
|
||||
|
||||
(schema/=> check-if-biometrics-available events-schema/?check-if-biometrics-available)
|
||||
(rf/reg-fx :biometric/check-if-available check-if-biometrics-available)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
(ns status-im.common.biometric.events-schema
|
||||
(:require
|
||||
[status-im.constants :as constants]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(def ?set-supported-type
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple
|
||||
[:enum
|
||||
constants/biometrics-type-android
|
||||
constants/biometrics-type-face-id
|
||||
constants/biometrics-type-touch-id]]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?show-message
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple keyword?]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?authenticate
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple
|
||||
[:map {:closed true}
|
||||
[:on-success {:optional true} fn?]
|
||||
[:on-fail {:optional true} fn?]
|
||||
[:on-cancel {:optional true} fn?]
|
||||
[:prompt-message {:optional true} :string]]]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?enable-biometrics
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple
|
||||
[:fn {:error/message "Should be an instance of security/MaskedData"}
|
||||
(fn [pw] (instance? security/MaskedData pw))]]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?disable-biometrics
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?check-if-biometrics-available
|
||||
[:=>
|
||||
[:cat
|
||||
[:map {:closed true}
|
||||
[:key-uid string?]
|
||||
[:on-success {:optional true} [:maybe fn?]]
|
||||
[:on-fail {:optional true} [:maybe fn?]]]]
|
||||
:any])
|
||||
@@ -0,0 +1,80 @@
|
||||
(ns status-im.common.biometric.events-test
|
||||
(:require [cljs.test :refer [deftest testing is]]
|
||||
matcher-combinators.test
|
||||
[react-native.biometrics :as biometrics]
|
||||
[status-im.common.biometric.events :as sut]
|
||||
[status-im.constants :as constants]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(deftest set-supported-biometrics-type-test
|
||||
(testing "successfully setting supported biometrics type"
|
||||
(let [cofx {:db {}}
|
||||
supported-type constants/biometrics-type-face-id
|
||||
expected {:db (assoc (:db cofx) :biometric/supported-type supported-type)}]
|
||||
(is (match? expected (sut/set-supported-type cofx [supported-type])))))
|
||||
|
||||
(testing "throws error when setting unsupported biometrics type"
|
||||
(let [cofx {:db {}}
|
||||
supported-type :unsupported-type]
|
||||
(is (thrown? js/Error (sut/set-supported-type cofx [supported-type]))))))
|
||||
|
||||
(deftest show-message-test
|
||||
(testing "informs the user to enable biometrics from settings"
|
||||
(let [cofx {:db {}}
|
||||
expected {:fx [[:effects.utils/show-popup
|
||||
{:title (i18n/label :t/biometric-auth-login-error-title)
|
||||
:content (i18n/label :t/grant-face-id-permissions)}]]}]
|
||||
(is (match? expected
|
||||
(sut/show-message cofx
|
||||
[::biometrics/not-available])))))
|
||||
|
||||
(testing "shows a generic error message"
|
||||
(let [cofx {:db {}}
|
||||
error-cause :test-error
|
||||
expected {:fx [[:effects.utils/show-popup
|
||||
{:title (i18n/label :t/biometric-auth-login-error-title)
|
||||
:content (i18n/label :t/biometric-auth-error {:code error-cause})}]]}]
|
||||
(is (match? expected
|
||||
(sut/show-message cofx
|
||||
[error-cause]))))))
|
||||
|
||||
(deftest authenticate-biometrics-test
|
||||
(testing "passing the right args to authenticate"
|
||||
(let [cofx {:db {}}
|
||||
args {:on-success identity
|
||||
:on-fail identity
|
||||
:prompt-message "test"}
|
||||
result (sut/authenticate cofx [args])]
|
||||
(is (= (:prompt-message args) (get-in result [:fx 0 1 :prompt-message])))
|
||||
(is (not (nil? (get-in result [:fx 0 1 :on-success]))))
|
||||
(is (not (nil? (get-in result [:fx 0 1 :on-fail]))))))
|
||||
|
||||
(testing "skips biometric check if another one pending"
|
||||
(let [cofx {:db {:biometric/auth-pending? true}}
|
||||
result (sut/authenticate cofx [{}])]
|
||||
(is (nil? result)))))
|
||||
|
||||
(deftest enable-biometrics-test
|
||||
(testing "successfully enabling biometrics"
|
||||
(let [key-uid "test-uid"
|
||||
password (security/mask-data "test-pw")
|
||||
cofx {:db {:profile/profile {:key-uid key-uid}}}
|
||||
expected-db (assoc (:db cofx) :auth-method constants/auth-method-biometric)
|
||||
result (sut/enable-biometrics cofx [password])]
|
||||
(is (match? expected-db (:db result)))
|
||||
(is (= password (get-in result [:fx 0 1 1 :masked-password])))))
|
||||
|
||||
(testing "throws error if raw password is passed"
|
||||
(let [key-uid "test-uid"
|
||||
password "test-password"
|
||||
cofx {:db {:profile/profile {:key-uid key-uid}}}]
|
||||
(is (thrown? js/Error (sut/enable-biometrics cofx [password]))))))
|
||||
|
||||
(deftest disable-biometrics-test
|
||||
(testing "successfully disabling biometrics"
|
||||
(let [key-uid "test-uid"
|
||||
cofx {:db {:profile/profile {:key-uid key-uid}}}
|
||||
expected {:db (assoc (:db cofx) :auth-method constants/auth-method-none)
|
||||
:fx [[:keychain/clear-user-password key-uid]]}]
|
||||
(is (match? expected (sut/disable-biometrics cofx))))))
|
||||
@@ -0,0 +1,36 @@
|
||||
(ns status-im.common.biometric.utils
|
||||
(:require
|
||||
[native-module.core :as native-module]
|
||||
[react-native.platform :as platform]
|
||||
[status-im.constants :as constants]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def android-device-blacklisted?
|
||||
(and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand")))
|
||||
|
||||
(defn get-label-by-type
|
||||
[biometric-type]
|
||||
(condp = biometric-type
|
||||
constants/biometrics-type-android (i18n/label :t/biometric-fingerprint)
|
||||
constants/biometrics-type-face-id (i18n/label :t/biometric-faceid)
|
||||
(i18n/label :t/biometric-touchid)))
|
||||
|
||||
(defn get-icon-by-type
|
||||
[biometric-type]
|
||||
(condp = biometric-type
|
||||
constants/biometrics-type-face-id :i/face-id
|
||||
:i/touch-id))
|
||||
|
||||
;; NOTE: move to utils.re-frame?
|
||||
(defn handle-cb
|
||||
"Handles re-frame callbacks that are passed as anonymous fn or as dispatch vectors to effects.
|
||||
|
||||
e.g.:\n
|
||||
* with dispatch vector - `:on-success [:my-ns/my-event]`\n
|
||||
* with fn - `:on-success #(rf/dispatch [:my-ns/my-event])`"
|
||||
[cb & args]
|
||||
(when cb
|
||||
(cond
|
||||
(fn? cb) (apply cb args)
|
||||
(vector? cb) (rf/dispatch (into cb args)))))
|
||||
@@ -9,7 +9,6 @@
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.common.lightbox.style :as style]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
|
||||
@@ -40,25 +39,6 @@
|
||||
(anim/animate top-view-width screen-width)
|
||||
(anim/animate top-view-bg colors/neutral-100-opa-0)))))
|
||||
|
||||
(defn drawer
|
||||
[images index]
|
||||
(let [{:keys [image]} (nth images index)
|
||||
uri (url/replace-port image (rf/sub [:mediaserver/port]))]
|
||||
[quo/action-drawer
|
||||
[[{:icon :i/save
|
||||
:accessibility-label :save-image
|
||||
:label (i18n/label :t/save-image-library)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch
|
||||
[:lightbox/save-image-to-gallery
|
||||
uri
|
||||
#(rf/dispatch [:toasts/upsert
|
||||
{:id :random-id
|
||||
:type :positive
|
||||
:container-style {:bottom (when platform/android? 20)}
|
||||
:text (i18n/label :t/photo-saved)}])]))}]]]))
|
||||
|
||||
(defn share-image
|
||||
[images index]
|
||||
(let [{:keys [image]} (nth images index)
|
||||
@@ -66,7 +46,7 @@
|
||||
(rf/dispatch [:lightbox/share-image uri])))
|
||||
|
||||
(defn top-view
|
||||
[images insets index animations derived landscape? screen-width]
|
||||
[images insets index animations derived landscape? screen-width on-options-press]
|
||||
(let [{:keys [description header]} (nth images @index)
|
||||
bg-color (if landscape?
|
||||
colors/neutral-100-opa-70
|
||||
@@ -113,7 +93,6 @@
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:accessibility-label :image-options
|
||||
:on-press #(rf/dispatch [:show-bottom-sheet
|
||||
{:content (fn [] [drawer images @index])}])
|
||||
:on-press #(on-options-press images @index)
|
||||
:style style/close-container}
|
||||
[quo/icon :options {:size 20 :color colors/white}]]]]))
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
[rn/view {:style {:width constants/separator-width}}]])
|
||||
|
||||
(defn lightbox-content
|
||||
[{:keys [props state animations derived images index handle-items-changed bottom-text-component]}]
|
||||
[{:keys [props state animations derived images index handle-items-changed bottom-text-component
|
||||
on-options-press]}]
|
||||
(let [{:keys [data transparent? scroll-index
|
||||
set-full-height?]} state
|
||||
insets (safe-area/get-insets)
|
||||
@@ -69,7 +70,7 @@
|
||||
{:height screen-height})}
|
||||
(when-not @transparent?
|
||||
[:f> top-view/top-view images insets scroll-index animations derived landscape?
|
||||
screen-width])
|
||||
screen-width on-options-press])
|
||||
[gesture/gesture-detector
|
||||
{:gesture (utils/drag-gesture animations (and landscape? platform/ios?) set-full-height?)}
|
||||
[reanimated/view
|
||||
@@ -136,7 +137,8 @@
|
||||
|
||||
(defn- f-lightbox
|
||||
[]
|
||||
(let [{:keys [images index bottom-text-component]} (rf/sub [:get-screen-params])
|
||||
(let [{:keys [images index bottom-text-component on-options-press]}
|
||||
(rf/sub [:get-screen-params])
|
||||
props
|
||||
(utils/init-props)
|
||||
state
|
||||
@@ -160,7 +162,8 @@
|
||||
:images images
|
||||
:index index
|
||||
:handle-items-changed handle-items-changed
|
||||
:bottom-text-component bottom-text-component}]))))
|
||||
:bottom-text-component bottom-text-component
|
||||
:on-options-press on-options-press}]))))
|
||||
|
||||
(defn lightbox
|
||||
[]
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
:discover (js/require "../resources/images/ui2/discover.png")
|
||||
:invite-friends (js/require "../resources/images/ui2/invite-friends.png")
|
||||
:transaction-progress (js/require "../resources/images/ui2/transaction-progress.png")
|
||||
:welcome-illustration (js/require "../resources/images/ui2/welcome_illustration.png")})
|
||||
:welcome-illustration (js/require "../resources/images/ui2/welcome_illustration.png")
|
||||
:notifications (js/require "../resources/images/ui2/notifications.png")})
|
||||
|
||||
(def ui-themed
|
||||
{:angry-man
|
||||
|
||||
@@ -6,47 +6,59 @@
|
||||
[status-im.common.standard-authentication.password-input.view :as password-input]
|
||||
[status-im.contexts.profile.utils :as profile.utils]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
[utils.re-frame :as rf]
|
||||
[reagent.core :as reagent]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(defn view
|
||||
[{:keys [on-enter-password on-press-biometrics button-label button-icon-left]}]
|
||||
(let [{:keys [key-uid customization-color] :as profile} (rf/sub [:profile/profile-with-image])
|
||||
{:keys [error processing password]} (rf/sub [:profile/login])
|
||||
sign-in-enabled? (rf/sub [:sign-in-enabled?])]
|
||||
[:<>
|
||||
[rn/view {:style style/enter-password-container}
|
||||
[rn/view
|
||||
[quo/text
|
||||
{:accessibility-label :sync-code-generated
|
||||
:weight :bold
|
||||
:size :heading-1
|
||||
:style {:margin-bottom 4}}
|
||||
(i18n/label :t/enter-password)]
|
||||
[rn/view
|
||||
{:style style/context-tag}
|
||||
[quo/context-tag
|
||||
{:type :default
|
||||
:blur? true
|
||||
:profile-picture (profile.utils/photo profile)
|
||||
:full-name (profile.utils/displayed-name profile)
|
||||
:customization-color customization-color
|
||||
:size 24}]]
|
||||
[password-input/view
|
||||
{:on-press-biometrics on-press-biometrics
|
||||
:processing processing
|
||||
:error error
|
||||
:default-password password
|
||||
:sign-in-enabled? sign-in-enabled?}]
|
||||
[quo/button
|
||||
{:size 40
|
||||
:container-style style/enter-password-button
|
||||
:type :primary
|
||||
:customization-color (or customization-color :primary)
|
||||
:accessibility-label :login-button
|
||||
:icon-left button-icon-left
|
||||
:disabled? (or (not sign-in-enabled?) processing)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:set-in [:profile/login :key-uid] key-uid])
|
||||
(rf/dispatch [:profile.login/verify-database-password password
|
||||
#(on-enter-password password)]))}
|
||||
button-label]]]]))
|
||||
[]
|
||||
(let [password-value (reagent/atom nil)]
|
||||
(fn [{:keys [on-enter-password on-press-biometrics button-label button-icon-left]}]
|
||||
(let [{:keys [key-uid customization-color]
|
||||
:as profile} (rf/sub [:profile/profile-with-image])
|
||||
{:keys [error processing password]} (rf/sub [:profile/login])
|
||||
sign-in-enabled? (rf/sub [:sign-in-enabled?])
|
||||
on-change-password (fn [value]
|
||||
(reset! password-value value)
|
||||
(when (not= value (security/safe-unmask-data password))
|
||||
(rf/dispatch [:set-in [:profile/login :password]
|
||||
(security/mask-data value)])
|
||||
(rf/dispatch [:set-in [:profile/login :error] ""])))]
|
||||
[:<>
|
||||
[rn/view {:style style/enter-password-container}
|
||||
[rn/view
|
||||
[quo/text
|
||||
{:accessibility-label :sync-code-generated
|
||||
:weight :bold
|
||||
:size :heading-1
|
||||
:style {:margin-bottom 4}}
|
||||
(i18n/label :t/enter-password)]
|
||||
[rn/view
|
||||
{:style style/context-tag}
|
||||
[quo/context-tag
|
||||
{:type :default
|
||||
:blur? true
|
||||
:profile-picture (profile.utils/photo profile)
|
||||
:full-name (profile.utils/displayed-name profile)
|
||||
:customization-color customization-color
|
||||
:size 24}]]
|
||||
[password-input/view
|
||||
{:on-press-biometrics on-press-biometrics
|
||||
:processing processing
|
||||
:error error
|
||||
:password @password-value
|
||||
:on-change-password on-change-password
|
||||
:sign-in-enabled? sign-in-enabled?}]
|
||||
[quo/button
|
||||
{:size 40
|
||||
:container-style style/enter-password-button
|
||||
:type :primary
|
||||
:customization-color (or customization-color :primary)
|
||||
:accessibility-label :login-button
|
||||
:icon-left button-icon-left
|
||||
:disabled? (or (not sign-in-enabled?) processing)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:set-in [:profile/login :key-uid] key-uid])
|
||||
(rf/dispatch [:profile.login/verify-database-password password
|
||||
#(on-enter-password password)]))}
|
||||
button-label]]]]))))
|
||||
|
||||
@@ -1,11 +1,86 @@
|
||||
(ns status-im.common.standard-authentication.events
|
||||
(:require
|
||||
[utils.re-frame :as rf]))
|
||||
[schema.core :as schema]
|
||||
[status-im.common.standard-authentication.enter-password.view :as enter-password]
|
||||
[status-im.common.standard-authentication.events-schema :as events-schema]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(rf/reg-event-fx :standard-auth/on-biometric-success
|
||||
(fn [{:keys [db]} [callback]]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:fx [[:keychain/get-user-password [key-uid callback]]]})))
|
||||
(defn authorize
|
||||
[{:keys [db]} [args]]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:fx [[:biometric/check-if-available
|
||||
{:key-uid key-uid
|
||||
:on-success [:standard-auth/authorize-with-biometric args]
|
||||
:on-fail [:standard-auth/authorize-with-password args]}]]}))
|
||||
|
||||
(schema/=> authorize events-schema/?authorize)
|
||||
(rf/reg-event-fx :standard-auth/authorize authorize)
|
||||
|
||||
(defn authorize-with-biometric
|
||||
[_ [{:keys [on-auth-success on-auth-fail] :as args}]]
|
||||
(let [args-with-biometric-btn
|
||||
(assoc args
|
||||
:on-press-biometric
|
||||
#(rf/dispatch [:standard-auth/authorize-with-biometric args]))]
|
||||
{:fx [[:dispatch [:dismiss-keyboard]]
|
||||
[:dispatch
|
||||
[:biometric/authenticate
|
||||
{:prompt-message (i18n/label :t/biometric-auth-confirm-message)
|
||||
:on-cancel [:standard-auth/authorize-with-password args-with-biometric-btn]
|
||||
:on-success [:standard-auth/on-biometric-success on-auth-success]
|
||||
:on-fail [:standard-auth/on-biometric-fail on-auth-fail]}]]]}))
|
||||
|
||||
(schema/=> authorize-with-biometric events-schema/?authorize-with-biometric)
|
||||
(rf/reg-event-fx :standard-auth/authorize-with-biometric authorize-with-biometric)
|
||||
|
||||
(defn on-biometric-success
|
||||
[{:keys [db]} [on-auth-success]]
|
||||
(let [key-uid (get-in db [:profile/profile :key-uid])]
|
||||
{:fx [[:keychain/get-user-password [key-uid on-auth-success]]]}))
|
||||
|
||||
(schema/=> on-biometric-success events-schema/?on-biometric-success)
|
||||
(rf/reg-event-fx :standard-auth/on-biometric-success on-biometric-success)
|
||||
|
||||
(defn on-biometric-fail
|
||||
[_ [on-auth-fail error]]
|
||||
(when on-auth-fail
|
||||
(on-auth-fail error))
|
||||
(log/error (ex-message error)
|
||||
(-> error
|
||||
ex-data
|
||||
(assoc :code (ex-cause error)
|
||||
:event :standard-auth/on-biometric-fail)))
|
||||
{:fx [[:dispatch [:biometric/show-message (ex-cause error)]]]})
|
||||
|
||||
(schema/=> on-biometric-fail events-schema/?on-biometrics-fail)
|
||||
(rf/reg-event-fx :standard-auth/on-biometric-fail on-biometric-fail)
|
||||
|
||||
(defn- bottom-sheet-password-view
|
||||
[{:keys [on-press-biometric on-auth-success auth-button-icon-left auth-button-label]}]
|
||||
(fn []
|
||||
(let [handle-password-success (fn [password]
|
||||
(-> password security/hash-masked-password on-auth-success))]
|
||||
[enter-password/view
|
||||
{:on-enter-password handle-password-success
|
||||
:on-press-biometrics on-press-biometric
|
||||
:button-icon-left auth-button-icon-left
|
||||
:button-label auth-button-label}])))
|
||||
|
||||
(defn authorize-with-password
|
||||
[_ [{:keys [on-close theme blur?] :as args}]]
|
||||
{:fx [[:dispatch [:standard-auth/reset-login-password]]
|
||||
[:dispatch
|
||||
[:show-bottom-sheet
|
||||
{:on-close on-close
|
||||
:theme theme
|
||||
:shell? blur?
|
||||
:content #(bottom-sheet-password-view args)}]]]})
|
||||
|
||||
(schema/=> authorize-with-password events-schema/?authorize-with-password)
|
||||
(rf/reg-event-fx :standard-auth/authorize-with-password authorize-with-password)
|
||||
|
||||
(rf/reg-event-fx
|
||||
:standard-auth/reset-login-password
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
(ns status-im.common.standard-authentication.events-schema)
|
||||
|
||||
(def ^:private ?authorize-map
|
||||
[:map {:closed true}
|
||||
[:on-auth-success fn?]
|
||||
[:on-auth-fail {:optional true} [:maybe fn?]]
|
||||
[:on-close {:optional true} [:maybe fn?]]
|
||||
[:auth-button-label {:optional true} [:maybe string?]]
|
||||
[:auth-button-icon-left {:optional true} [:maybe keyword?]]
|
||||
[:blur? {:optional true} [:maybe boolean?]]
|
||||
[:theme {:optional true} [:maybe :schema.common/theme]]])
|
||||
|
||||
(def ?authorize
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple ?authorize-map]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?authorize-with-biometric
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple ?authorize-map]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?on-biometric-success
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple fn?]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?on-biometrics-fail
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple
|
||||
[:maybe fn?]
|
||||
[:maybe :schema.common/error]]]]
|
||||
:schema.re-frame/event-fx])
|
||||
|
||||
(def ?authorize-with-password
|
||||
[:=>
|
||||
[:catn
|
||||
[:cofx :schema.re-frame/cofx]
|
||||
[:args
|
||||
[:tuple ?authorize-map]]]
|
||||
:schema.re-frame/event-fx])
|
||||
@@ -9,7 +9,8 @@
|
||||
[status-im.common.standard-authentication.password-input.style :as style]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
[utils.security.core :as security]
|
||||
[reagent.core :as reagent]))
|
||||
|
||||
(defn get-error-message
|
||||
[error]
|
||||
@@ -20,14 +21,8 @@
|
||||
(i18n/label :t/oops-wrong-password)
|
||||
error))
|
||||
|
||||
(defn- on-change-password
|
||||
[entered-password]
|
||||
(rf/dispatch [:set-in [:profile/login :password]
|
||||
(security/mask-data entered-password)])
|
||||
(rf/dispatch [:set-in [:profile/login :error] ""]))
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [default-password theme shell? on-press-biometrics blur?]}]
|
||||
[{:keys [password on-change-password theme shell? on-press-biometrics blur?]}]
|
||||
(let [{:keys [error processing]} (rf/sub [:profile/login])
|
||||
error-message (get-error-message error)
|
||||
error? (boolean (seq error-message))]
|
||||
@@ -43,7 +38,7 @@
|
||||
:error? error?
|
||||
:label (i18n/label :t/profile-password)
|
||||
:on-change-text on-change-password
|
||||
:default-value (security/safe-unmask-data default-password)}]
|
||||
:value password}]
|
||||
(when on-press-biometrics
|
||||
[quo/button
|
||||
{:container-style style/auth-button
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
(ns status-im.common.standard-authentication.standard-auth.authorize
|
||||
(:require
|
||||
[react-native.biometrics :as biometrics]
|
||||
[status-im.common.standard-authentication.enter-password.view :as enter-password]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(defn reset-password
|
||||
[]
|
||||
(rf/dispatch [:set-in [:profile/login :password] nil])
|
||||
(rf/dispatch [:set-in [:profile/login :error] ""]))
|
||||
|
||||
(defn authorize
|
||||
[{:keys [biometric-auth? on-auth-success on-auth-fail on-close
|
||||
auth-button-label theme blur? auth-button-icon-left]}]
|
||||
(let [handle-auth-success (fn [biometric?]
|
||||
(fn [entered-password]
|
||||
(let [sha3-masked-password (if biometric?
|
||||
entered-password
|
||||
(security/hash-masked-password
|
||||
entered-password))]
|
||||
(on-auth-success sha3-masked-password))))
|
||||
password-login (fn [{:keys [on-press-biometrics]}]
|
||||
(rf/dispatch [:show-bottom-sheet
|
||||
{:on-close on-close
|
||||
:theme theme
|
||||
:shell? blur?
|
||||
:content (fn []
|
||||
[enter-password/view
|
||||
{:on-enter-password (handle-auth-success
|
||||
false)
|
||||
:on-press-biometrics on-press-biometrics
|
||||
:button-icon-left auth-button-icon-left
|
||||
:button-label auth-button-label}])}]))
|
||||
; biometrics-login recursively passes itself as a parameter because if the user
|
||||
; fails biometric auth they will be shown the password bottom sheet with an option
|
||||
; to retrigger biometric auth, so they can endlessly repeat this cycle.
|
||||
biometrics-login (fn [on-press-biometrics]
|
||||
(rf/dispatch [:dismiss-keyboard])
|
||||
(rf/dispatch
|
||||
[:biometric/authenticate
|
||||
{:prompt-message (i18n/label :t/biometric-auth-confirm-message)
|
||||
:on-success (fn []
|
||||
(on-close)
|
||||
(rf/dispatch [:standard-auth/on-biometric-success
|
||||
(handle-auth-success true)]))
|
||||
:on-fail (fn [error]
|
||||
(on-close)
|
||||
(log/error
|
||||
(ex-message error)
|
||||
(-> error ex-data (assoc :code (ex-cause error))))
|
||||
(when on-auth-fail (on-auth-fail error))
|
||||
(password-login {:on-press-biometrics
|
||||
#(on-press-biometrics
|
||||
on-press-biometrics)}))}]))]
|
||||
(if biometric-auth?
|
||||
(-> (biometrics/get-supported-type)
|
||||
(.then (fn [biometric-type]
|
||||
(if biometric-type
|
||||
(biometrics-login biometrics-login)
|
||||
(do
|
||||
(reset-password)
|
||||
(password-login {})))))
|
||||
(.catch #(password-login {})))
|
||||
(password-login {}))))
|
||||
@@ -4,7 +4,6 @@
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
|
||||
[status-im.constants :as constants]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
@@ -33,14 +32,15 @@
|
||||
{:size size
|
||||
: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?
|
||||
:biometric-auth? biometric-auth?
|
||||
:on-auth-success on-auth-success
|
||||
:on-auth-fail on-auth-fail
|
||||
:auth-button-label auth-button-label})
|
||||
:on-complete #(rf/dispatch [:standard-auth/authorize
|
||||
{:on-close on-close
|
||||
:auth-button-icon-left auth-button-icon-left
|
||||
:theme theme
|
||||
:blur? blur?
|
||||
: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}]])))
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
|
||||
(def default-kdf-iterations 3200)
|
||||
|
||||
(def community-accounts-selection-enabled? false)
|
||||
(def community-accounts-selection-enabled? (enabled? (get-config :ACCOUNT_SELECTION_ENABLED "0")))
|
||||
(def fetch-messages-enabled? (enabled? (get-config :FETCH_MESSAGES_ENABLED "1")))
|
||||
|
||||
(def wallet-feature-flags
|
||||
|
||||
@@ -153,6 +153,11 @@
|
||||
(def ^:const community-token-permission-can-view-and-post-channel 4)
|
||||
(def ^:const community-token-permission-become-token-master 5)
|
||||
(def ^:const community-token-permission-become-token-owner 6)
|
||||
(def ^:const community-role-permissions
|
||||
#{community-token-permission-become-admin
|
||||
community-token-permission-become-member
|
||||
community-token-permission-become-token-master
|
||||
community-token-permission-become-token-owner})
|
||||
|
||||
;; Community rules for joining
|
||||
(def ^:const community-rule-ens-only "ens-only")
|
||||
|
||||
@@ -66,7 +66,12 @@
|
||||
album-messages))
|
||||
:bottom-text-component
|
||||
[lightbox/bottom-text-for-lightbox
|
||||
first-image]}])}
|
||||
first-image]
|
||||
:on-options-press (fn [images index]
|
||||
(rf/dispatch [:show-bottom-sheet
|
||||
{:content (fn [] [lightbox/drawer
|
||||
images
|
||||
index])}]))}])}
|
||||
[fast-image/fast-image
|
||||
{:style (style/image dimensions index portrait? images-count)
|
||||
:source {:uri (url/replace-port (:image (:content item)) media-server-port)}
|
||||
|
||||
@@ -57,7 +57,13 @@
|
||||
:index 0
|
||||
:insets insets
|
||||
:bottom-text-component
|
||||
[lightbox/bottom-text-for-lightbox message]}])}
|
||||
[lightbox/bottom-text-for-lightbox message]
|
||||
:on-options-press (fn [images index]
|
||||
(rf/dispatch [:show-bottom-sheet
|
||||
{:content (fn []
|
||||
[lightbox/drawer
|
||||
images
|
||||
index])}]))}])}
|
||||
[fast-image/fast-image
|
||||
{:source {:uri image-local-url}
|
||||
:style (merge dimensions {:border-radius 12})
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
(ns status-im.contexts.chat.messenger.messages.content.lightbox.view
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[quo.core :as quo]
|
||||
[react-native.platform :as platform]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.messages.content.lightbox.style :as style]
|
||||
[status-im.contexts.chat.messenger.messages.content.text.view :as message-view]))
|
||||
[status-im.contexts.chat.messenger.messages.content.text.view :as message-view]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
|
||||
(defn bottom-text-for-lightbox
|
||||
[_]
|
||||
@@ -16,3 +21,22 @@
|
||||
:chat-id chat-id
|
||||
:style-override (style/bottom-text expandable-text?)
|
||||
:on-layout #(reset! text-height (oops/oget % "nativeEvent.layout.height"))}]))))
|
||||
|
||||
(defn drawer
|
||||
[images index]
|
||||
(let [{:keys [image]} (nth images index)
|
||||
uri (url/replace-port image (rf/sub [:mediaserver/port]))]
|
||||
[quo/action-drawer
|
||||
[[{:icon :i/save
|
||||
:accessibility-label :save-image
|
||||
:label (i18n/label :t/save-image-library)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch
|
||||
[:lightbox/save-image-to-gallery
|
||||
uri
|
||||
#(rf/dispatch [:toasts/upsert
|
||||
{:id :random-id
|
||||
:type :positive
|
||||
:container-style {:bottom (when platform/android? 20)}
|
||||
:text (i18n/label :t/photo-saved)}])]))}]]]))
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
[quo/drawer-top
|
||||
{:type :context-tag
|
||||
:title (i18n/label :t/addresses-for-permissions)
|
||||
:context-tag-type :community
|
||||
:community-name name
|
||||
:button-icon :i/info
|
||||
:on-button-press not-implemented/alert
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[:<>
|
||||
[quo/drawer-top
|
||||
{:type :context-tag
|
||||
:context-tag-type :community
|
||||
:title (i18n/label :t/airdrop-addresses)
|
||||
:community-name name
|
||||
:button-icon :i/info
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
(defn- view-internal
|
||||
[{:keys [theme]}]
|
||||
(fn []
|
||||
(let [{:keys [name
|
||||
id
|
||||
images]} (rf/sub [:get-screen-params])
|
||||
{:keys [color]} (rf/sub [:communities/community id])]
|
||||
(let [{:keys [id]} (rf/sub [:get-screen-params])
|
||||
{:keys [color name images]} (rf/sub [:communities/community id])]
|
||||
[rn/safe-area-view {:flex 1}
|
||||
[gesture/scroll-view {:style style/container}
|
||||
[rn/view style/page-container
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.common.home.actions.view :as actions]
|
||||
[status-im.common.password-authentication.view :as password-authentication]
|
||||
[status-im.common.scroll-page.style :as scroll-page.style]
|
||||
[status-im.common.scroll-page.view :as scroll-page]
|
||||
[status-im.config :as config]
|
||||
@@ -15,38 +14,20 @@
|
||||
[status-im.contexts.communities.actions.chat.view :as chat-actions]
|
||||
[status-im.contexts.communities.actions.community-options.view :as options]
|
||||
[status-im.contexts.communities.overview.style :as style]
|
||||
[status-im.contexts.communities.overview.utils :as utils]
|
||||
[status-im.contexts.communities.utils :as communities.utils]
|
||||
[utils.debounce :as debounce]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn preview-user-list
|
||||
[user-list]
|
||||
(when (seq user-list)
|
||||
[rn/view style/preview-user
|
||||
[quo/preview-list
|
||||
{:type :user
|
||||
:number (count user-list)
|
||||
:size :size-24}
|
||||
user-list]
|
||||
[quo/text
|
||||
{:accessibility-label :communities-screen-title
|
||||
:style {:margin-left 8}
|
||||
:size :label}
|
||||
(utils/join-existing-users-string user-list)]]))
|
||||
|
||||
(defn add-category-height
|
||||
(defn- add-category-height
|
||||
[categories-heights category height]
|
||||
(swap! categories-heights
|
||||
(fn [heights]
|
||||
(assoc heights category height))))
|
||||
(swap! categories-heights assoc category height))
|
||||
|
||||
(defn collapse-category
|
||||
(defn- collapse-category
|
||||
[community-id category-id collapsed?]
|
||||
(rf/dispatch [:communities/toggle-collapsed-category community-id category-id (not collapsed?)]))
|
||||
|
||||
(defn layout-y
|
||||
(defn- layout-y
|
||||
[event]
|
||||
(oops/oget event "nativeEvent.layout.y"))
|
||||
|
||||
@@ -77,7 +58,7 @@
|
||||
{:on-press on-press
|
||||
:on-long-press #(rf/dispatch [:show-bottom-sheet channel-sheet-data])})]]))
|
||||
|
||||
(defn channel-list-component
|
||||
(defn- channel-list-component
|
||||
[{:keys [on-category-layout community-id community-color on-first-channel-height-changed]}
|
||||
channels-list]
|
||||
[rn/view
|
||||
@@ -104,7 +85,7 @@
|
||||
^{:key (:id chat)}
|
||||
[channel-chat-item community-id community-color chat])])])])
|
||||
|
||||
(defn get-access-type
|
||||
(defn- get-access-type
|
||||
[access]
|
||||
(condp = access
|
||||
constants/community-no-membership-access :open
|
||||
@@ -112,17 +93,9 @@
|
||||
constants/community-on-request-access :request-access
|
||||
:unknown-access))
|
||||
|
||||
(defn join-gated-community
|
||||
[id]
|
||||
(rf/dispatch [:password-authentication/show
|
||||
{:content (fn [] [password-authentication/view])}
|
||||
{:label (i18n/label :t/join-open-community)
|
||||
:on-press #(rf/dispatch [:communities/request-to-join
|
||||
{:community-id id :password %}])}]))
|
||||
|
||||
(defn info-button
|
||||
(defn- info-button
|
||||
[]
|
||||
[rn/touchable-without-feedback
|
||||
[rn/pressable
|
||||
{:on-press
|
||||
#(rf/dispatch
|
||||
[:show-bottom-sheet
|
||||
@@ -137,119 +110,116 @@
|
||||
[rn/view
|
||||
[quo/icon :i/info {:no-color true}]]])
|
||||
|
||||
(defn token-gates
|
||||
[]
|
||||
(fn [{:keys [id color]}]
|
||||
(let [{:keys [can-request-access?
|
||||
number-of-hold-tokens tokens
|
||||
highest-permission-role]} (rf/sub [:community/token-gated-overview id])
|
||||
highest-role-text
|
||||
(i18n/label
|
||||
(communities.utils/role->translation-key highest-permission-role :t/member))]
|
||||
[rn/view {:style (style/token-gated-container)}
|
||||
[rn/view
|
||||
{:style {:padding-horizontal 12
|
||||
:flex-direction :row
|
||||
:align-items :center
|
||||
:justify-content :space-between
|
||||
:flex 1}}
|
||||
[quo/text {:weight :medium}
|
||||
(if can-request-access?
|
||||
(i18n/label :t/you-eligible-to-join-as {:role highest-role-text})
|
||||
(i18n/label :t/you-not-eligible-to-join))]
|
||||
[info-button]]
|
||||
(when (pos? number-of-hold-tokens)
|
||||
[quo/text {:style {:padding-horizontal 12 :padding-bottom 18} :size :paragraph-2}
|
||||
(if can-request-access?
|
||||
(i18n/label :t/you-hold-number-of-hold-tokens-of-these
|
||||
{:number-of-hold-tokens number-of-hold-tokens})
|
||||
(i18n/label :t/you-must-hold))])
|
||||
[quo/token-requirement-list
|
||||
{:tokens tokens
|
||||
:padding? true}]
|
||||
[quo/button
|
||||
{:on-press
|
||||
(if config/community-accounts-selection-enabled?
|
||||
#(rf/dispatch [:open-modal :community-account-selection {:community-id id}])
|
||||
#(join-gated-community id))
|
||||
:accessibility-label :join-community-button
|
||||
:customization-color color
|
||||
:container-style {:margin-horizontal 12 :margin-top 12 :margin-bottom 12}
|
||||
:disabled? (not can-request-access?)
|
||||
:icon-left (if can-request-access? :i/unlocked :i/locked)}
|
||||
(i18n/label :t/join-open-community)]])))
|
||||
(defn- token-requirements
|
||||
[{:keys [id color]}]
|
||||
(let [{:keys [can-request-access?
|
||||
number-of-hold-tokens tokens
|
||||
highest-permission-role]} (rf/sub [:community/token-gated-overview id])
|
||||
highest-role-text
|
||||
(i18n/label
|
||||
(communities.utils/role->translation-key highest-permission-role :t/member))]
|
||||
[rn/view {:style (style/token-gated-container)}
|
||||
[rn/view
|
||||
{:style {:padding-horizontal 12
|
||||
:flex-direction :row
|
||||
:align-items :center
|
||||
:justify-content :space-between
|
||||
:flex 1}}
|
||||
[quo/text {:weight :medium}
|
||||
(if can-request-access?
|
||||
(i18n/label :t/you-eligible-to-join-as {:role highest-role-text})
|
||||
(i18n/label :t/you-not-eligible-to-join))]
|
||||
[info-button]]
|
||||
(when (pos? number-of-hold-tokens)
|
||||
[quo/text {:style {:padding-horizontal 12 :padding-bottom 18} :size :paragraph-2}
|
||||
(if can-request-access?
|
||||
(i18n/label :t/you-hold-number-of-hold-tokens-of-these
|
||||
{:number-of-hold-tokens number-of-hold-tokens})
|
||||
(i18n/label :t/you-must-hold))])
|
||||
[quo/token-requirement-list
|
||||
{:tokens tokens
|
||||
:padding? true}]
|
||||
[quo/button
|
||||
{:on-press (if config/community-accounts-selection-enabled?
|
||||
#(rf/dispatch [:open-modal :community-account-selection
|
||||
{:community-id id}])
|
||||
#(rf/dispatch [:open-modal :community-requests-to-join {:id id}]))
|
||||
:accessibility-label :join-community-button
|
||||
:customization-color color
|
||||
:container-style {:margin-horizontal 12 :margin-top 12 :margin-bottom 12}
|
||||
:disabled? (not can-request-access?)
|
||||
:icon-left (if can-request-access? :i/unlocked :i/locked)}
|
||||
(i18n/label :t/join-open-community)]]))
|
||||
|
||||
(defn join-community
|
||||
[{:keys [joined color permissions token-permissions membership-permissions? id] :as community}
|
||||
pending?]
|
||||
(let [access-type (get-access-type (:access permissions))
|
||||
(defn- join-community
|
||||
[{:keys [id color joined permissions role-permissions? can-join?] :as community}]
|
||||
(let [pending? (rf/sub [:communities/my-pending-request-to-join id])
|
||||
access-type (get-access-type (:access permissions))
|
||||
unknown-access? (= access-type :unknown-access)
|
||||
invite-only? (= access-type :invite-only)]
|
||||
[:<>
|
||||
(when-not (or joined pending? invite-only? unknown-access?)
|
||||
(if membership-permissions?
|
||||
[token-gates community]
|
||||
(if role-permissions?
|
||||
[token-requirements community]
|
||||
[quo/button
|
||||
{:on-press
|
||||
(if config/community-accounts-selection-enabled?
|
||||
#(rf/dispatch [:open-modal :community-account-selection {:community-id id}])
|
||||
#(rf/dispatch [:open-modal :community-requests-to-join community]))
|
||||
|
||||
{:on-press (if config/community-accounts-selection-enabled?
|
||||
#(rf/dispatch [:open-modal :community-account-selection
|
||||
{:community-id id}])
|
||||
#(rf/dispatch [:open-modal :community-requests-to-join {:id id}]))
|
||||
:accessibility-label :show-request-to-join-screen-button
|
||||
:customization-color color
|
||||
:icon-left :i/communities}
|
||||
:icon-left :i/communities}
|
||||
(i18n/label :t/request-to-join)]))
|
||||
|
||||
(when (not (or joined pending? token-permissions))
|
||||
(when (not (or pending? role-permissions? can-join?))
|
||||
[quo/text
|
||||
{:size :paragraph-2
|
||||
:weight :regular
|
||||
:style style/review-notice}
|
||||
(i18n/label :t/community-admins-will-review-your-request)])]))
|
||||
|
||||
(defn status-tag
|
||||
[pending? joined]
|
||||
(when (or pending? joined)
|
||||
[rn/view {:style {:position :absolute :top 12 :right 12}}
|
||||
[quo/status-tag
|
||||
{:status {:type (if joined :positive :pending)}
|
||||
:label (if joined
|
||||
(i18n/label :t/joined)
|
||||
(i18n/label :t/pending))}]]))
|
||||
(defn- status-tag
|
||||
[community-id joined]
|
||||
(let [pending? (rf/sub [:communities/my-pending-request-to-join community-id])]
|
||||
(when (or pending? joined)
|
||||
[rn/view {:style {:position :absolute :top 12 :right 12}}
|
||||
[quo/status-tag
|
||||
{:status {:type (if joined :positive :pending)}
|
||||
:label (if joined
|
||||
(i18n/label :t/joined)
|
||||
(i18n/label :t/pending))}]])))
|
||||
|
||||
(defn add-handlers
|
||||
(defn- add-handlers
|
||||
[community-id
|
||||
joined-or-spectated
|
||||
{:keys [id locked?]
|
||||
:or {locked? false}
|
||||
:as chat}]
|
||||
(merge
|
||||
chat
|
||||
(when (and (not locked?) id)
|
||||
{:on-press (when joined-or-spectated
|
||||
(fn []
|
||||
(rf/dispatch [:dismiss-keyboard])
|
||||
(debounce/dispatch-and-chill
|
||||
[:communities/navigate-to-community-chat (str community-id id)]
|
||||
1000)))
|
||||
:on-long-press #(rf/dispatch
|
||||
[:show-bottom-sheet
|
||||
{:content (fn []
|
||||
[chat-actions/actions chat false])}])
|
||||
:community-id community-id})))
|
||||
(cond-> chat
|
||||
(and (not locked?) id)
|
||||
(assoc :on-press (when joined-or-spectated
|
||||
(fn []
|
||||
(rf/dispatch [:dismiss-keyboard])
|
||||
(debounce/dispatch-and-chill
|
||||
[:communities/navigate-to-community-chat (str community-id id)]
|
||||
1000)))
|
||||
:on-long-press #(rf/dispatch
|
||||
[:show-bottom-sheet
|
||||
{:content (fn []
|
||||
[chat-actions/actions chat false])}])
|
||||
:community-id community-id)))
|
||||
|
||||
(defn add-handlers-to-chats
|
||||
(defn- add-handlers-to-chats
|
||||
[community-id joined-or-spectated chats]
|
||||
(mapv (partial add-handlers community-id joined-or-spectated) chats))
|
||||
|
||||
(defn add-handlers-to-categorized-chats
|
||||
(defn- add-handlers-to-categorized-chats
|
||||
[community-id categorized-chats joined-or-spectated]
|
||||
(let [add-on-press (partial add-handlers-to-chats community-id joined-or-spectated)]
|
||||
(map (fn [[category v]]
|
||||
[category (update v :chats add-on-press)])
|
||||
categorized-chats)))
|
||||
|
||||
(defn community-header
|
||||
(defn- community-header
|
||||
[title logo description]
|
||||
[quo/text-combinations
|
||||
{:container-style
|
||||
@@ -266,11 +236,11 @@
|
||||
:title-accessibility-label :community-title
|
||||
:description-accessibility-label :community-description}])
|
||||
|
||||
(defn community-content
|
||||
[community]
|
||||
(rf/dispatch [:communities/check-all-community-channels-permissions (:id community)])
|
||||
(fn [{:keys [name description joined spectated images tags color id token-permissions] :as community}
|
||||
pending?
|
||||
(defn- community-content
|
||||
[{:keys [id]}]
|
||||
(rf/dispatch [:communities/check-all-community-channels-permissions id])
|
||||
(fn [{:keys [name description joined spectated images tags color id membership-permissions?]
|
||||
:as community}
|
||||
{:keys [on-category-layout
|
||||
collapsed?
|
||||
on-first-channel-height-changed]}]
|
||||
@@ -279,7 +249,7 @@
|
||||
[:<>
|
||||
[rn/view {:style style/community-content-container}
|
||||
(when-not collapsed?
|
||||
[status-tag pending? joined])
|
||||
[status-tag id joined])
|
||||
[community-header name (when collapsed? (get-in images [:thumbnail :uri]))
|
||||
(when-not collapsed? description)]
|
||||
(when (and (seq tags) (not collapsed?))
|
||||
@@ -287,9 +257,8 @@
|
||||
{:tags tags
|
||||
:last-item-style style/last-community-tag
|
||||
:container-style style/community-tag-container}])
|
||||
[join-community community pending?]]
|
||||
(when (or (and (seq token-permissions) joined)
|
||||
(empty? token-permissions))
|
||||
[join-community community]]
|
||||
(when (or joined (not membership-permissions?))
|
||||
[channel-list-component
|
||||
{:on-category-layout on-category-layout
|
||||
:community-id id
|
||||
@@ -297,7 +266,7 @@
|
||||
:on-first-channel-height-changed on-first-channel-height-changed}
|
||||
(add-handlers-to-categorized-chats id chats-by-category joined-or-spectated)])])))
|
||||
|
||||
(defn sticky-category-header
|
||||
(defn- sticky-category-header
|
||||
[_]
|
||||
(fn [{:keys [enabled label]}]
|
||||
(when enabled
|
||||
@@ -310,7 +279,7 @@
|
||||
{:chevron :left}
|
||||
label]])))
|
||||
|
||||
(defn page-nav-right-section-buttons
|
||||
(defn- page-nav-right-section-buttons
|
||||
[id]
|
||||
[{:icon-name :i/options
|
||||
:accessibility-label :community-options-for-community
|
||||
@@ -318,7 +287,7 @@
|
||||
[:show-bottom-sheet
|
||||
{:content (fn [] [options/community-options-bottom-sheet id])}])}])
|
||||
|
||||
(defn pick-first-category-by-height
|
||||
(defn- pick-first-category-by-height
|
||||
[scroll-height first-channel-height categories-heights]
|
||||
(->> categories-heights
|
||||
(sort-by (comp - second))
|
||||
@@ -326,7 +295,7 @@
|
||||
(and (>= scroll-height (+ height first-channel-height))
|
||||
category)))))
|
||||
|
||||
(defn community-scroll-page
|
||||
(defn- community-scroll-page
|
||||
[{:keys [joined]}]
|
||||
(let [scroll-height (reagent/atom 0)
|
||||
categories-heights (reagent/atom {})
|
||||
@@ -336,7 +305,7 @@
|
||||
;; from not collapsed to collapsed if the
|
||||
;; user is on this page
|
||||
initial-joined? joined]
|
||||
(fn [{:keys [id name images] :as community} pending?]
|
||||
(fn [{:keys [id name images] :as community}]
|
||||
(let [cover {:uri (get-in images [:banner :uri])}
|
||||
logo {:uri (get-in images [:thumbnail :uri])}
|
||||
collapsed? (and initial-joined? (:joined community))
|
||||
@@ -363,7 +332,6 @@
|
||||
@categories-heights)}]}
|
||||
[community-content
|
||||
community
|
||||
pending?
|
||||
{:on-category-layout (partial add-category-height categories-heights)
|
||||
:collapsed? collapsed?
|
||||
:on-first-channel-height-changed
|
||||
@@ -373,17 +341,16 @@
|
||||
(swap! categories-heights select-keys categories)
|
||||
(reset! first-channel-height height))}]]))))
|
||||
|
||||
(defn community-card-page-view
|
||||
(defn- community-card-page-view
|
||||
[id]
|
||||
(let [{:keys [id joined]
|
||||
:as community} (rf/sub [:communities/community id])
|
||||
pending? (rf/sub [:communities/my-pending-request-to-join id])]
|
||||
:as community} (rf/sub [:communities/community id])]
|
||||
(when community
|
||||
(when joined
|
||||
(rf/dispatch [:activity-center.notifications/dismiss-community-overview id]))
|
||||
[community-scroll-page community pending?])))
|
||||
[community-scroll-page community])))
|
||||
|
||||
(defn overview
|
||||
(defn view
|
||||
[id]
|
||||
(let [id (or id (rf/sub [:get-screen-params :community-overview]))
|
||||
customization-color (rf/sub [:profile/customization-color])]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[quo.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im.common.biometric.events :as biometric]
|
||||
[status-im.common.biometric.utils :as biometric]
|
||||
[status-im.common.parallax.view :as parallax]
|
||||
[status-im.common.parallax.whitelist :as whitelist]
|
||||
[status-im.common.resources :as resources]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require
|
||||
[native-module.core :as native-module]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.common.biometric.events :as biometric]
|
||||
status-im.common.biometric.events
|
||||
[status-im.constants :as constants]
|
||||
[status-im.contexts.profile.create.events :as profile.create]
|
||||
[status-im.contexts.profile.recover.events :as profile.recover]
|
||||
@@ -32,8 +32,10 @@
|
||||
(rf/defn enable-biometrics
|
||||
{:events [:onboarding/enable-biometrics]}
|
||||
[_]
|
||||
{:biometric/authenticate {:on-success #(rf/dispatch [:onboarding/biometrics-done])
|
||||
:on-fail #(rf/dispatch [:onboarding/biometrics-fail %])}})
|
||||
{:fx [[:dispatch
|
||||
[:biometric/authenticate
|
||||
{:on-success #(rf/dispatch [:onboarding/biometrics-done])
|
||||
:on-fail #(rf/dispatch [:onboarding/biometrics-fail %])}]]]})
|
||||
|
||||
(rf/defn navigate-to-enable-notifications
|
||||
{:events [:onboarding/navigate-to-enable-notifications]}
|
||||
@@ -51,10 +53,10 @@
|
||||
[:onboarding/finalize-setup]
|
||||
[:onboarding/create-account-and-login])}))
|
||||
|
||||
(rf/defn biometrics-fail
|
||||
{:events [:onboarding/biometrics-fail]}
|
||||
[cofx code]
|
||||
(biometric/show-message cofx code))
|
||||
(rf/reg-event-fx
|
||||
:onboarding/biometrics-fail
|
||||
(fn [_ [error]]
|
||||
{:dispatch [:biometric/show-message (ex-cause error)]}))
|
||||
|
||||
(rf/defn create-account-and-login
|
||||
{:events [:onboarding/create-account-and-login]}
|
||||
|
||||
@@ -177,12 +177,14 @@
|
||||
(rf/defn login-with-biometric-if-available
|
||||
{:events [:profile.login/login-with-biometric-if-available]}
|
||||
[_ key-uid]
|
||||
{:biometric/check-if-available [key-uid
|
||||
#(rf/dispatch [:profile.login/check-biometric-success % key-uid])]})
|
||||
{:biometric/check-if-available {:key-uid key-uid
|
||||
:on-success (fn [auth-method]
|
||||
(rf/dispatch [:profile.login/check-biometric-success
|
||||
key-uid auth-method]))}})
|
||||
|
||||
(rf/defn check-biometric-success
|
||||
{:events [:profile.login/check-biometric-success]}
|
||||
[{:keys [db]} auth-method key-uid]
|
||||
[{:keys [db]} key-uid auth-method]
|
||||
(merge {:db (assoc db :auth-method auth-method)}
|
||||
(when (= auth-method keychain/auth-method-biometric)
|
||||
{:keychain/password-hash-migration
|
||||
@@ -218,7 +220,7 @@
|
||||
ex-data
|
||||
(assoc :code (ex-cause error)
|
||||
:event :profile.login/biometric-auth-fail)))
|
||||
{:dispatch [:biometric/show-message error]}))
|
||||
{:dispatch [:biometric/show-message (ex-cause error)]}))
|
||||
|
||||
(rf/defn verify-database-password
|
||||
{:events [:profile.login/verify-database-password]}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im.common.biometric.events :as biometric]
|
||||
[status-im.common.biometric.utils :as biometric]
|
||||
[status-im.common.not-implemented :as not-implemented]
|
||||
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.contexts.profile.settings.screens.password.style :as style]
|
||||
[utils.i18n :as i18n]
|
||||
@@ -14,16 +13,19 @@
|
||||
(defn- on-press-biometric-enable
|
||||
[button-label theme]
|
||||
(fn []
|
||||
(authorize/authorize
|
||||
{:biometric-auth? false
|
||||
:blur? true
|
||||
:theme theme
|
||||
:auth-button-label (i18n/label :t/biometric-enable-button {:bio-type-label button-label})
|
||||
:on-close (fn [] (rf/dispatch [:standard-auth/reset-login-password]))
|
||||
:on-auth-success (fn [password]
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch [:standard-auth/reset-login-password])
|
||||
(rf/dispatch [:biometric/enable password]))})))
|
||||
(rf/dispatch
|
||||
[:standard-auth/authorize-with-password
|
||||
{:blur? true
|
||||
:theme theme
|
||||
:auth-button-label (i18n/label :t/biometric-enable-button {:bio-type-label button-label})
|
||||
:on-close (fn [] (rf/dispatch [:standard-auth/reset-login-password]))
|
||||
:on-auth-success (fn [password]
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch [:standard-auth/reset-login-password])
|
||||
(rf/dispatch
|
||||
[:biometric/authenticate
|
||||
{:on-success #(rf/dispatch [:biometric/enable password])
|
||||
:on-fail #(rf/dispatch [:biometric/show-message (ex-cause %)])}]))}])))
|
||||
|
||||
(defn- get-biometric-item
|
||||
[theme]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
(def screens-map
|
||||
{shell.constants/chat-screen chat/chat
|
||||
shell.constants/community-screen communities.overview/overview
|
||||
shell.constants/community-screen communities.overview/view
|
||||
shell.constants/discover-communities-screen communities.discover/view})
|
||||
|
||||
(defn f-screen
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.svg :as svg]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.common.scroll-page.view :as scroll-page]
|
||||
[status-im.contexts.wallet.collectible.style :as style]
|
||||
[status-im.contexts.wallet.collectible.tabs.view :as tabs]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
|
||||
(defn header
|
||||
[collectible-name collection-name collection-image-url]
|
||||
@@ -71,6 +73,32 @@
|
||||
:accessibility-label :share-details
|
||||
:label (i18n/label :t/share-details)}]]])
|
||||
|
||||
(defn options-drawer
|
||||
[images index]
|
||||
(let [{:keys [image]} (nth images index)
|
||||
uri (url/replace-port image (rf/sub [:mediaserver/port]))]
|
||||
[quo/action-drawer
|
||||
[[{:icon :i/link
|
||||
:accessibility-label :view-on-etherscan
|
||||
:label (i18n/label :t/view-on-eth)}]
|
||||
[{:icon :i/save
|
||||
:accessibility-label :save-image
|
||||
:label (i18n/label :t/save-image-to-photos)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch
|
||||
[:lightbox/save-image-to-gallery
|
||||
uri
|
||||
#(rf/dispatch [:toasts/upsert
|
||||
{:id :random-id
|
||||
:type :positive
|
||||
:container-style {:bottom (when platform/android? 20)}
|
||||
:text (i18n/label :t/photo-saved)}])]))}]
|
||||
[{:icon :i/share
|
||||
:accessibility-label :share-collectible
|
||||
:label (i18n/label :t/share-collectible)
|
||||
:right-icon :i/external}]]]))
|
||||
|
||||
(defn view-internal
|
||||
[{:keys [theme] :as _props}]
|
||||
(let [selected-tab (reagent/atom :overview)
|
||||
@@ -107,20 +135,27 @@
|
||||
:on-press (fn []
|
||||
(if svg?
|
||||
(js/alert "Can't visualize SVG images in lightbox")
|
||||
(rf/dispatch [:lightbox/navigate-to-lightbox
|
||||
token-id
|
||||
{:images [{:image preview-uri
|
||||
:image-width 300 ; collectibles don't have
|
||||
; width/height but we need
|
||||
; to pass something
|
||||
:image-height 300 ; without it animation
|
||||
; doesn't work smoothly and
|
||||
; :border-radius not
|
||||
; applied
|
||||
:id token-id
|
||||
:header collectible-name
|
||||
:description collection-name}]
|
||||
:index 0}])))}
|
||||
(rf/dispatch
|
||||
[:lightbox/navigate-to-lightbox
|
||||
token-id
|
||||
{:images [{:image preview-uri
|
||||
:image-width 300 ; collectibles don't have
|
||||
; width/height but we need
|
||||
; to pass something
|
||||
:image-height 300 ; without it animation
|
||||
; doesn't work smoothly
|
||||
; and :border-radius not
|
||||
; applied
|
||||
:id token-id
|
||||
:header collectible-name
|
||||
:description collection-name}]
|
||||
:index 0
|
||||
:on-options-press (fn [images index]
|
||||
(rf/dispatch [:show-bottom-sheet
|
||||
{:content (fn []
|
||||
[options-drawer
|
||||
images
|
||||
index])}]))}])))}
|
||||
(if svg?
|
||||
[rn/view
|
||||
{:style (assoc style/preview :overflow :hidden)
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
(fn [{:keys [db]} [{:keys [address token recipient stack-id]}]]
|
||||
(let [[prefix to-address] (utils/split-prefix-and-address address)
|
||||
prefix-seq (string/split prefix #":")
|
||||
selected-networks (mapv #(utils/short-name->id (keyword %)) prefix-seq)]
|
||||
selected-networks (->> prefix-seq
|
||||
(remove string/blank?)
|
||||
(mapv #(utils/short-name->id (keyword %))))]
|
||||
{:db (-> db
|
||||
(assoc-in [:wallet :ui :send :recipient] (or recipient address))
|
||||
(assoc-in [:wallet :ui :send :to-address] to-address)
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{:db db/app-db
|
||||
:theme/init-theme nil
|
||||
:network/listen-to-network-info nil
|
||||
:biometric/get-supported-biometric-type nil
|
||||
:biometric/get-supported-type nil
|
||||
;;app starting flow continues in get-profiles-overview
|
||||
:profile/get-profiles-overview #(rf/dispatch [:profile/get-profiles-overview-success %])
|
||||
:effects.font/get-font-file-for-initials-avatar
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
(ns status-im.integration-test.standard-auth-test
|
||||
(:require
|
||||
[cljs.test :refer [deftest testing is use-fixtures]]
|
||||
[day8.re-frame.test :as rf-test]
|
||||
re-frame.core
|
||||
[test-helpers.integration :as h]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn fixture-re-frame
|
||||
[]
|
||||
(let [restore-re-frame (atom nil)]
|
||||
{:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
|
||||
:after #(@restore-re-frame)}))
|
||||
|
||||
(use-fixtures :each (fixture-re-frame))
|
||||
|
||||
(def default-args
|
||||
{:on-auth-success identity
|
||||
:on-auth-fail identity
|
||||
:on-close identity
|
||||
:auth-button-label "test"
|
||||
:auth-button-icon-left :test-icon
|
||||
:blur? false
|
||||
:theme :light})
|
||||
|
||||
(deftest standard-auth-biometric-authorize-success
|
||||
(testing "calling success callback when completing biometric authentication"
|
||||
(h/log-headline :standard-auth-authorize-success)
|
||||
(rf-test/run-test-async
|
||||
(do
|
||||
(rf/reg-fx :biometric/check-if-available
|
||||
(fn [{:keys [on-success]}]
|
||||
(rf/dispatch on-success)))
|
||||
(rf/reg-event-fx :biometric/authenticate
|
||||
(fn [_ [{:keys [on-success]}]]
|
||||
(rf/dispatch on-success)))
|
||||
(rf/reg-fx :keychain/get-user-password
|
||||
(fn [[_ on-success]]
|
||||
(on-success))))
|
||||
(let [on-success-called? (atom false)
|
||||
args (assoc default-args :on-auth-success #(reset! on-success-called? true))]
|
||||
(rf/dispatch [:standard-auth/authorize args])
|
||||
(rf-test/wait-for [:standard-auth/on-biometric-success]
|
||||
(is @on-success-called?))))))
|
||||
|
||||
(deftest standard-auth-biometric-authorize-cancel
|
||||
(testing "falling back to password authorization when biometrics canceled"
|
||||
(h/log-headline :standard-auth-authorize-cancel)
|
||||
(rf-test/run-test-async
|
||||
(do
|
||||
(rf/reg-fx :biometric/check-if-available
|
||||
(fn [{:keys [on-success]}]
|
||||
(rf/dispatch on-success)))
|
||||
(rf/reg-event-fx :biometric/authenticate
|
||||
(fn [_ [{:keys [on-cancel]}]]
|
||||
(rf/dispatch on-cancel)))
|
||||
(rf/reg-event-fx :show-bottom-sheet
|
||||
(fn [_])))
|
||||
(rf/dispatch [:standard-auth/authorize default-args])
|
||||
(rf-test/wait-for [:show-bottom-sheet]
|
||||
(is true)))))
|
||||
|
||||
(deftest standard-auth-biometric-authorize-fail
|
||||
(testing "showing biometric error message when authorization failed"
|
||||
(h/log-headline :standard-auth-authorize-fail)
|
||||
(rf-test/run-test-async
|
||||
(let [on-fail-called? (atom false)
|
||||
expected-error-cause :bad-error
|
||||
error (atom nil)
|
||||
args (assoc default-args
|
||||
:on-auth-fail
|
||||
(fn [err]
|
||||
(reset! on-fail-called? true)
|
||||
(reset! error err)))]
|
||||
(rf/reg-fx :biometric/check-if-available
|
||||
(fn [{:keys [on-success]}]
|
||||
(rf/dispatch on-success)))
|
||||
(rf/reg-event-fx :biometric/authenticate
|
||||
(fn [_ [{:keys [on-fail]}]]
|
||||
(rf/dispatch (conj on-fail (ex-info "error" {} expected-error-cause)))))
|
||||
(rf/reg-fx :biometric/show-message
|
||||
(fn [_]))
|
||||
(rf/dispatch [:standard-auth/authorize args])
|
||||
(rf-test/wait-for [:biometric/show-message]
|
||||
(is @on-fail-called?)
|
||||
(is (= expected-error-cause (ex-cause @error))))))))
|
||||
|
||||
(deftest standard-auth-password-authorize-fallback
|
||||
(testing "falling back to password when biometrics is not available"
|
||||
(h/log-headline :standard-auth-password-authorize-fallback)
|
||||
(rf-test/run-test-async
|
||||
(do
|
||||
(rf/reg-fx :biometric/check-if-available
|
||||
(fn [{:keys [on-fail]}]
|
||||
(rf/dispatch on-fail)))
|
||||
(rf/reg-event-fx :show-bottom-sheet
|
||||
(fn [_])))
|
||||
(rf/dispatch [:standard-auth/authorize default-args])
|
||||
(rf-test/wait-for [:standard-auth/authorize-with-password]
|
||||
(is true)))))
|
||||
@@ -143,7 +143,7 @@
|
||||
:component communities.discover/view}
|
||||
|
||||
{:name :community-overview
|
||||
:component communities.overview/overview}
|
||||
:component communities.overview/view}
|
||||
|
||||
{:name :settings
|
||||
:options options/transparent-screen-options
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns status-im.subs.communities
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[legacy.status-im.data-store.communities :as data-store]
|
||||
[legacy.status-im.ui.screens.profile.visibility-status.utils :as visibility-status-utils]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.constants :as constants]
|
||||
@@ -307,10 +308,6 @@
|
||||
[(re-frame/subscribe [:communities/community community-id])])
|
||||
(fn [[{:keys [token-permissions-check token-permissions checking-permissions? token-images]}] _]
|
||||
(let [can-request-access? (:satisfied token-permissions-check)
|
||||
role-permissions #{constants/community-token-permission-become-admin
|
||||
constants/community-token-permission-become-member
|
||||
constants/community-token-permission-become-token-master
|
||||
constants/community-token-permission-become-token-owner}
|
||||
highest-permission-role
|
||||
(when can-request-access?
|
||||
(->> token-permissions-check
|
||||
@@ -321,7 +318,7 @@
|
||||
(and (first criteria)
|
||||
(some #{(:type (permission-id->permission-value token-permissions
|
||||
permission-id))}
|
||||
role-permissions))]
|
||||
constants/community-role-permissions))]
|
||||
(if highest-permission-role
|
||||
(min highest-permission-role permission-type)
|
||||
permission-type)
|
||||
@@ -339,8 +336,8 @@
|
||||
(:permissions token-permissions-check))
|
||||
:tokens (->>
|
||||
token-permissions
|
||||
(filter (fn [[_ {:keys [type]}]]
|
||||
(= type constants/community-token-permission-become-member)))
|
||||
(filter (fn [[_ permission]]
|
||||
(data-store/role-permission? permission)))
|
||||
(map (fn [[perm-key {:keys [token_criteria]}]]
|
||||
(let [check-criteria (get-in token-permissions-check
|
||||
[:permissions perm-key :criteria])]
|
||||
|
||||
@@ -455,7 +455,11 @@
|
||||
(swap! rf-db/app-db assoc-in [:communities community-id] community)
|
||||
(is (match? {:can-request-access? true
|
||||
:number-of-hold-tokens 2
|
||||
:tokens [[{:symbol "ETH"
|
||||
:tokens [[{:symbol "DAI"
|
||||
:amount "5.0"
|
||||
:sufficient? nil
|
||||
:loading? checking-permissions?}]
|
||||
[{:symbol "ETH"
|
||||
:amount "0.001"
|
||||
:sufficient? nil
|
||||
:loading? checking-permissions?
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
(reg-root-key-sub :get-pairing-installations :pairing/installations)
|
||||
(reg-root-key-sub :tooltips :tooltips)
|
||||
(reg-root-key-sub :biometric/supported-type :biometric/supported-type)
|
||||
(reg-root-key-sub :biometric/auth-pending? :biometric/auth-pending?)
|
||||
(reg-root-key-sub :app-state :app-state)
|
||||
(reg-root-key-sub :home-items-show-number :home-items-show-number)
|
||||
(reg-root-key-sub :waku/v2-peer-stats :peer-stats)
|
||||
|
||||
@@ -1283,6 +1283,7 @@
|
||||
"share-channel": "Share channel",
|
||||
"share-address": "Share address",
|
||||
"share-chat": "Share chat",
|
||||
"share-collectible": "Share collectible",
|
||||
"share-contact-code": "Share my chat key",
|
||||
"share-community": "Share community",
|
||||
"share-dapp-text": "Check out this DApp I'm using on Status: {{link}}",
|
||||
|
||||
Reference in New Issue
Block a user