Compare commits

...
Author SHA1 Message Date
Yevheniia Berdnyk 579fde7f56 e2e: temp, will be deleted 2025-01-31 17:53:08 +02:00
Yevheniia Berdnyk 5ff1c8f1e4 e2e: added wallet test for bridge flow 2025-01-30 20:13:34 +02:00
Brian Sztamfater 7289940653 fix: swap fees are not displayed if route estimated time is nil (#22001)
Signed-off-by: Brian Sztamfater <brian@status.im>
2025-01-30 10:08:37 -03:00
Yevheniia Berdnyk a253f8cedb e2e: updated tests with editing profile when starting chat 2025-01-30 11:49:58 +02:00
Ulises Manuel daf52102ce feat(onboarding): ask user to update their profile (#21862)
* Add `buttons-style` prop to bottom actions to allow same button dimensions

* Make events to update a profile more generic

* Fix tests

* Add modal screen to update the profile

* Add introduce yourself modal

* Ask user to update their profile before sending a message or contact request

* Fix color picker selected styles

* Remove some `:f>` now unnecessary

* No longer shows the modal if the user manually updates their profile
2025-01-29 19:50:01 -06:00
28 changed files with 741 additions and 136 deletions
+2 -1
View File
@@ -30,7 +30,8 @@
selected? (assoc :border-top-color (colors/alpha color 0.4)
:border-end-color (colors/alpha color 0.4)
:border-bottom-color (colors/alpha color 0.2)
:border-start-color (colors/alpha color 0.2))
:border-start-color (colors/alpha color 0.2)
:border-color nil)
(zero? idx) (assoc :margin-left -4))))
(defn color-circle
@@ -31,7 +31,8 @@
[:scroll? {:optional true} [:maybe :boolean]]
[:blur? {:optional true} [:maybe :boolean]]
[:container-style {:optional true} [:maybe :map]]
[:buttons-container-style {:optional true} [:maybe :map]]]]]
[:buttons-container-style {:optional true} [:maybe :map]]
[:buttons-style {:optional true} [:maybe :map]]]]]
:any])
(def ^:private role-icon
@@ -43,10 +44,9 @@
(defn- view-internal
[{:keys [actions description description-text description-top-text error-message role button-one-label
button-two-label blur? button-one-props button-two-props scroll? container-style
buttons-container-style context-tag-props]}]
buttons-container-style buttons-style context-tag-props]}]
(let [theme (quo.theme/use-theme)]
[rn/view
{:style (merge (style/container scroll? blur? theme) container-style)}
[rn/view {:style (merge (style/container scroll? blur? theme) container-style)}
(when (= description :top-error)
[rn/view {:style style/error-message}
[icon/icon
@@ -82,7 +82,7 @@
[button/button
(merge
{:size 40
:container-style style/button-container
:container-style (merge style/button-container buttons-style)
:background (when (or blur? scroll?) :blur)
:theme theme
:accessibility-label :button-two}
@@ -91,7 +91,7 @@
[button/button
(merge
{:size 40
:container-style style/button-container
:container-style (merge style/button-container buttons-style)
:background (when (or blur? scroll?) :blur)
:theme theme
:accessibility-label :button-one}
+3 -3
View File
@@ -73,10 +73,10 @@
[{:keys [scroll-ref tabs selected-tab on-tab-change scroll-shared-value content customization-color]}]
(let [theme (quo.theme/use-theme)]
[:<>
[:f> banner-card-blur-layer scroll-shared-value
[:f> banner-card-hiding-layer
[banner-card-blur-layer scroll-shared-value
[banner-card-hiding-layer
(assoc content :scroll-shared-value scroll-shared-value :theme theme)]]
[:f> banner-card-tabs-layer
[banner-card-tabs-layer
{:scroll-shared-value scroll-shared-value
:selected-tab selected-tab
:tabs tabs
+6 -2
View File
@@ -114,13 +114,17 @@
{:scroll-input (oops/oget % "nativeEvent.contentOffset.y")
:shared-value scroll-shared-value})}])))
(defn- on-new-message-press
[]
(let [main-event [:show-bottom-sheet {:content chat.actions.view/new-chat}]]
(rf/dispatch [:profile/check-profile-update-prompt main-event])))
(defn- banner-data
[profile-link]
{:title-props
{:beta? true
:label (i18n/label :t/messages)
:handler #(rf/dispatch
[:show-bottom-sheet {:content chat.actions.view/new-chat}])
:handler on-new-message-press
:accessibility-label :new-chat-button}
:card-props
{:on-press #(rf/dispatch [:open-share {:options {:url profile-link}}])
@@ -14,15 +14,14 @@
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn on-contact-request
(defn on-send-contact-request
[]
(rf/dispatch [:show-bottom-sheet
{:content (fn [] [contact-request/view])}]))
(let [main-event [:show-bottom-sheet {:content contact-request/view}]]
(rf/dispatch [:profile/check-profile-update-prompt main-event])))
(defn on-contact-review
[]
(rf/dispatch [:show-bottom-sheet
{:content (fn [] [contact-review/view])}]))
(rf/dispatch [:show-bottom-sheet {:content contact-review/view}]))
(defn view
[{:keys [scroll-y]}]
@@ -34,27 +33,27 @@
profile-picture (profile.utils/photo contact)
online? (rf/sub [:visibility-status-updates/online? public-key])
theme (quo.theme/use-theme)
contact-status (rn/use-memo (fn []
(cond
(= contact-request-state
constants/contact-request-state-mutual) :contact
blocked? :blocked
:else nil))
[blocked? contact-request-state])
on-start-chat (rn/use-callback #(rf/dispatch [:chat.ui/start-chat
public-key
ens-name])
[ens-name public-key])
on-unblock-press (rn/use-callback (fn []
(rf/dispatch [:contact/unblock-contact
public-key])
(rf/dispatch [:toasts/upsert
{:id :user-unblocked
:type :positive
:text (i18n/label :t/user-unblocked
{:username
full-name})}]))
[public-key full-name])]
contact-status (rn/use-memo
(fn []
(cond
(= contact-request-state
constants/contact-request-state-mutual) :contact
blocked? :blocked
:else nil))
[blocked? contact-request-state])
on-start-chat (rn/use-callback
#(rf/dispatch [:chat.ui/start-chat public-key ens-name])
[ens-name public-key])
on-unblock-press (rn/use-callback
(fn []
(rf/dispatch [:contact/unblock-contact
public-key])
(rf/dispatch [:toasts/upsert
{:id :user-unblocked
:type :positive
:text (i18n/label :t/user-unblocked
{:username full-name})}]))
[public-key full-name])]
[rn/view {:style style/header-container}
[rn/view {:style style/header-top-wrapper}
[rn/view {:style style/avatar-wrapper}
@@ -93,14 +92,12 @@
(cond
(and (not blocked?)
(or
(not contact-request-state)
(= contact-request-state constants/contact-request-state-none)
(= contact-request-state constants/contact-request-state-dismissed)))
(or (not contact-request-state)
(= contact-request-state constants/contact-request-state-none)
(= contact-request-state constants/contact-request-state-dismissed)))
[quo/button
{:container-style style/button-wrapper
:on-press on-contact-request
:on-press on-send-contact-request
:customization-color customization-color
:icon-left :i/add-user}
(i18n/label :t/send-contact-request)]
@@ -4,27 +4,35 @@
[utils.re-frame :as rf]))
(rf/reg-event-fx :profile/edit-accent-colour-success
(fn [_ [customization-color]]
(fn [_ [{:keys [customization-color navigate-back? show-toast?]}]]
{:fx [[:dispatch [:profile/save-local-accent-color customization-color]]
[:dispatch [:navigate-back]]
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label :t/accent-colour-updated)}]]]}))
(when navigate-back?
[:dispatch [:navigate-back]])
(when show-toast?
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label :t/accent-colour-updated)}]])]}))
(rf/reg-event-fx :profile/save-local-accent-color
(fn [{:keys [db]} [customization-color]]
{:db (assoc-in db [:profile/profile :customization-color] customization-color)}))
(defn edit-accent-colour
[{:keys [db]} [customization-color]]
[{:keys [db]}
[{:keys [color navigate-back? show-toast?]
:or {navigate-back? true
show-toast? true}}]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:json-rpc/call
[{:method "wakuext_setCustomizationColor"
:params [{:customizationColor customization-color
:params [{:customizationColor color
:keyUid key-uid}]
:on-success [:profile/edit-accent-colour-success customization-color]
:on-success [:profile/edit-accent-colour-success
{:customization-color color
:navigate-back? navigate-back?
:show-toast? show-toast?}]
:on-error #(log/error "failed to edit accent color." {:error %})}]]]}))
(rf/reg-event-fx :profile/edit-accent-colour edit-accent-colour)
@@ -11,7 +11,10 @@
[{:method "wakuext_setCustomizationColor"
:params [{:customizationColor new-color
:keyUid key-uid}]
:on-success [:profile/edit-accent-colour-success new-color]
:on-success [:profile/edit-accent-colour-success
{:customization-color new-color
:navigate-back? true
:show-toast? true}]
:on-error fn?}]]]}]
(is (match? expected
(sut/edit-accent-colour cofx [new-color])))))
(sut/edit-accent-colour cofx [{:color new-color}])))))
@@ -58,7 +58,8 @@
{:type :primary
:customization-color @unsaved-custom-color
:on-press (fn []
(rf/dispatch [:profile/edit-accent-colour @unsaved-custom-color]))}
(rf/dispatch [:profile/edit-accent-colour
{:color @unsaved-custom-color}]))}
(i18n/label :t/save-colour)]]]]))))
(defn view
@@ -11,19 +11,22 @@
(update db :profile/profile dissoc :images))}))
(rf/reg-event-fx :profile/edit-profile-picture-success
(fn [_ [images]]
(let [has-picture? (rf/sub [:profile/has-picture])]
(fn [{db :db} [{:keys [show-toast?]} images]]
(let [has-picture? (-> db :profile/profile :images count pos?)]
{:fx [[:dispatch [:profile/update-local-picture (reverse images)]]
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label (if has-picture?
:t/profile-picture-updated
:t/profile-picture-added))}]]]})))
(when show-toast?
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label (if has-picture?
:t/profile-picture-updated
:t/profile-picture-added))}]])]})))
(defn edit-profile-picture
[{:keys [db]} [picture crop-width crop-height]]
[{:keys [db]}
[{:keys [picture crop-width crop-height show-toast?]
:or {show-toast? true}}]]
(let [key-uid (get-in db [:profile/profile :key-uid])
crop-width (or crop-width profile-picture-picker/crop-size)
crop-height (or crop-height profile-picture-picker/crop-size)
@@ -31,25 +34,28 @@
{:fx [[:json-rpc/call
[{:method "multiaccounts_storeIdentityImage"
:params [key-uid path 0 0 crop-width crop-height]
:on-success [:profile/edit-profile-picture-success]}]]]}))
:on-success [:profile/edit-profile-picture-success {:show-toast? show-toast?}]}]]]}))
(rf/reg-event-fx :profile/edit-picture edit-profile-picture)
(rf/reg-event-fx :profile/delete-profile-picture-success
(fn [_]
(fn [_ [{:keys [show-toast?]}]]
{:fx [[:dispatch [:profile/update-local-picture nil]]
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label :t/profile-picture-removed)}]]]}))
(when show-toast?
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label :t/profile-picture-removed)}]])]}))
(defn delete-profile-picture
[{:keys [db]}]
[{:keys [db]}
[{:keys [show-toast?]
:or {show-toast? true}}]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:json-rpc/call
[{:method "multiaccounts_deleteIdentityImage"
:params [key-uid]
:on-success [:profile/delete-profile-picture-success]}]]]}))
:on-success [:profile/delete-profile-picture-success {:show-toast? show-toast?}]}]]]}))
(rf/reg-event-fx :profile/delete-picture delete-profile-picture)
@@ -12,9 +12,9 @@
[{:method "multiaccounts_storeIdentityImage"
:params [key-uid picture 0 0 profile-picture-picker/crop-size
profile-picture-picker/crop-size]
:on-success [:profile/edit-profile-picture-success]}]]]}]
:on-success [:profile/edit-profile-picture-success {:show-toast? true}]}]]]}]
(is (match? expected
(sut/edit-profile-picture cofx [picture])))))
(sut/edit-profile-picture cofx [{:picture picture}])))))
(deftest delete-picture-test
(let [key-uid "key-uid"
@@ -22,6 +22,6 @@
expected {:fx [[:json-rpc/call
[{:method "multiaccounts_deleteIdentityImage"
:params [key-uid]
:on-success [:profile/delete-profile-picture-success]}]]]}]
:on-success [:profile/delete-profile-picture-success {:show-toast? true}]}]]]}]
(is (match? expected
(sut/delete-profile-picture cofx)))))
(sut/delete-profile-picture cofx [{}])))))
@@ -16,7 +16,7 @@
has-picture? (rf/sub [:profile/has-picture])
on-change-profile-pic (fn [picture]
(if picture
(rf/dispatch [:profile/edit-picture picture])
(rf/dispatch [:profile/edit-picture {:picture picture}])
(rf/dispatch [:profile/delete-picture])))]
[rn/view
{:key :edit-profile
@@ -0,0 +1,33 @@
(ns status-im.contexts.profile.edit.introduce-yourself.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn sheet
[{:keys [pending-event]}]
(let [on-accept (fn []
(rf/dispatch [:open-modal :screen/profile.edit-profile-modal
{:pending-event pending-event}]))
on-reject #(rf/dispatch pending-event)]
[:<>
[rn/view {:style {:padding-horizontal 20}}
[quo/text
{:style {:padding-bottom 4}
:size :heading-2
:weight :semi-bold}
(i18n/label :t/just-introduce-yourself)]
[quo/text
{:style {:padding-top 4 :padding-bottom 12}
:size :paragraph-1
:weight :regular}
(i18n/label :t/add-display-name-and-picture)]]
[quo/bottom-actions
{:actions :two-actions
:buttons-style {:flex-shrink 1 :flex-basis 0}
:button-one-label (i18n/label :t/edit-profile)
:button-one-props {:type :primary
:on-press on-accept}
:button-two-label (i18n/label :t/skip)
:button-two-props {:type :grey
:on-press on-reject}}]]))
@@ -0,0 +1,62 @@
(ns status-im.contexts.profile.edit.modal.events
(:require [status-im.contexts.profile.edit.introduce-yourself.view :as introduce-yourself]
[utils.re-frame :as rf]))
(rf/reg-event-fx
:profile/edit-profile
(fn [_ [{:keys [display-name picture color on-success]}]]
{:fx (cond-> []
display-name
(conj [:dispatch
[:profile/edit-name
{:display-name display-name
:navigate-back? false
:show-toast? false}]])
color
(conj [:dispatch
[:profile/edit-accent-colour
{:color color
:navigate-back? false
:show-toast? false}]])
picture
(conj [:dispatch
[:profile/edit-picture
{:picture picture
:show-toast? false}]])
(nil? picture)
(conj [:dispatch [:profile/delete-picture {:show-toast? false}]])
:always
(conj [:dispatch [:navigate-back]]
[:dispatch on-success]))}))
(defn- profile-update-asked-storage-key
[key-uid]
(keyword :update-profile-asked key-uid))
(rf/reg-event-fx
:profile/set-profile-update-as-asked
(fn [{db :db}]
(let [storage-key (-> db :profile/profile :key-uid profile-update-asked-storage-key)]
{:fx [[:effects.async-storage/set {storage-key true}]]})))
(rf/reg-event-fx
:profile/ask-profile-update
(fn [_ [pending-event]]
{:fx [[:dispatch [:profile/set-profile-update-as-asked]]
[:dispatch
[:show-bottom-sheet
{:content (fn []
[introduce-yourself/sheet {:pending-event pending-event}])}]]]}))
(rf/reg-event-fx
:profile/check-profile-update-prompt
(fn [{db :db} [pending-event]]
(let [storage-key (-> db :profile/profile :key-uid profile-update-asked-storage-key)]
{:fx [[:effects.async-storage/get
{:keys [storage-key]
:cb (fn [profile-updated-data]
(rf/dispatch (if (storage-key profile-updated-data)
pending-event
[:profile/ask-profile-update pending-event])))}]]})))
@@ -0,0 +1,55 @@
(ns status-im.contexts.profile.edit.modal.style
(:require
[quo.foundations.colors :as colors]
[react-native.platform :as platform]))
(def continue-button
{:width "100%"
:margin-left :auto
:margin-top (if platform/android? :auto 0)
:margin-right :auto})
(def button-container
{:width "100%"
:padding-left 20
:padding-right 20
:padding-top 12
:align-self :flex-end
:height 64})
(defn view-button-container
[keyboard-shown?]
(merge button-container
(if platform/ios?
{:margin-bottom (if keyboard-shown? 0 34)}
{:margin-bottom (if keyboard-shown? 12 34)})))
(def blur-button-container
(merge button-container
(when platform/android? {:padding-bottom 12})))
(def page-container
{:flex 1})
(def info-message
{:margin-top 8})
(def title
{:color colors/white
:margin-top 12
:margin-bottom 18})
(def color-title
{:color colors/white-70-blur
:margin-top 20
:margin-bottom 16})
(def content-container
{:padding-horizontal 20})
(def input-container
{:align-items :flex-start})
(def profile-input-container
{:flex-direction :row
:justify-content :center})
@@ -0,0 +1,266 @@
(ns status-im.contexts.profile.edit.modal.view
(:require
[clojure.string :as string]
[oops.core :as oops]
[quo.core :as quo]
[quo.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.hooks :as hooks]
[react-native.platform :as platform]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im.common.avatar-picture-picker.view :as profile-picture-picker]
[status-im.common.events-helper :as events-helper]
[status-im.common.validation.profile :as profile-validator]
[status-im.constants :as c]
[status-im.contexts.profile.edit.modal.events]
[status-im.contexts.profile.edit.modal.style :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.responsiveness :as responsiveness]))
(def scroll-view-height (reagent/atom 0))
(def content-container-height (reagent/atom 0))
(def set-scroll-view-height #(reset! scroll-view-height %))
(defn set-content-container-height
[e]
(reset! content-container-height (oops/oget e "nativeEvent.layout.height")))
(defn show-button-background?
[keyboard-height keyboard-shown content-scroll-y]
(let [button-container-height 64
keyboard-view-height (+ keyboard-height button-container-height)]
(when keyboard-shown
(cond
platform/android?
(< (- @scroll-view-height button-container-height) @content-container-height)
platform/ios?
(< (- @scroll-view-height keyboard-view-height) (- @content-container-height content-scroll-y))
:else
false))))
(defn button-container
[show-keyboard? keyboard-shown show-background? keyboard-height children]
(let [height (reagent/atom 0)]
(reset! height (if show-keyboard? (if keyboard-shown keyboard-height 0) 0))
[rn/view {:style {:margin-top :auto}}
(cond
(and (> @height 0) show-background?)
[quo/blur
(when keyboard-shown
{:blur-amount 34
:blur-type :transparent
:overlay-color :transparent
:background-color (if platform/android?
colors/neutral-100
colors/neutral-80-opa-1-blur)
:style style/blur-button-container})
children]
(and (> @height 0) (not show-background?))
[rn/view {:style (style/view-button-container true)}
children]
(not show-keyboard?)
[rn/view {:style (style/view-button-container false)}
children])]))
(defn- add-keyboard-listener
[e callback]
(oops/ocall rn/keyboard "addListener" e callback))
(defn- on-keyboard-show
[f]
(add-keyboard-listener (if platform/android? "keyboardDidShow" "keyboardWillShow") f))
(defn- on-keyboard-hide
[f]
(add-keyboard-listener (if platform/android? "keyboardDidHide" "keyboardWillHide") f))
(defn floating-button
[{:keys [custom-color scroll-y on-submit disabled?]}]
(reagent/with-let [show-keyboard? (reagent/atom false)
show-listener (on-keyboard-show #(reset! show-keyboard? true))
hide-listener (on-keyboard-hide #(reset! show-keyboard? false))]
(let [{:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard)
show-background? (show-button-background? keyboard-height
keyboard-shown
scroll-y)]
[rn/keyboard-avoiding-view
{:style {:position :absolute
:top 0
:bottom 0
:left 0
:right 0}
:pointer-events :box-none}
[button-container @show-keyboard? keyboard-shown show-background? keyboard-height
[quo/button
{:accessibility-label :submit-create-profile-button
:type :primary
:customization-color custom-color
:on-press on-submit
:container-style style/continue-button
:disabled? disabled?}
(i18n/label :t/continue)]]])
(finally
(oops/ocall show-listener "remove")
(oops/ocall hide-listener "remove"))))
(defn- content
[{:keys [set-display-name set-validation-msg picture-picker]}]
(let [open-picture-picker (fn []
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch [:show-bottom-sheet
{:content picture-picker
:shell? true
:theme :dark}]))
on-change-text (fn [s]
(set-validation-msg s)
(set-display-name s))]
(fn [{:keys [profile-image custom-color display-name validation-msg valid-name?
name-too-short? initial-display-name set-scroll set-scroll-height
set-custom-color]}]
(let [{window-width :width} (rn/get-window)
info-type (cond
validation-msg :error
name-too-short? :default
:else :success)
info-message (or validation-msg
(i18n/label :t/minimum-characters
{:min-chars profile-validator/min-length}))
full-name (if (seq display-name)
display-name
initial-display-name)]
[rn/scroll-view
{:on-layout set-scroll-height
:on-scroll set-scroll
:scroll-event-throttle 64
:content-container-style {:flex-grow 1}}
[rn/view {:on-layout set-content-container-height}
[rn/view {:style style/content-container}
[quo/text
{:style style/title
:size :heading-1
:weight :semi-bold}
(i18n/label :t/edit-profile)]
[rn/view {:style style/input-container}
[rn/view {:style style/profile-input-container}
[quo/profile-input
{:customization-color custom-color
:placeholder initial-display-name
:on-press open-picture-picker
:image-picker-props {:profile-picture profile-image
:full-name full-name
:customization-color custom-color}
:title-input-props {:default-value ""
:auto-focus true
:max-length c/profile-name-max-length
:on-change-text on-change-text}}]]
[quo/info-message
{:status info-type
:size :default
:icon (if valid-name? :i/positive-state :i/info)
:color (when (= :default info-type) colors/white-70-blur)
:container-style style/info-message}
info-message]
[quo/text
{:size :paragraph-2
:weight :medium
:style style/color-title}
(i18n/label :t/colour)]]]
[quo/color-picker
{:blur? true
:default-selected custom-color
:on-change set-custom-color
:window-width window-width
:container-style {:padding-left (responsiveness/iphone-11-Pro-20-pixel-from-width
window-width)}}]]]))))
(defn- submittable-state?
[{:keys [new-color? new-picture? new-name? valid-name?]}]
(let [acceptable-name? (or (not new-name?) valid-name?)]
(or (and new-color? acceptable-name?)
(and new-picture? acceptable-name?)
(and new-name? valid-name?))))
(defn view
[]
(let [{:keys [pending-event]} (rf/sub [:get-screen-params])
{initial-display-name :display-name
initial-color :customization-color
[initial-image] :images} (rf/sub [:profile/profile])
top (safe-area/get-top)
scroll-y (reagent/atom 0)
display-name (reagent/atom "")
validation-msg (reagent/atom (profile-validator/validation-name @display-name))
custom-color (reagent/atom (or initial-color c/profile-default-color))
profile-image (reagent/atom initial-image)
set-display-name #(reset! validation-msg (profile-validator/validation-name %))
set-validation-msg #(reset! display-name (string/trim %))
set-custom-color #(reset! custom-color %)
set-scroll (fn [e]
(reset! scroll-y (oops/oget e "nativeEvent.contentOffset.y")))
set-scroll-height (fn [e]
(reset! scroll-y 0)
(-> e
(oops/oget "nativeEvent.layout.height")
set-scroll-view-height))
picture-picker (fn []
[profile-picture-picker/view
{:on-result #(reset! profile-image %)
:has-picture? (some? @profile-image)}])
update-profile (fn []
(rf/dispatch
[:profile/edit-profile
(cond-> {:on-success pending-event}
(and (seq @display-name)
(not= initial-display-name @display-name))
(assoc :display-name @display-name)
(and (not= initial-image @profile-image))
(assoc :picture @profile-image)
(not= initial-color @custom-color)
(assoc :color @custom-color))]))
on-close (fn []
(events-helper/navigate-back)
(rf/dispatch pending-event))]
(fn []
(let [name-too-short? (profile-validator/name-too-short? @display-name)
valid-name? (and (not @validation-msg) (not name-too-short?))
disabled? (not
(submittable-state?
{:new-color? (not= initial-color @custom-color)
:new-picture? (not= @profile-image initial-image)
:new-name? (and (not= initial-display-name @display-name)
(seq @display-name))
:valid-name? valid-name?}))]
[quo/overlay {:type :shell}
[rn/view {:style style/page-container}
[quo/page-nav
{:margin-top top
:background :blur
:icon-name :i/close
:on-press on-close}]
[content
{:profile-image @profile-image
:custom-color @custom-color
:display-name @display-name
:validation-msg @validation-msg
:valid-name? valid-name?
:name-too-short? name-too-short?
:picture-picker picture-picker
:initial-display-name initial-display-name
:set-scroll set-scroll
:set-scroll-height set-scroll-height
:set-custom-color set-custom-color
:set-display-name set-display-name
:set-validation-msg set-validation-msg}]
[floating-button
{:custom-color @custom-color
:scroll-y @scroll-y
:on-submit update-profile
:disabled? disabled?}]]]))))
@@ -5,21 +5,26 @@
[utils.re-frame :as rf]))
(rf/reg-event-fx :profile/edit-profile-name-success
(fn [_]
{:fx [[:dispatch [:navigate-back]]
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label :t/name-updated)}]]]}))
(fn [{:keys [db]} [display-name navigate-back? show-toast?]]
{:db (assoc-in db [:profile/profile :display-name] display-name)
:fx [(when navigate-back?
[:dispatch [:navigate-back]])
(when show-toast?
[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (i18n/label :t/name-updated)}]])]}))
(defn edit-profile-name
[{:keys [db]} [name]]
{:db (assoc-in db [:profile/profile :display-name] name)
:fx [[:json-rpc/call
[_
[{:keys [display-name navigate-back? show-toast?]
:or {navigate-back? true
show-toast? true}}]]
{:fx [[:json-rpc/call
[{:method "wakuext_setDisplayName"
:params [name]
:on-success [:profile/edit-profile-name-success]}]]]})
:params [display-name]
:on-success [:profile/edit-profile-name-success display-name navigate-back? show-toast?]}]]]})
(rf/reg-event-fx :profile/edit-name edit-profile-name)
@@ -6,10 +6,9 @@
(deftest edit-name-test
(let [new-name "John Doe"
cofx {:db {:profile/profile {:display-name "Old name"}}}
expected {:db {:profile/profile {:display-name new-name}}
:fx [[:json-rpc/call
expected {:fx [[:json-rpc/call
[{:method "wakuext_setDisplayName"
:params [name]
:on-success [:profile/edit-profile-name-success]}]]]}]
:on-success [:profile/edit-profile-name-success new-name true true]}]]]}]
(is (match? expected
(sut/edit-profile-name cofx [new-name])))))
(sut/edit-profile-name cofx [{:display-name new-name}])))))
@@ -67,7 +67,8 @@
{:type :primary
:customization-color customization-color
:on-press (fn []
(rf/dispatch [:profile/edit-name @full-name]))
(rf/dispatch [:profile/edit-name {:display-name @full-name}])
(rf/dispatch [:profile/set-profile-update-as-asked]))
:disabled? (boolean (or @typing?
(string/blank? @full-name)
(not (string/blank? @error-msg))))}
@@ -29,8 +29,8 @@
:z-index (get shared-values
(get shell.constants/stacks-z-index-keywords stack-id))})}
(case stack-id
:communities-stack [:f> communities/view]
:chats-stack [:f> chat/view]
:communities-stack [communities/view]
:chats-stack [chat/view]
:wallet-stack [wallet/view]
:browser-stack [browser.stack/browser-stack]
[:<>])])
@@ -38,7 +38,7 @@
(defn lazy-screen
[stack-id shared-values]
(when (load-stack? stack-id)
[:f> f-stack-view stack-id shared-values]))
[f-stack-view stack-id shared-values]))
(defn view
[shared-values]
@@ -202,18 +202,17 @@
:network-image (:source network)}]
[data-item
{:title (i18n/label :t/max-fees)
:subtitle (if (and estimated-time approval-fees-formatted)
:subtitle (if approval-fees-formatted
approval-fees-formatted
(i18n/label :t/unknown))
:loading? loading-swap-proposal?
:size :small}]
[data-item
{:title (i18n/label :t/est-time)
:subtitle (if estimated-time
(i18n/label :t/time-in-mins {:minutes (str estimated-time)})
(i18n/label :t/unknown))
:loading? loading-swap-proposal?
:size :small}]]]))
(when estimated-time
[data-item
{:title (i18n/label :t/est-time)
:subtitle (i18n/label :t/time-in-mins {:minutes (str estimated-time)})
:loading? loading-swap-proposal?
:size :small}])]]))
(defn- slide-button
[]
@@ -148,15 +148,14 @@
max-slippage (rf/sub [:wallet/swap-max-slippage])]
[rn/view {:style style/details-container}
[:<>
[data-item
{:title (i18n/label :t/est-time)
:subtitle (if estimated-time
(i18n/label :t/time-in-mins {:minutes (str estimated-time)})
(i18n/label :t/unknown))
:loading? loading-swap-proposal?}]
(when estimated-time
[data-item
{:title (i18n/label :t/est-time)
:subtitle (i18n/label :t/time-in-mins {:minutes (str estimated-time)})
:loading? loading-swap-proposal?}])
[data-item
{:title (i18n/label :t/max-fees)
:subtitle (if (and estimated-time max-fees-formatted) max-fees-formatted (i18n/label :t/unknown))
:subtitle (if max-fees-formatted max-fees-formatted (i18n/label :t/unknown))
:loading? loading-swap-proposal?}]
[data-item
{:title (i18n/label :t/max-slippage)
+7 -1
View File
@@ -64,6 +64,7 @@
[status-im.contexts.profile.contact.view :as contact-profile]
[status-im.contexts.profile.edit.accent-colour.view :as edit-accent-colour]
[status-im.contexts.profile.edit.bio.view :as edit-bio]
[status-im.contexts.profile.edit.modal.view :as edit-profile-modal]
[status-im.contexts.profile.edit.name.view :as edit-name]
[status-im.contexts.profile.edit.view :as edit-profile]
[status-im.contexts.profile.profiles.view :as profiles]
@@ -1127,7 +1128,12 @@
:options {:theme :dark
:layout options/onboarding-layout}
:on-focus [:onboarding/overlay-dismiss]
:component profiles/view}]
:component profiles/view}
{:name :screen/profile.edit-profile-modal
:metrics {:track? true}
:options options/transparent-screen-options
:component edit-profile-modal/view}]
[{:name :shell
:metrics {:track? true}
+5 -2
View File
@@ -14,7 +14,7 @@
(fn []
(let [new-name "John Doe"]
(promesa/do
(rf/dispatch [:profile/edit-name new-name])
(rf/dispatch [:profile/edit-name {:display-name new-name}])
(h/wait-for [:navigate-back :toasts/upsert])
(let [profile (rf/sub [:profile/profile])
display-name (profile.utils/displayed-name profile)]
@@ -26,7 +26,10 @@
(let [mock-image "resources/images/mock2/monkey.png"
absolute-path (.resolve test-utils/path mock-image)]
(promesa/do
(rf/dispatch [:profile/edit-picture absolute-path 80 80])
(rf/dispatch [:profile/edit-picture
{:picture absolute-path
:crop-width 80
:crop-height 80}])
(h/wait-for [:profile/update-local-picture :toasts/upsert])
(let [profile (rf/sub [:profile/profile])]
(is (not (nil? (:images profile))))))))))
@@ -32,7 +32,8 @@ class TestOneToOneChatMultipleSharedDevicesNewUi(MultipleSharedDeviceTestCase):
for home in (self.home_1, self.home_2):
home.navigate_back_to_home_view()
home.chats_tab.click()
self.home_1.add_contact(self.public_key_2)
self.username_1 = "user_1"
self.home_1.add_contact(public_key=self.public_key_2, username=self.username_1)
self.home_2.just_fyi("Accepting contact request from activity centre")
self.home_2.handle_contact_request(self.username_1)
@@ -243,7 +244,7 @@ class TestOneToOneChatMultipleSharedDevicesNewUi(MultipleSharedDeviceTestCase):
unpin_element.click_until_absense_of_element(desired_element=unpin_element)
self.chat_1.pin_message(self.message_4, 'pin-to-chat')
for chat in self.chat_1, self.chat_2:
if chat.chat_element_by_text(self.message_4).pinned_by_label.is_element_displayed(30):
if not chat.chat_element_by_text(self.message_4).pinned_by_label.is_element_displayed(30):
self.errors.append(chat, "Message 4 is not pinned in chat after unpinning previous one")
self.home_1.just_fyi("Check pinned messages are visible in Pinned panel for both users")
+126 -7
View File
@@ -21,6 +21,7 @@ class TestWalletMultipleDevice(MultipleSharedDeviceTestCase):
def prepare_devices(self):
self.network_api = NetworkApi()
print('1')
self.drivers, self.loop = create_shared_drivers(2)
self.sign_in_1, self.sign_in_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1])
self.sender, self.receiver = transaction_senders['ETH_1'], transaction_senders['ETH_2']
@@ -214,10 +215,11 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
self.drivers, self.loop = create_shared_drivers(1)
self.sign_in_view = SignInView(self.drivers[0])
self.sender, self.receiver = transaction_senders['ETH_1'], transaction_senders['ETH_2']
self.total_balance = {'Ether': 0.0062, 'USDCoin': 5.0, 'Status': 13.0, 'Uniswap': 0.627, 'Dai Stablecoin': 0.0}
self.total_balance = {'Ether': 0.0262, 'USDCoin': 5.0, 'Status': 13.0, 'Uniswap': 0.627, 'Dai Stablecoin': 0.0}
self.mainnet_balance = {'Ether': 0.005, 'USDCoin': 0.0, 'Status': 10.0, 'Uniswap': 0.127, 'Dai Stablecoin': 0.0}
self.optimism_balance = {'Ether': 0.0011, 'USDCoin': 5.0, 'Status': 3.0, 'Uniswap': 0, 'Dai Stablecoin': 0.0}
self.arb_balance = {'Ether': 0.0001, 'USDCoin': 0.0, 'Status': 0.0, 'Uniswap': 0.5, 'Dai Stablecoin': 0.0}
self.arb_balance = {'Ether': 0.0051, 'USDCoin': 0.0, 'Status': 0.0, 'Uniswap': 0.5, 'Dai Stablecoin': 0.0}
self.base_balance = {'Ether': 0.015, 'USDCoin': 0.0, 'Status': 0.0, 'Uniswap': 0.0, 'Dai Stablecoin': 0.0}
self.sender['wallet_address'] = '0x' + self.sender['address']
self.receiver['wallet_address'] = '0x' + self.receiver['address']
self.sign_in_view.recover_access(passphrase=self.sender['passphrase'])
@@ -228,6 +230,7 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
self.profile_view.switch_network()
self.sign_in_view.sign_in(user_name=self.sender_username)
self.wallet_view = self.home_view.wallet_tab.click()
self.account_name = 'Account 1'
@marks.testrail_id(740490)
def test_wallet_balance_mainnet(self):
@@ -243,7 +246,8 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
expected_balances = {
'Mainnet': self.mainnet_balance,
'Arbitrum': self.arb_balance,
'Optimism': self.optimism_balance
'Optimism': self.optimism_balance,
'Base': self.base_balance
}
for network in expected_balances:
@@ -295,7 +299,7 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
'').lower()
receiver_short_address = self.receiver['wallet_address'].replace(self.receiver['wallet_address'][6:-3],
'').lower()
for text in ['Account 1', sender_short_address, expected_amount]:
for text in [self.account_name, sender_short_address, expected_amount]:
if not self.wallet_view.from_data_container.get_child_element_by_text(text).is_element_displayed():
self.errors.append(
self.wallet_view,
@@ -376,7 +380,7 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
'1 SNT').is_element_displayed():
self.errors.append(self.wallet_view,
"%s: Spending cap is not shown on the 'Set Spending Cap' screen" % network)
for text in ['Account 1',
for text in [self.account_name,
self.sender['wallet_address'].replace(self.sender['wallet_address'][5:-3], '...').lower()]:
if not self.wallet_view.account_approval_info_container.get_child_element_by_text(
text).is_element_displayed():
@@ -426,6 +430,121 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
element=self.wallet_view.add_account_button, attempts=6)
self.errors.verify_no_errors()
@marks.testrail_id(741612)
def test_wallet_bridge_flow_mainnet(self):
self.wallet_view.navigate_back_to_wallet_view()
self.wallet_view.get_account_element().click()
self.wallet_view.bridge_button.click()
networks = {'Optimism': 'Arbitrum', 'Arbitrum': 'Base', 'Base': 'Optimism'}
amount = '0.001'
for network_from, network_to in networks.items():
self.wallet_view.just_fyi("Checking bridge from %s to %s" % (network_from, network_to))
self.wallet_view.select_asset('Ether')
self.wallet_view.select_network(network_from)
self.wallet_view.select_network(network_to)
self.wallet_view.set_amount(amount)
data_to_check = {
'Max fees': r"[$]\d+.\d+",
'Bridged to %s' % network_to: r"0.000\d+ ETH"
}
for key, expected_value in data_to_check.items():
try:
text = self.wallet_view.get_data_item_element_text(data_item_name=key)
if not re.findall(expected_value, text):
self.errors.append(self.wallet_view,
"%s to %s: %s is not a number - %s before pressing Review Bridge button" % (
network_from, network_to, key, text))
except TimeoutException:
self.errors.append(self.wallet_view,
"%s to %s: %s is not shown before pressing Review Bridge button" % (
network_from, network_to, key))
self.wallet_view.just_fyi("Checking routes from %s to %s" % (network_from, network_to))
try:
element = self.wallet_view.get_route_element('from')
element.wait_for_element()
shown_amount = element.amount_text
if shown_amount != amount + ' ETH':
self.errors.append(self.wallet_view, "%s to %s: 'From' route amount %s doesn't match expected %s" %
(network_from, network_to, shown_amount, amount + ' ETH'))
shown_network = element.network_text
if shown_network != network_from:
self.errors.append(self.wallet_view, "%s to %s: 'From' route network %s doesn't match expected %s" %
(network_from, network_to, shown_network, network_from))
except TimeoutException:
self.errors.append(self.wallet_view, "%s to %s: 'From' route is not shown" % (network_from, network_to))
try:
element = self.wallet_view.get_route_element('to')
element.wait_for_element()
shown_amount = element.amount_text
if not re.findall(r"0.000\d+ ETH", shown_amount):
self.errors.append(self.wallet_view, "%s to %s: 'To' route amount %s is not a number" %
(network_from, network_to, shown_amount))
shown_network = element.network_text
if shown_network != network_to:
self.errors.append(self.wallet_view, "%s to %s: 'To' route network %s doesn't match expected %s" %
(network_from, network_to, shown_network, network_to))
except TimeoutException:
self.errors.append(self.wallet_view, "%s to %s: 'To' route is not shown" % (network_from, network_to))
self.wallet_view.confirm_button.click()
self.wallet_view.just_fyi("Checking Bridge screen from %s to %s" % (network_from, network_to))
containers = {'from': self.wallet_view.from_data_container, 'to': self.wallet_view.to_data_container}
for name, container in containers.items():
try:
container.wait_for_element()
for text in [self.account_name,
self.sender['wallet_address'].replace(self.sender['wallet_address'][6:-3],
'').lower()]:
if not container.get_child_element_by_text(text).is_element_displayed():
self.errors.append(
self.wallet_view,
"%s to %s: Text %s is not shown in the '%s' data container on the Review Bridge screen"
% (network_from, network_to, text, name))
amount_text = container.amount_text
if name == 'from' and amount_text != amount + ' ETH':
self.errors.append(
self.wallet_view,
"%s to %s: amount %s in the 'from' data container doesn't match expected %s ETH"
% (network_from, network_to, amount_text, amount))
if name == 'to' and not re.findall(r"0.000\d+ ETH", amount_text):
self.errors.append(
self.wallet_view,
"%s to %s: amount %s in the 'to' data container is not a number"
% (network_from, network_to, amount_text))
except TimeoutException:
self.errors.append(self.wallet_view, "%s to %s: data '%s' is not shown in Review Bridge screen" %
(network_from, network_to, name))
if network_to == 'Arbitrum':
network_to_short_name = 'Arb1.'
elif network_to == 'Optimism':
network_to_short_name = 'Oeth.'
else:
network_to_short_name = network_to
data_to_check = {
'Est. time': ' min',
'Max fees': r"[$]\d+.\d+",
'Bridged to %s' % network_to_short_name: r"0.000\d+ ETH"
}
for key, expected_value in data_to_check.items():
try:
text = self.wallet_view.get_data_item_element_text(data_item_name=key)
if not re.findall(expected_value, text):
self.errors.append(self.wallet_view,
"%s to %s: %s has incorrect value - %s on the Review Bridge screen" % (
network_from, network_to, key, text))
except TimeoutException:
self.errors.append(self.wallet_view,
"%s to %s: %s is not shown on the Review Bridge screen" % (
network_from, network_to, key))
self.wallet_view.slide_button_track.slide()
if not self.wallet_view.password_input.is_element_displayed():
self.errors.append("%s to %s: can't confirm bridge" % (network_from, network_to))
self.wallet_view.click_system_back_button(times=5)
self.wallet_view.click_system_back_button_until_presence_of_element(
element=self.wallet_view.add_account_button, attempts=6)
self.errors.verify_no_errors()
@marks.testrail_id(727231)
def test_wallet_add_remove_regular_account(self):
self.wallet_view.navigate_back_to_wallet_view()
@@ -441,7 +560,7 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
self.wallet_view.just_fyi("Checking that the new wallet is added to the Share QR Code menu")
self.home_view.show_qr_code_button.click()
self.home_view.share_wallet_tab_button.click()
if self.home_view.account_name_text.text != 'Account 1':
if self.home_view.account_name_text.text != self.account_name:
self.errors.append(self.home_view, "Incorrect first account is shown on Share QR Code menu")
self.home_view.qr_code_image_element.swipe_left_on_element()
try:
@@ -484,7 +603,7 @@ class TestWalletOneDevice(MultipleSharedDeviceTestCase):
self.wallet_view.just_fyi("Checking that the new wallet is added to the Share QR Code menu")
self.home_view.show_qr_code_button.click()
self.home_view.share_wallet_tab_button.click()
if self.home_view.account_name_text.text != 'Account 1':
if self.home_view.account_name_text.text != self.account_name:
self.errors.append(self.home_view, "Incorrect first account is shown on Share QR Code menu")
self.home_view.qr_code_image_element.swipe_left_on_element()
try:
+11 -3
View File
@@ -247,7 +247,9 @@ class HomeView(BaseView):
def __init__(self, driver):
super().__init__(driver)
self.plus_button = Button(self.driver, accessibility_id="new-chat-button")
self.skip_editing_profile_button = Button(self.driver, xpath="//*[@content-desc='button-two']//*[@text='Skip']")
self.edit_profile_button = Button(self.driver, xpath="//*[@content-desc='button-one']//*[@text='Edit Profile']")
self.submit_create_profile_button = Button(self.driver, accessibility_id="submit-create-profile-button")
self.plus_community_button = Button(self.driver, accessibility_id="new-communities-button")
self.chat_name_text = Text(self.driver, accessibility_id="chat-name-text")
self.start_new_chat_button = ChatButton(self.driver, accessibility_id="start-1-1-chat-button")
@@ -400,9 +402,15 @@ class HomeView(BaseView):
self.close_activity_centre.wait_for_rendering_ended_and_click()
self.chats_tab.wait_for_visibility_of_element()
def add_contact(self, public_key, nickname=''):
def add_contact(self, public_key, username='', nickname=''):
self.driver.info("Adding user to Contacts via chats > add new contact")
self.new_chat_button.click_until_presence_of_element(self.add_a_contact_chat_bottom_sheet_button)
self.new_chat_button.click()
if username:
self.edit_profile_button.click()
self.get_sign_in_view().profile_title_input.send_keys(username)
self.submit_create_profile_button.click()
else:
self.skip_editing_profile_button.click_if_shown()
self.add_a_contact_chat_bottom_sheet_button.click()
chat = self.get_chat_view()
+32 -5
View File
@@ -59,6 +59,17 @@ class ActivityElement(BaseElement):
xpath="//*[@content-desc='context-tag'][3]/android.widget.TextView").text
class ConfirmationViewInfoContainer(BaseElement):
def __init__(self, driver, label_name: str):
self.locator = "//*[@content-desc='summary-%s-label']/following-sibling::android.view.ViewGroup[1]" % label_name
super().__init__(driver, xpath=self.locator)
@property
def amount_text(self):
return Text(self.driver, xpath=self.locator + "/*[@content-desc='networks']/android.widget.TextView").text
class WalletView(BaseView):
def __init__(self, driver):
super().__init__(driver)
@@ -85,6 +96,7 @@ class WalletView(BaseView):
self.account_emoji_button = Button(self.driver, accessibility_id='account-emoji')
self.send_button = Button(self.driver, accessibility_id='send')
self.swap_button = Button(self.driver, accessibility_id='swap')
self.bridge_button = Button(self.driver, accessibility_id='bridge')
self.send_from_drawer_button = Button(
self.driver, xpath="//*[@content-desc='send']/*[@content-desc='left-icon-for-action']")
self.copy_address_button = Button(self.driver, accessibility_id='copy-address')
@@ -105,11 +117,9 @@ class WalletView(BaseView):
self.confirm_button = Button(self.driver, accessibility_id='button-one')
self.done_button = Button(self.driver, accessibility_id='done')
# Review Send page
self.from_data_container = BaseElement(
self.driver, xpath="//*[@content-desc='summary-from-label']/following-sibling::android.view.ViewGroup[1]")
self.to_data_container = BaseElement(
self.driver, xpath="//*[@content-desc='summary-to-label']/following-sibling::android.view.ViewGroup[1]")
# Review Send and Review Bridge screens
self.from_data_container = ConfirmationViewInfoContainer(self.driver, label_name='from')
self.to_data_container = ConfirmationViewInfoContainer(self.driver, label_name='to')
# Swap flow
self.approve_swap_button = Button(self.driver, accessibility_id='Approve')
@@ -282,3 +292,20 @@ class WalletView(BaseView):
locator = "//*[@content-desc='swap-input'][2]//*[@content-desc='token-avatar']" \
"/following-sibling::*//*[starts-with(@text,'0.000')]"
BaseElement(self.driver, xpath=locator).wait_for_visibility_of_element()
def get_route_element(self, route_name: str):
class RouteElement(BaseElement):
def __init__(self, driver, route_name):
self.locator = "//*[@text='%s']/following-sibling::*[@content-desc='container'][%s]" % (
route_name.capitalize(), 1 if route_name == 'from' else 2)
super().__init__(driver, xpath=self.locator)
@property
def amount_text(self):
return Text(self.driver, xpath="(%s//android.widget.TextView)[1]" % self.locator).text
@property
def network_text(self):
return Text(self.driver, xpath="(%s//android.widget.TextView)[2]" % self.locator).text
return RouteElement(self.driver, route_name)
+2
View File
@@ -59,6 +59,7 @@
"add-bootnode": "Add bootnode",
"add-contact": "Add contact",
"add-custom-token": "Add custom token",
"add-display-name-and-picture": "Add an optional display name and profile picture so others can easily recognize you",
"add-eth": "Add ETH",
"add-favourite": "Add favourite",
"add-mailserver": "Add Status node",
@@ -1348,6 +1349,7 @@
"joined-group-chat-description": "You've joined {{group-name}} from invitation by {{username}}",
"jul": "Jul",
"jun": "Jun",
"just-introduce-yourself": "Introduce yourself",
"K": "K",
"keep-card-steady": "Keep the card steady under the phone",
"keep-key": "KeepKey",