From 7948582e46763dabfbcd0cd56d22539a49f9cc4a Mon Sep 17 00:00:00 2001 From: Icaro Motta Date: Mon, 18 Mar 2024 12:07:15 -0300 Subject: [PATCH] Account selection: Implement features related to editing shared addresses (#19177) * Fix schema error * Implement feature to edit shared addresses * fix: show tokens when sharing all future addresses toggled * fix: eligibility not checked when toggling all addresses * Revert "fix: show tokens when sharing all future addresses toggled" This reverts commit 8443a3432e1ea69cd692caf51db03324b64fccda. * fix: don't fetch balances on mount if toggled share all --------- Co-authored-by: Cristian Lungu --- .../bottom_actions/component_spec.cljs | 11 + .../drawers/bottom_actions/view.cljs | 10 +- src/schema/re_frame.cljs | 10 +- .../actions/accounts_selection/effects.cljs | 102 +++++ .../actions/accounts_selection/events.cljs | 103 +++++ .../accounts_selection/events_test.cljs | 169 ++++++++ .../actions/accounts_selection/view.cljs | 187 +++++---- .../addresses_for_permissions/events.cljs | 95 +++++ .../events_test.cljs | 100 ++++- .../addresses_for_permissions/style.cljs | 6 + .../addresses_for_permissions/view.cljs | 395 ++++++++++++++---- .../actions/airdrop_addresses/events.cljs | 9 + .../airdrop_addresses/events_test.cljs | 18 + .../actions/airdrop_addresses/view.cljs | 90 ++-- .../actions/community_options/view.cljs | 23 +- .../communities/actions/leave/events.cljs | 8 +- .../contexts/communities/events.cljs | 127 +----- .../contexts/communities/events_test.cljs | 128 ++---- .../contexts/communities/overview/events.cljs | 44 +- .../contexts/communities/overview/view.cljs | 4 +- .../preview/quo/drawers/bottom_actions.cljs | 46 +- src/status_im/feature_flags.cljs | 10 +- src/status_im/navigation/screens.cljs | 18 +- src/status_im/subs/communities.cljs | 51 --- src/status_im/subs/communities_test.cljs | 42 -- .../subs/community/account_selection.cljs | 69 +++ .../community/account_selection_test.cljs | 75 ++++ src/status_im/subs/root.cljs | 6 + translations/en.json | 13 + 29 files changed, 1403 insertions(+), 566 deletions(-) create mode 100644 src/status_im/contexts/communities/actions/accounts_selection/effects.cljs create mode 100644 src/status_im/contexts/communities/actions/accounts_selection/events.cljs create mode 100644 src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs create mode 100644 src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs create mode 100644 src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs create mode 100644 src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs create mode 100644 src/status_im/subs/community/account_selection.cljs create mode 100644 src/status_im/subs/community/account_selection_test.cljs diff --git a/src/quo/components/drawers/bottom_actions/component_spec.cljs b/src/quo/components/drawers/bottom_actions/component_spec.cljs index 5be35794ad..2ccf7ff278 100644 --- a/src/quo/components/drawers/bottom_actions/component_spec.cljs +++ b/src/quo/components/drawers/bottom_actions/component_spec.cljs @@ -59,4 +59,15 @@ :button-one-label "Request to join"}]) (h/is-truthy (h/get-by-text "Eligible to join as")) (h/is-truthy (h/get-by-text "Admin")) + (h/is-truthy (h/get-by-text "Request to join"))) + + (h/test "render with top description with custom text & 1 action" + (render [bottom-actions/view + {:description :top + :role :admin + :actions :one-action + :description-top-text "You'll be an" + :button-one-label "Request to join"}]) + (h/is-truthy (h/get-by-text "You'll be an")) + (h/is-truthy (h/get-by-text "Admin")) (h/is-truthy (h/get-by-text "Request to join")))) diff --git a/src/quo/components/drawers/bottom_actions/view.cljs b/src/quo/components/drawers/bottom_actions/view.cljs index 3361c4e24c..46d97a22c1 100644 --- a/src/quo/components/drawers/bottom_actions/view.cljs +++ b/src/quo/components/drawers/bottom_actions/view.cljs @@ -19,6 +19,7 @@ [:actions [:maybe [:enum :one-action :two-actions]]] [:description {:optional true} [:maybe [:enum :top :bottom :top-error]]] [:description-text {:optional true} [:maybe :string]] + [:description-top-text {:optional true} [:maybe :string]] [:error-message {:optional true} [:maybe :string]] [:role {:optional true} [:maybe [:enum :admin :member :token-master :owner]]] [:button-one-label {:optional true} [:maybe :string]] @@ -38,8 +39,8 @@ :owner :i/crown}) (defn- view-internal - [{:keys [actions description description-text error-message role button-one-label button-two-label - blur? button-one-props button-two-props theme scroll? container-style]}] + [{:keys [actions description description-text description-top-text error-message role button-one-label + button-two-label blur? button-one-props button-two-props theme scroll? container-style]}] [rn/view {:style (merge (style/container scroll? blur? theme) container-style)} (when (= description :top-error) @@ -54,12 +55,11 @@ error-message]]) (when (and (= description :top) role) - [rn/view - {:style style/description-top} + [rn/view {:style style/description-top} [text/text {:size :paragraph-2 :style (style/description-top-text scroll? blur? theme)} - (i18n/label :t/eligible-to-join-as)] + (or description-top-text (i18n/label :t/eligible-to-join-as))] [context-tag/view {:type :icon :size 24 diff --git a/src/schema/re_frame.cljs b/src/schema/re_frame.cljs index cbe2a95456..6f12af5aa8 100644 --- a/src/schema/re_frame.cljs +++ b/src/schema/re_frame.cljs @@ -5,6 +5,14 @@ (def ^:private ?cofx [:map]) +(def ^:private ?event + [:and + [:vector {:min 1} :any] + [:catn + [:event-id keyword?] + [:event-args [:* :any]]]]) + (defn register-schemas [] - (registry/register ::cofx ?cofx)) + (registry/register ::cofx ?cofx) + (registry/register ::event ?event)) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/effects.cljs b/src/status_im/contexts/communities/actions/accounts_selection/effects.cljs new file mode 100644 index 0000000000..d92229ab93 --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/effects.cljs @@ -0,0 +1,102 @@ +(ns status-im.contexts.communities.actions.accounts-selection.effects + (:require + [promesa.core :as p] + [schema.core :as schema] + [status-im.common.json-rpc.events :as rpc] + [utils.re-frame :as rf])) + +(defn- generate-requests-for-signing + [pub-key community-id addresses-to-reveal] + (p/create + (fn [p-resolve p-reject] + (rpc/call + {:method :wakuext_generateJoiningCommunityRequestsForSigning + :params [pub-key community-id addresses-to-reveal] + :on-success p-resolve + :on-error #(p-reject (str "failed to generate requests for signing\n" %))})))) + +(defn- sign-data + [sign-params password] + (p/create + (fn [p-resolve p-reject] + (rpc/call + {:method :wakuext_signData + :params [(map #(assoc % :password password) sign-params)] + :on-success p-resolve + :on-error #(p-reject (str "failed to sign data\n" %))})))) + +(defn- edit-shared-addresses-for-community + [community-id signatures addresses-to-reveal airdrop-address] + (p/create + (fn [p-resolve p-reject] + (rpc/call + {:method :wakuext_editSharedAddressesForCommunity + :params [{:communityId community-id + :signatures signatures + :addressesToReveal addresses-to-reveal + :airdropAddress airdrop-address}] + :js-response true + :on-success p-resolve + :on-error p-reject})))) + +(defn- request-to-join + [community-id signatures addresses-to-reveal airdrop-address] + (p/create + (fn [p-resolve p-reject] + (rpc/call + {:method :wakuext_requestToJoinCommunity + :params [{:communityId community-id + :signatures signatures + :addressesToReveal addresses-to-reveal + :airdropAddress airdrop-address}] + :js-response true + :on-success p-resolve + :on-error p-reject})))) + +(defn- run-callback-or-event + [callback-or-event result] + (cond (fn? callback-or-event) + (callback-or-event result) + + (vector? callback-or-event) + (rf/dispatch (conj callback-or-event result)))) + +(defn- sign-and-call-endpoint + [{:keys [community-id password pub-key + addresses-to-reveal airdrop-address + on-success on-error + callback]}] + (-> (p/let [sign-params (generate-requests-for-signing pub-key community-id addresses-to-reveal) + signatures (sign-data sign-params password) + result (callback community-id + signatures + addresses-to-reveal + airdrop-address)] + (run-callback-or-event on-success result)) + (p/catch #(run-callback-or-event on-error %)))) + +(schema/=> sign-and-call-endpoint + [:=> + [:cat + [:map {:closed true} + [:community-id string?] + [:password string?] + [:pub-key string?] + [:addresses-to-reveal + [:or [:set string?] + [:sequential string?]]] + [:airdrop-address string?] + [:on-success [:or fn? :schema.re-frame/event]] + [:on-error [:or fn? :schema.re-frame/event]] + [:callback fn?]]] + :any]) + +(rf/reg-fx :effects.community/edit-shared-addresses + (fn [opts] + (sign-and-call-endpoint + (assoc opts :callback edit-shared-addresses-for-community)))) + +(rf/reg-fx :effects.community/request-to-join + (fn [opts] + (sign-and-call-endpoint + (assoc opts :callback request-to-join)))) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/events.cljs b/src/status_im/contexts/communities/actions/accounts_selection/events.cljs new file mode 100644 index 0000000000..601c999dd8 --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/events.cljs @@ -0,0 +1,103 @@ +(ns status-im.contexts.communities.actions.accounts-selection.events + (:require + status-im.contexts.communities.actions.accounts-selection.effects + [status-im.contexts.communities.utils :as utils] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn initialize-permission-addresses + [{:keys [db]} [community-id]] + (let [{:keys [joined]} (get-in db [:communities community-id]) + pending-requests (get-in db [:communities/my-pending-requests-to-join community-id]) + init-using-revealed-accounts? (or joined (seq pending-requests))] + {:fx [(if init-using-revealed-accounts? + [:dispatch + [:communities/get-revealed-accounts community-id + [:communities/do-init-permission-addresses community-id]]] + [:dispatch [:communities/do-init-permission-addresses community-id]])]})) + +(rf/reg-event-fx :communities/initialize-permission-addresses initialize-permission-addresses) + +(defn do-init-permission-addresses + [{:keys [db]} [community-id revealed-accounts]] + (let [wallet-accounts (utils/sorted-non-watch-only-accounts db) + addresses-to-reveal (if (seq revealed-accounts) + (set (keys revealed-accounts)) + ;; Reveal all addresses as fallback. + (set (map :address wallet-accounts))) + + ;; When there are no revealed addresses, such as when joining a + ;; community, use first address for airdrops. + airdrop-address (or (->> revealed-accounts + vals + (filter :airdrop-address?) + first + :address) + (->> wallet-accounts + first + :address))] + {:db (-> db + ;; Set to false by default while we don't persist the user's choice + ;; in status-go, otherwise whenever the view is mounted, the choice + ;; of selected addresses won't be respected. + (assoc-in [:communities/selected-share-all-addresses community-id] false) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal) + (assoc-in [:communities/all-airdrop-addresses community-id] airdrop-address)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + ;; Pre-fetch permissions check so that when first opening the + ;; Addresses for Permissions screen the highest permission role is + ;; already available and no incorrect information flashes on screen. + [:dispatch + [:communities/check-permissions-to-join-during-selection community-id + addresses-to-reveal]]]})) + +(rf/reg-event-fx :communities/do-init-permission-addresses do-init-permission-addresses) + +(defn edit-shared-addresses + "This event will effectively persist the choice of addresses and airdrop address + in status-go. It can either take addresses to share AND an airdrop address, or + just addresses to share OR an airdrop address. This is because when the user + is selecting an airdrop address, we must submit to status-go the current + choice of addresses to share, and vice-versa. If we omit addresses to share, + status-go will default to all available." + [{:keys [db]} [{:keys [community-id password on-success addresses airdrop-address]}]] + (let [pub-key (get-in db [:profile/profile :public-key]) + wallet-accounts (utils/sorted-non-watch-only-accounts db) + addresses-to-reveal (if (seq addresses) + (set addresses) + (get-in db [:communities/all-addresses-to-reveal community-id])) + new-airdrop-address (if (contains? addresses-to-reveal airdrop-address) + airdrop-address + (->> wallet-accounts + (filter #(contains? addresses-to-reveal (:address %))) + first + :address))] + {:fx [[:effects.community/edit-shared-addresses + {:community-id community-id + :password password + :pub-key pub-key + :addresses-to-reveal addresses-to-reveal + :airdrop-address new-airdrop-address + :on-success (fn [] + (when (fn? on-success) + (on-success addresses-to-reveal new-airdrop-address)) + (rf/dispatch [:communities/edit-shared-addresses-success + community-id addresses-to-reveal airdrop-address])) + :on-error [:communities/edit-shared-addresses-failure community-id]}]]})) + +(rf/reg-event-fx :communities/edit-shared-addresses edit-shared-addresses) + +(rf/reg-event-fx :communities/edit-shared-addresses-success + (fn [_ [community-id addresses-to-reveal airdrop-address]] + {:fx [[:dispatch [:communities/set-airdrop-address community-id airdrop-address]] + [:dispatch [:communities/set-addresses-to-reveal community-id addresses-to-reveal]]]})) + +(rf/reg-event-fx :communities/edit-shared-addresses-failure + (fn [{:keys [db]} [community-id error]] + (log/error "failed to edit shared addresses" + {:event :communities/edit-shared-addresses + :community-id community-id + :error error}) + {:db (assoc-in db [:password-authentication :error] error)})) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs b/src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs new file mode 100644 index 0000000000..47e504b69b --- /dev/null +++ b/src/status_im/contexts/communities/actions/accounts_selection/events_test.cljs @@ -0,0 +1,169 @@ +(ns status-im.contexts.communities.actions.accounts-selection.events-test + (:require [cljs.test :refer [deftest is testing]] + matcher-combinators.test + [status-im.contexts.communities.actions.accounts-selection.events :as sut])) + +(def community-id "0x99") +(def password "password") + +(def wallet-accounts + {"0xA" {:address "0xA" + :watch-only? true + :position 2 + :color :red + :emoji "🦇"} + "0xB" {:address "0xB" + :position 0 + :color :blue + :emoji "🐈"} + "0xC" {:address "0xC" + :position 1 + :color :orange + :emoji "🛏️"}}) + +(def permissioned-accounts + [{:address "0xB" + :position 0 + :color :blue + :emoji "🐈" + :airdrop-address? true + :reveal? true} + {:address "0xC" + :position 1 + :color :orange + :emoji "🛏️" + :airdrop-address? false + :reveal? false}]) + +(deftest initialize-permission-addresses-test + (testing "fetches revealed accounts when pending and not joined" + (let [cofx {:db {:communities {community-id {:joined false}} + :communities/my-pending-requests-to-join {community-id [:anything]}}}] + (is (match? + {:fx [[:dispatch + [:communities/get-revealed-accounts community-id + [:communities/do-init-permission-addresses community-id]]]]} + (sut/initialize-permission-addresses cofx [community-id]))))) + + (testing "fetches revealed accounts when not pending and joined" + (let [cofx {:db {:communities {community-id {:joined true}} + :communities/my-pending-requests-to-join {}}}] + (is (match? + {:fx [[:dispatch + [:communities/get-revealed-accounts community-id + [:communities/do-init-permission-addresses community-id]]]]} + (sut/initialize-permission-addresses cofx [community-id]))))) + + (testing "does not fetch revealed accounts when not pending and not joined" + (let [cofx {:db {:communities {community-id {:joined false}} + :communities/my-pending-requests-to-join {}}}] + (is (match? + {:fx [[:dispatch [:communities/do-init-permission-addresses community-id]]]} + (sut/initialize-permission-addresses cofx [community-id])))))) + +(deftest do-init-permission-addresses-test + (testing "uses already revealed accounts to initialize shareable accounts" + (let [cofx {:db {:wallet {:accounts wallet-accounts}}} + revealed-accounts {"0xC" {:address "0xC" + :airdrop-address? true + :position 1}} + airdrop-address "0xC" + addresses-to-reveal #{"0xC"}] + (is + (match? + {:db (-> (:db cofx) + (assoc-in [:communities/selected-share-all-addresses community-id] false) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal) + (assoc-in [:communities/all-airdrop-addresses community-id] airdrop-address)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + ;; Pre-fetch permissions check so that when first opening the + ;; Addresses for Permissions screen the highest permission role is + ;; already available and no incorrect information flashes on screen. + [:dispatch + [:communities/check-permissions-to-join-during-selection community-id + addresses-to-reveal]]]} + (sut/do-init-permission-addresses cofx [community-id revealed-accounts]))))) + + ;; Expect to mark all addresses to be revealed and first one to receive + ;; airdrops when no addresses were previously revealed. + (testing "handles case where there are no previously revealed addresses" + (let [cofx {:db {:wallet {:accounts wallet-accounts}}} + addresses-to-reveal #{"0xB" "0xC"} + revealed-accounts []] + (is + (match? + {:db (-> (:db cofx) + (assoc-in [:communities/selected-share-all-addresses community-id] false) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + [:dispatch + [:communities/check-permissions-to-join-during-selection community-id + addresses-to-reveal]]]} + (sut/do-init-permission-addresses cofx [community-id revealed-accounts])))))) + +(deftest edit-shared-addresses-test + (testing + "when airdrop address is passed, but addresses to reveal is not, then + fallback to all wallet addresses" + (let [pub-key "abcdef" + revealed-addresses #{"0xB" "0xC"} + cofx {:db {:profile/profile {:public-key pub-key} + :communities/all-addresses-to-reveal {community-id revealed-addresses}}} + airdrop-address "0xB" + + actual + (sut/edit-shared-addresses + cofx + [{:community-id community-id + :password password + :airdrop-address airdrop-address + :on-success (fn [new-addresses-to-reveal] + (is (match? revealed-addresses new-addresses-to-reveal)))}]) + + on-success-wrapper (-> actual :fx first second :on-success)] + (is (match? + {:fx [[:effects.community/edit-shared-addresses + {:community-id community-id + :password password + :pub-key pub-key + :addresses-to-reveal revealed-addresses + :airdrop-address airdrop-address + :on-success fn? + :on-error [:communities/edit-shared-addresses-failure community-id]}]]} + actual)) + + (on-success-wrapper))) + + (testing "when addresses to reveal are passed, but airdrop address is not" + (let [pub-key "abcdef" + cofx {:db {:profile/profile {:public-key pub-key} + :wallet {:accounts wallet-accounts} + :communities/all-addresses-to-reveal {community-id #{"0xB" "0xC"}}}} + + actual (sut/edit-shared-addresses + cofx + [{:community-id community-id + :password password + :addresses ["0xC"] + :on-success (fn [new-addresses-to-reveal new-airdrop-address] + (is (= #{"0xC"} new-addresses-to-reveal)) + (is (= "0xC" new-airdrop-address)))}]) + + on-success-wrapper + (-> actual :fx first second :on-success)] + (is (match? + {:fx [[:effects.community/edit-shared-addresses + {:community-id community-id + :password password + :pub-key pub-key + :addresses-to-reveal #{"0xC"} + :airdrop-address "0xC" + :on-success fn? + :on-error [:communities/edit-shared-addresses-failure community-id]}]]} + actual)) + + (on-success-wrapper)))) diff --git a/src/status_im/contexts/communities/actions/accounts_selection/view.cljs b/src/status_im/contexts/communities/actions/accounts_selection/view.cljs index 713d5ef455..16fc2ec2c6 100644 --- a/src/status_im/contexts/communities/actions/accounts_selection/view.cljs +++ b/src/status_im/contexts/communities/actions/accounts_selection/view.cljs @@ -14,83 +14,110 @@ (defn view [] - (let [{id :community-id} (rf/sub [:get-screen-params]) - show-addresses-for-permissions (fn [] - (rf/dispatch [:show-bottom-sheet - {:community-id id - :content addresses-for-permissions/view}])) - show-airdrop-addresses (fn [] - (rf/dispatch [:show-bottom-sheet - {:community-id id - :content airdrop-addresses/view}])) - navigate-back #(rf/dispatch [:navigate-back]) - join-and-go-back (fn [] - (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-with-addresses - {:community-id id :password %}])}]) - (navigate-back))] - (fn [] - (let [{:keys [name color images]} (rf/sub [:communities/community id]) - airdrop-account (rf/sub [:communities/airdrop-account id]) - selected-accounts (rf/sub [:communities/selected-permission-accounts id]) - {:keys [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/safe-area-view {:style style/container} - [quo/page-nav - {:text-align :left - :icon-name :i/close - :on-press navigate-back - :accessibility-label :back-button}] - [quo/page-top - {:title (i18n/label :t/request-to-join) - :description :context-tag - :context-tag {:type :community - :size 24 - :community-logo (get-in images [:thumbnail :uri]) - :community-name name}}] - [gesture/scroll-view - [:<> - [quo/text - {:style style/section-title - :accessibility-label :community-rules-title - :weight :semi-bold - :size :paragraph-1} - (i18n/label :t/address-to-share)] - [quo/category - {:list-type :settings - :data [{:title (i18n/label :t/join-as-a {:role highest-role-text}) - :on-press show-addresses-for-permissions - :description :text - :action :arrow - :label :preview - :label-props {:type :accounts - :data selected-accounts} - :description-props {:text (i18n/label :t/all-addresses)}} - {:title (i18n/label :t/for-airdrops) - :on-press show-airdrop-addresses - :description :text - :action :arrow - :label :preview - :label-props {:type :accounts - :data [airdrop-account]} - :description-props {:text (:name airdrop-account)}}]}] - [quo/text - {:style style/section-title - :accessibility-label :community-rules-title - :weight :semi-bold - :size :paragraph-1} - (i18n/label :t/community-rules)] - [community-rules/view id]]] - [rn/view {:style (style/bottom-actions)} - [quo/slide-button - {:size :size-48 - :track-text (i18n/label :t/slide-to-request-to-join) - :track-icon :i/face-id - :customization-color color - :on-complete join-and-go-back}]]])))) + (let [{id :community-id} (rf/sub [:get-screen-params]) + {:keys [name color images joined]} (rf/sub [:communities/community id]) + airdrop-account (rf/sub [:communities/airdrop-account id]) + revealed-accounts (rf/sub [:communities/accounts-to-reveal id]) + {:keys [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)) + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + navigate-back (rn/use-callback #(rf/dispatch [:navigate-back])) + + show-addresses-for-permissions + (rn/use-callback + (fn [] + (if can-edit-addresses? + (rf/dispatch [:open-modal :addresses-for-permissions {:community-id id}]) + (rf/dispatch [:show-bottom-sheet + {:community-id id + :content (fn [] [addresses-for-permissions/view])}]))) + [can-edit-addresses?]) + + show-airdrop-addresses + (rn/use-callback + (fn [] + (if can-edit-addresses? + (rf/dispatch [:open-modal :address-for-airdrop {:community-id id}]) + (rf/dispatch [:show-bottom-sheet + {:community-id id + :content (fn [] [airdrop-addresses/view])}]))) + [can-edit-addresses?]) + + confirm-choices + (rn/use-callback + (fn [] + (rf/dispatch + [:password-authentication/show + {:content (fn [] [password-authentication/view])} + {:label (i18n/label :t/join-open-community) + :on-press (fn [password] + (rf/dispatch [:communities/request-to-join-with-addresses + {:community-id id :password password}]))}]) + (navigate-back)))] + (rn/use-mount + (fn [] + (rf/dispatch [:communities/initialize-permission-addresses id]))) + + [rn/safe-area-view {:style style/container} + [quo/page-nav + {:text-align :left + :icon-name :i/close + :on-press navigate-back + :accessibility-label :back-button}] + [quo/page-top + {:title (if can-edit-addresses? + (i18n/label :t/edit-shared-addresses) + (i18n/label :t/request-to-join)) + :description :context-tag + :context-tag {:type :community + :size 24 + :community-logo (get-in images [:thumbnail :uri]) + :community-name name}}] + [gesture/scroll-view + [:<> + (when-not can-edit-addresses? + [quo/text + {:style style/section-title + :accessibility-label :community-rules-title + :weight :semi-bold + :size :paragraph-1} + (i18n/label :t/address-to-share)]) + [quo/category + {:list-type :settings + :data [{:title (if joined + (i18n/label :t/you-are-a-role {:role highest-role-text}) + (i18n/label :t/join-as-a {:role highest-role-text})) + :on-press show-addresses-for-permissions + :description :text + :action :arrow + :label :preview + :label-props {:type :accounts + :data revealed-accounts} + :description-props {:text (i18n/label :t/all-addresses)}} + {:title (i18n/label :t/for-airdrops) + :on-press show-airdrop-addresses + :description :text + :action :arrow + :label :preview + :label-props {:type :accounts + :data [airdrop-account]} + :description-props {:text (:name airdrop-account)}}]}] + (when-not can-edit-addresses? + [quo/text + {:style style/section-title + :accessibility-label :community-rules-title + :weight :semi-bold + :size :paragraph-1} + (i18n/label :t/community-rules)]) + (when-not can-edit-addresses? + [community-rules/view id])]] + (when-not can-edit-addresses? + [rn/view {:style (style/bottom-actions)} + [quo/slide-button + {:size :size-48 + :track-text (i18n/label :t/slide-to-request-to-join) + :track-icon :i/face-id + :customization-color color + :on-complete confirm-choices}]])])) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs index b7018aa478..f509d2d7b6 100644 --- a/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/events.cljs @@ -1,6 +1,7 @@ (ns status-im.contexts.communities.actions.addresses-for-permissions.events (:require [schema.core :as schema] + [status-im.contexts.communities.utils :as utils] [taoensso.timbre :as log] [utils.re-frame :as rf])) @@ -56,3 +57,97 @@ map?]) (rf/reg-event-fx :communities/get-permissioned-balances-success get-permissioned-balances-success) + +(rf/reg-event-fx :communities/addresses-for-permissions-cancel-request + (fn [_ [request-id]] + {:fx [[:dispatch [:communities/cancel-request-to-join request-id]] + [:dispatch [:pop-to-root :shell-stack]] + [:dispatch [:hide-bottom-sheet]]]})) + +(rf/reg-event-fx :communities/addresses-for-permissions-leave + (fn [_ [community-id]] + {:fx [[:dispatch [:communities/leave community-id]] + [:dispatch [:hide-bottom-sheet]] + [:dispatch [:dismiss-modal :addresses-for-permissions]] + [:dispatch [:pop-to-root :shell-stack]]]})) + +(defn check-permissions-to-join-for-selection + [{:keys [db]} [community-id addresses]] + (if (empty? addresses) + ;; When there are no addresses we can't just check permissions, otherwise + ;; status-go will consider all possible addresses and the user will see the + ;; incorrect highest permission role. + {:db (update db :communities/permissions-checks-for-selection dissoc community-id)} + (let [{:keys [checking?]} (get-in db [:communities/permissions-checks-for-selection community-id])] + (when-not checking? + {:db (assoc-in db [:communities/permissions-checks-for-selection community-id :checking?] true) + :fx [[:json-rpc/call + [{:method :wakuext_checkPermissionsToJoinCommunity + :params [{:communityId community-id :addresses addresses}] + :on-success [:communities/check-permissions-to-join-during-selection-success + community-id] + :on-error [:communities/check-permissions-to-join-during-selection-failure + community-id addresses]}]]]})))) + +;; This event should be used to check permissions temporarily because it won't +;; mutate the state `:communities/permissions-check` (used by many other +;; screens). +(rf/reg-event-fx :communities/check-permissions-to-join-during-selection + check-permissions-to-join-for-selection) + +(rf/reg-event-fx :communities/check-permissions-to-join-during-selection-success + (fn [{:keys [db]} [community-id result]] + {:db (assoc-in db + [:communities/permissions-checks-for-selection community-id] + {:checking? false + :based-on-client-selection? true + :check result})})) + +(rf/reg-event-fx :communities/check-permissions-to-join-during-selection-failure + (fn [_ [community-id addresses error]] + (log/error "failed to check permissions for currently selected addresses" + {:event :communities/check-permissions-to-join-during-selection + :community-id community-id + :addresses addresses + :error error}))) + +(defn set-permissioned-accounts + [{:keys [db]} [community-id addresses-to-reveal]] + (let [addresses-to-reveal (set addresses-to-reveal) + wallet-accounts (utils/sorted-non-watch-only-accounts db) + current-airdrop-address (get-in db [:communities/all-airdrop-addresses community-id]) + new-airdrop-address (if (contains? addresses-to-reveal current-airdrop-address) + current-airdrop-address + (->> wallet-accounts + (filter #(contains? addresses-to-reveal (:address %))) + first + :address))] + {:db (-> db + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal) + (assoc-in [:communities/all-airdrop-addresses community-id] new-airdrop-address)) + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id addresses-to-reveal :based-on-client-selection]] + [:dispatch [:hide-bottom-sheet]]]})) + +(rf/reg-event-fx :communities/set-addresses-to-reveal set-permissioned-accounts) + +(defn set-share-all-addresses + [{:keys [db]} [community-id new-value]] + (let [current-addresses (get-in db [:communities/all-addresses-to-reveal community-id]) + addresses-to-reveal (if new-value + (->> (utils/sorted-non-watch-only-accounts db) + (map :address) + set) + current-addresses)] + {:db (-> db + (assoc-in [:communities/selected-share-all-addresses community-id] new-value) + (assoc-in [:communities/all-addresses-to-reveal community-id] addresses-to-reveal)) + ;; We should check permissions again because the flag is being enabled and + ;; different addresses will be revealed. + :fx [(when new-value + [:dispatch + [:communities/check-permissions-to-join-during-selection + community-id addresses-to-reveal]])]})) + +(rf/reg-event-fx :communities/set-share-all-addresses set-share-all-addresses) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs index 7a77ab2b2a..6ae69760ac 100644 --- a/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/events_test.cljs @@ -1,6 +1,6 @@ (ns status-im.contexts.communities.actions.addresses-for-permissions.events-test (:require - [cljs.test :refer [deftest is]] + [cljs.test :refer [deftest is testing]] [status-im.contexts.communities.actions.addresses-for-permissions.events :as sut])) (def community-id "0x1") @@ -13,3 +13,101 @@ :on-success [:communities/get-permissioned-balances-success community-id] :on-error fn?}]]]} (sut/get-permissioned-balances cofx [community-id]))))) + +(deftest set-permissioned-accounts-test + (testing "new accounts contain the current airdrop address" + (let [cofx {:db {:communities/all-addresses-to-reveal + {community-id #{"0xA" "0xB"}}}} + addresses-to-reveal ["0xA"] + expected-addresses-to-reveal (set addresses-to-reveal)] + (is (match? + {:db {:communities/all-addresses-to-reveal + {community-id expected-addresses-to-reveal}} + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id expected-addresses-to-reveal :based-on-client-selection]] + [:dispatch [:hide-bottom-sheet]]]} + (sut/set-permissioned-accounts cofx [community-id addresses-to-reveal]))))) + + (testing "new accounts do not contain the current airdrop address" + (let [cofx + {:db {:communities/all-addresses-to-reveal {community-id #{"0xA" "0xB" "0xC"}} + :communities/all-airdrop-addresses {community-id "0xB"} + :wallet {:accounts {"0xB" {:address "0xB" :position 0} + "0xA" {:address "0xA" :position 1} + "0xC" {:address "0xC" :position 2}}}}} + addresses-to-reveal ["0xA" "0xC"]] + (is (match? + {:db {:communities/all-addresses-to-reveal + {community-id (set addresses-to-reveal)} + :communities/all-airdrop-addresses + {community-id "0xA"}} + :fx [[:dispatch + [:communities/check-permissions-to-join-community + community-id (set addresses-to-reveal) :based-on-client-selection]] + [:dispatch [:hide-bottom-sheet]]]} + (sut/set-permissioned-accounts cofx [community-id addresses-to-reveal])))))) + +(deftest set-share-all-addresses-test + (testing "sets flag from false -> true will mark all addresses to be revealed" + (let [cofx {:db + {:wallet + {:accounts {"0xB" {:address "0xB" :position 0} + "0xA" {:address "0xA" :position 1} + "0xC" {:address "0xC" :position 2}}} + :communities/all-addresses-to-reveal {community-id #{"0xA"}} + :communities/selected-share-all-addresses {community-id false}}} + addresses-to-reveal #{"0xA" "0xB" "0xC"}] + (is (match? + {:db {:communities/selected-share-all-addresses {community-id true} + :communities/all-addresses-to-reveal + {community-id addresses-to-reveal}} + :fx [[:dispatch + [:communities/check-permissions-to-join-during-selection + community-id addresses-to-reveal]]]} + (sut/set-share-all-addresses cofx [community-id true]))))) + + (testing "sets flag from true -> false will not change addresses to be revealed" + (let [cofx {:db {:communities/all-addresses-to-reveal {community-id #{"0xA"}}}}] + (is (match? + {:db {:communities/selected-share-all-addresses {community-id false} + :communities/all-addresses-to-reveal {community-id #{"0xA"}}} + :fx [nil]} + (sut/set-share-all-addresses cofx [community-id false])))))) + +(deftest check-permissions-to-join-for-selection-test + (testing "when no addresses are passed don't check permissions" + (let [addresses [] + cofx {:db {:foo :bar + :communities/permissions-checks-for-selection + {"0xC" :another-check + community-id :some-check}}}] + (is (match? + {:db {:foo :bar + :communities/permissions-checks-for-selection {"0xC" :another-check}}} + (sut/check-permissions-to-join-for-selection cofx [community-id addresses]))))) + + (testing "when there are addresses to check permissions and not currently checking" + (let [addresses ["0xA"] + cofx {:db {:communities/permissions-checks-for-selection + {"other-comm-id" {} + community-id {:checking? false}}}}] + (is (match? + {:db {:communities/permissions-checks-for-selection + {"other-comm-id" {} + community-id {:checking? true}}} + :fx [[:json-rpc/call + [{:method :wakuext_checkPermissionsToJoinCommunity + :params [{:communityId community-id :addresses addresses}] + :on-success [:communities/check-permissions-to-join-during-selection-success + community-id] + :on-error [:communities/check-permissions-to-join-during-selection-failure + community-id addresses]}]]]} + (sut/check-permissions-to-join-for-selection cofx [community-id addresses]))))) + + (testing "when there are addresses to check permissions, but currently checking, then skip the check" + (let [addresses ["0xA"] + cofx {:db {:communities/permissions-checks-for-selection + {"other-comm-id" {} + community-id {:checking? true}}}}] + (is (nil? (sut/check-permissions-to-join-for-selection cofx [community-id addresses])))))) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs new file mode 100644 index 0000000000..5b5c0b177c --- /dev/null +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/style.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.communities.actions.addresses-for-permissions.style) + +(def drawer-body + {:padding-top 4 + :padding-horizontal 20 + :padding-bottom 12}) diff --git a/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs b/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs index cdf44540ed..042d04a346 100644 --- a/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs +++ b/src/status_im/contexts/communities/actions/addresses_for_permissions/view.cljs @@ -1,11 +1,16 @@ (ns status-im.contexts.communities.actions.addresses-for-permissions.view - (:require [quo.core :as quo] - [react-native.gesture :as gesture] - [status-im.common.not-implemented :as not-implemented] - [status-im.constants :as constants] - [utils.i18n :as i18n] - [utils.money :as money] - [utils.re-frame :as rf])) + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [status-im.common.not-implemented :as not-implemented] + [status-im.common.password-authentication.view :as password-authentication] + [status-im.constants :as constants] + [status-im.contexts.communities.actions.addresses-for-permissions.style :as style] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) (defn- role-keyword [role] @@ -36,96 +41,316 @@ :token (:symbol balance) :token-img-src (images-by-symbol sym))))) +(defn- cancel-join-request-drawer + [community-id] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id]) + request-id (rf/sub [:communities/my-pending-request-to-join community-id])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/cancel-request?) + :community-name name + :community-logo logo + :customization-color color}] + [rn/view {:style style/drawer-body} + [quo/text (i18n/label :t/pending-join-request-farewell)]] + [quo/bottom-actions + {:actions :two-actions + + :button-one-label (i18n/label :t/confirm-and-cancel) + :button-one-props {:customization-color color + :on-press + (fn [] + (rf/dispatch [:communities/addresses-for-permissions-cancel-request + request-id]))} + + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) + +(defn- confirm-discard-drawer + [community-id] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/discard-changes?) + :community-name name + :community-logo logo + :customization-color color}] + [rn/view {:style style/drawer-body} + [quo/text (i18n/label :t/all-changes-will-be-discarded)]] + [quo/bottom-actions + {:actions :two-actions + + :button-one-label (i18n/label :t/discard) + :button-one-props {:customization-color color + :on-press + (fn [] + (rf/dispatch [:dismiss-modal :addresses-for-permissions]) + (rf/dispatch [:hide-bottom-sheet]))} + + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) + +(defn- leave-community-drawer + [community-id outro-message] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id])] + [:<> + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/leave-community?) + :community-name name + :community-logo logo + :customization-color color}] + [rn/view {:style style/drawer-body} + [quo/text + (if (string/blank? outro-message) + (i18n/label :t/leave-community-farewell) + outro-message)]] + [quo/bottom-actions + {:actions :two-actions + + :button-one-label (i18n/label :t/confirm-and-leave) + :button-one-props {:customization-color color + :on-press + #(rf/dispatch [:communities/addresses-for-permissions-leave community-id])} + + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press #(rf/dispatch [:hide-bottom-sheet])}}]])) + (defn- account-item - [{:keys [color address name emoji]} _ _ - {:keys [selected-addresses community-id share-all-addresses? community-color]}] + [{:keys [customization-color address name emoji]} _ _ + [addresses-to-reveal set-addresses-to-reveal community-id community-color share-all-addresses?]] (let [balances (rf/sub [:communities/permissioned-balances-by-address community-id address]) - images-by-symbol (rf/sub [:communities/token-images-by-symbol community-id])] + images-by-symbol (rf/sub [:communities/token-images-by-symbol community-id]) + checked? (contains? addresses-to-reveal address) + toggle-address (fn [] + (let [new-addresses (if checked? + (disj addresses-to-reveal address) + (conj addresses-to-reveal address))] + (set-addresses-to-reveal new-addresses)))] [quo/account-permissions {:account {:name name :address address :emoji emoji - :customization-color color} - :token-details (balances->components-props balances images-by-symbol) - :checked? (contains? selected-addresses address) + :customization-color customization-color} + :token-details (when-not share-all-addresses? + (balances->components-props balances images-by-symbol)) + :checked? checked? :disabled? share-all-addresses? - :on-change #(rf/dispatch [:communities/toggle-selected-permission-address - address community-id]) + :on-change toggle-address :container-style {:margin-bottom 8} :customization-color community-color}])) +(defn- page-top + [{:keys [community-id identical-choices? can-edit-addresses?]}] + (let [{:keys [name logo color]} (rf/sub [:communities/for-context-tag community-id]) + confirm-discard-changes (rn/use-callback + (fn [] + (if identical-choices? + (rf/dispatch [:dismiss-modal :addresses-for-permissions]) + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [confirm-discard-drawer + community-id])}]))) + [identical-choices?])] + [:<> + (when can-edit-addresses? + [quo/page-nav + {:type :no-title + :icon-name :i/arrow-left + :on-press confirm-discard-changes}]) + + (if can-edit-addresses? + [quo/page-top + {:title (i18n/label :t/addresses-for-permissions) + :title-accessibility-label :title-label + :description :context-tag + :context-tag {:type :community + :community-logo logo + :community-name name}}] + [quo/drawer-top + {:type :context-tag + :title (i18n/label :t/addresses-for-permissions) + :context-tag-type :community + :community-name name + :button-icon :i/info + :button-type :grey + :on-button-press not-implemented/alert + :community-logo logo + :customization-color color}])])) + (defn view [] - (let [{id :community-id} (rf/sub [:get-screen-params]) - toggle-share-all-addresses #(rf/dispatch [:communities/toggle-share-all-addresses id]) - update-previous-addresses (fn [] - (rf/dispatch [:communities/update-previous-permission-addresses id]) - (rf/dispatch [:hide-bottom-sheet])) - reset-selected-addresses (fn [] - (rf/dispatch [:communities/reset-selected-permission-addresses id]) - (rf/dispatch [:hide-bottom-sheet]))] - (rf/dispatch [:communities/get-permissioned-balances id]) - (fn [] - (let [{:keys [name color images]} (rf/sub [:communities/community id]) - {:keys [checking? - highest-permission-role]} (rf/sub [:community/token-gated-overview id]) - accounts (rf/sub [:wallet/accounts-without-watched-accounts]) - selected-addresses (rf/sub [:communities/selected-permission-addresses id]) - share-all-addresses? (rf/sub [:communities/share-all-addresses? id]) - unsaved-address-changes? (rf/sub [:communities/unsaved-address-changes? id])] - [:<> - [quo/drawer-top - {:type :context-tag - :title (i18n/label :t/addresses-for-permissions) - :context-tag-type :community - :community-name name - :button-icon :i/info - :button-type :grey - :on-button-press not-implemented/alert - :community-logo (get-in images [:thumbnail :uri]) - :customization-color color}] + (let [{id :community-id} (rf/sub [:get-screen-params]) + {:keys [color joined] outro-message :outroMessage} (rf/sub [:communities/community id]) - [gesture/flat-list - {:render-fn account-item - :render-data {:selected-addresses selected-addresses - :community-id id - :share-all-addresses? share-all-addresses? - :community-color color} - :header [quo/category - {:list-type :settings - :data [{:title - (i18n/label - :t/share-all-current-and-future-addresses) - :action :selector - :action-props - {:on-change toggle-share-all-addresses - :customization-color color - :checked? share-all-addresses?}}] - :container-style {:padding-bottom 16 :padding-horizontal 0}}] - :content-container-style {:padding-horizontal 20} - :key-fn :address - :data accounts}] + highest-role (rf/sub [:communities/highest-role-for-selection id]) + [unmodified-role _] (rn/use-state highest-role) - [quo/bottom-actions - {:actions :two-actions - :button-one-label (i18n/label :t/confirm-changes) - :button-one-props {:customization-color color - :disabled? (or checking? - (empty? selected-addresses) - (not highest-permission-role) - (not unsaved-address-changes?)) - :on-press update-previous-addresses} - :button-two-label (i18n/label :t/cancel) - :button-two-props {:type :grey - :on-press reset-selected-addresses} - :description (if (or (empty? selected-addresses) - (not highest-permission-role)) - :top-error - :top) - :role (when-not checking? (role-keyword highest-permission-role)) - :error-message (cond - (empty? selected-addresses) - (i18n/label :t/no-addresses-selected) + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + pending? (boolean (rf/sub [:communities/my-pending-request-to-join id])) - (not highest-permission-role) - (i18n/label :t/addresses-dont-contain-tokens-needed))}]])))) + wallet-accounts (rf/sub [:wallet/accounts-without-watched-accounts]) + unmodified-addresses-to-reveal (rf/sub [:communities/addresses-to-reveal id]) + [addresses-to-reveal set-addresses-to-reveal] (rn/use-state unmodified-addresses-to-reveal) + + unmodified-flag-share-all-addresses (rf/sub [:communities/share-all-addresses? id]) + [flag-share-all-addresses + set-flag-share-all-addresses] (rn/use-state unmodified-flag-share-all-addresses) + + identical-choices? (and (= unmodified-addresses-to-reveal addresses-to-reveal) + (= unmodified-flag-share-all-addresses flag-share-all-addresses)) + + toggle-flag-share-all-addresses + (fn [] + (let [new-value (not flag-share-all-addresses)] + (set-flag-share-all-addresses new-value) + (when new-value + (set-addresses-to-reveal (set (map :address wallet-accounts)))))) + + cancel-join-request + (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [cancel-join-request-drawer id])}]))) + + leave-community + (rn/use-callback + (fn [] + (rf/dispatch [:show-bottom-sheet + {:content (fn [] [leave-community-drawer id outro-message])}])) + [outro-message]) + + cancel-selection + (fn [] + (rf/dispatch [:communities/check-permissions-to-join-community + id + unmodified-addresses-to-reveal + :based-on-client-selection]) + (rf/dispatch [:hide-bottom-sheet])) + + confirm-changes + (fn [] + (if can-edit-addresses? + (rf/dispatch + [:password-authentication/show + {:content (fn [] [password-authentication/view])} + {:label (i18n/label :t/enter-password) + :on-press (fn [password] + (rf/dispatch + [:communities/edit-shared-addresses + {:community-id id + :password password + :addresses addresses-to-reveal + :on-success (fn [] + (rf/dispatch [:dismiss-modal :addresses-for-permissions]) + (rf/dispatch [:hide-bottom-sheet]))}]))}]) + (do + (rf/dispatch [:communities/set-share-all-addresses id flag-share-all-addresses]) + (rf/dispatch [:communities/set-addresses-to-reveal id addresses-to-reveal]))))] + + (rn/use-mount + (fn [] + (when-not flag-share-all-addresses + (rf/dispatch [:communities/get-permissioned-balances id])))) + + (rn/use-effect + (fn [] + (rf/dispatch [:communities/check-permissions-to-join-during-selection id addresses-to-reveal])) + [id addresses-to-reveal]) + + [:<> + [page-top + {:community-id id + :identical-choices? identical-choices? + :can-edit-addresses? can-edit-addresses?}] + + [gesture/flat-list + {:render-fn account-item + :render-data [addresses-to-reveal + set-addresses-to-reveal + id + color + flag-share-all-addresses] + :header [quo/category + {:list-type :settings + :data [{:title + (i18n/label + :t/share-all-current-and-future-addresses) + :action :selector + :action-props + {:on-change toggle-flag-share-all-addresses + :customization-color color + :checked? flag-share-all-addresses}}] + :container-style {:padding-bottom 16 :padding-horizontal 0}}] + :content-container-style {:padding-horizontal 20} + :key-fn :address + :data wallet-accounts}] + + [quo/bottom-actions + (cond-> {:role (role-keyword highest-role) + :description (if highest-role + :top + :top-error) + :button-one-props {:customization-color color + :on-press confirm-changes + :disabled? (or (empty? addresses-to-reveal) + (not highest-role) + identical-choices?)}} + can-edit-addresses? + (-> + (assoc :actions :one-action + :button-one-label (cond + (and pending? (not highest-role)) + (i18n/label :t/cancel-request) + + (and joined (not highest-role)) + (i18n/label :t/leave-community) + + :else + (i18n/label :t/confirm-changes)) + :description-top-text (cond + (and pending? highest-role) + (i18n/label :t/eligible-to-join-as) + + (and joined (= highest-role unmodified-role)) + (i18n/label :t/you-are-a) + + (and joined (not= highest-role unmodified-role)) + (i18n/label :t/you-will-be-a)) + :error-message (cond + (and pending? (not highest-role)) + (i18n/label :t/community-join-requirements-not-met) + + (and joined (not highest-role)) + (i18n/label :t/membership-requirements-not-met))) + (update :button-one-props + merge + (cond (and pending? (not highest-role)) + {:type :danger + :disabled? false + :on-press cancel-join-request} + + (and joined (not highest-role)) + {:type :danger + :disabled? false + :on-press leave-community}))) + + (not can-edit-addresses?) + (assoc :actions :two-actions + :button-one-label (i18n/label :t/confirm-changes) + :button-two-label (i18n/label :t/cancel) + :button-two-props {:type :grey + :on-press cancel-selection} + :error-message (cond + (empty? addresses-to-reveal) + (i18n/label :t/no-addresses-selected) + + (not highest-role) + (i18n/label :t/addresses-dont-contain-tokens-needed))))]])) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs new file mode 100644 index 0000000000..bd366da290 --- /dev/null +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/events.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.communities.actions.airdrop-addresses.events + (:require + [utils.re-frame :as rf])) + +(defn set-airdrop-address + [{:keys [db]} [community-id airdrop-address]] + {:db (assoc-in db [:communities/all-airdrop-addresses community-id] airdrop-address)}) + +(rf/reg-event-fx :communities/set-airdrop-address set-airdrop-address) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs new file mode 100644 index 0000000000..7ec97de5a3 --- /dev/null +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/events_test.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.communities.actions.airdrop-addresses.events-test + (:require + [cljs.test :refer [deftest is testing]] + [status-im.contexts.communities.actions.airdrop-addresses.events :as sut])) + +(def community-id "community-id") + +(deftest set-airdrop-address-test + (testing "updates all reveal? flags" + (let [new-airdrop-address "0xA" + cofx {:db {:communities/all-airdrop-addresses + {"another-community" "0xF" + community-id "0xB"}}}] + (is (match? + (assoc-in cofx + [:db :communities/all-airdrop-addresses community-id] + new-airdrop-address) + (sut/set-airdrop-address cofx [community-id new-airdrop-address])))))) diff --git a/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs b/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs index 6bfaa95e8b..4973f1b075 100644 --- a/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs +++ b/src/status_im/contexts/communities/actions/airdrop_addresses/view.cljs @@ -1,41 +1,81 @@ (ns status-im.contexts.communities.actions.airdrop-addresses.view (:require [quo.core :as quo] + [react-native.core :as rn] [react-native.gesture :as gesture] [status-im.common.not-implemented :as not-implemented] + [status-im.common.password-authentication.view :as password-authentication] [status-im.contexts.communities.actions.airdrop-addresses.style :as style] [utils.i18n :as i18n] [utils.re-frame :as rf])) -(defn- render-item - [item _ _ [airdrop-address community-id]] - [quo/account-item - {:account-props item - :state (when (= airdrop-address (:address item)) :selected) - :on-press (fn [] - (rf/dispatch [:communities/set-airdrop-address (:address item) community-id]) - (rf/dispatch [:hide-bottom-sheet])) - :emoji (:emoji item)}]) +(defn- account-item + [{:keys [address emoji] :as account} + _ _ + [community-id airdrop-address can-edit-addresses?]] + (let [airdrop-address? (= address airdrop-address) + on-press + (when-not airdrop-address? + (fn [] + (if can-edit-addresses? + (rf/dispatch + [:password-authentication/show + {:content (fn [] [password-authentication/view])} + {:label (i18n/label :t/enter-password) + :on-press (fn [password] + (rf/dispatch + [:communities/edit-shared-addresses + {:community-id community-id + :password password + :airdrop-address address + :on-success (fn [] + (rf/dispatch [:dismiss-modal :address-for-airdrop]) + (rf/dispatch [:hide-bottom-sheet]))}]))}]) + (do + (rf/dispatch [:communities/set-airdrop-address community-id address]) + (rf/dispatch [:hide-bottom-sheet])))))] + [quo/account-item + {:account-props account + :emoji emoji + :state (if airdrop-address? :selected :default) + :on-press on-press}])) (defn view [] - (let [{id :community-id} (rf/sub [:get-screen-params]) - {:keys [name images color]} (rf/sub [:communities/community id]) - selected-accounts (rf/sub [:communities/selected-permission-accounts id]) - airdrop-address (rf/sub [:communities/airdrop-address id])] + (let [{id :community-id} (rf/sub [:get-screen-params]) + {:keys [name logo color]} (rf/sub [:communities/for-context-tag id]) + accounts (rf/sub [:communities/accounts-to-reveal id]) + airdrop-address (rf/sub [:communities/airdrop-address id]) + can-edit-addresses? (rf/sub [:communities/can-edit-shared-addresses? id]) + go-back (rn/use-callback #(rf/dispatch [:dismiss-modal :address-for-airdrop]))] [:<> - [quo/drawer-top - {:type :context-tag - :context-tag-type :community - :title (i18n/label :t/airdrop-addresses) - :community-name name - :button-icon :i/info - :on-button-press not-implemented/alert - :community-logo (get-in images [:thumbnail :uri]) - :customization-color color}] + (when can-edit-addresses? + [quo/page-nav + {:type :no-title + :icon-name :i/arrow-left + :on-press go-back}]) + + (if can-edit-addresses? + [quo/page-top + {:title (i18n/label :t/airdrop-addresses) + :title-accessibility-label :title-label + :description :context-tag + :context-tag {:type :community + :community-logo logo + :community-name name}}] + [quo/drawer-top + {:type :context-tag + :context-tag-type :community + :title (i18n/label :t/airdrop-addresses) + :community-name name + :button-icon :i/info + :on-button-press not-implemented/alert + :community-logo logo + :customization-color color}]) + [gesture/flat-list - {:data selected-accounts - :render-fn render-item - :render-data [airdrop-address id] + {:data accounts + :render-fn account-item + :render-data [id airdrop-address can-edit-addresses?] :content-container-style style/account-list-container :key-fn :address}]])) diff --git a/src/status_im/contexts/communities/actions/community_options/view.cljs b/src/status_im/contexts/communities/actions/community_options/view.cljs index ab94b38040..959be3c087 100644 --- a/src/status_im/contexts/communities/actions/community_options/view.cljs +++ b/src/status_im/contexts/communities/actions/community_options/view.cljs @@ -7,6 +7,7 @@ [status-im.contexts.communities.actions.leave.view :as leave-menu] [status-im.contexts.communities.actions.see-rules.view :as see-rules] [status-im.contexts.communities.actions.token-gating.view :as token-gating] + [status-im.feature-flags :as ff] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -41,6 +42,15 @@ {:content (fn [] [token-gating/token-requirements id])}]) :label (i18n/label :t/view-token-gating)}) +(defn edit-shared-addresses + [id] + {:icon :i/wallet + :right-icon :i/chevron-right + :accessibility-label :edit-shared-addresses + :on-press (fn [] + (rf/dispatch [:open-modal :community-account-selection {:community-id id}])) + :label (i18n/label :t/edit-shared-addresses)}) + (defn mark-as-read [id] {:icon :i/up-to-date @@ -119,28 +129,33 @@ request-id])}])}) (defn not-joined-options - [id token-gated?] + [id token-gated? pending?] [[(when-not token-gated? (view-members id)) (when-not token-gated? (view-rules id)) (invite-contacts id) (when token-gated? (view-token-gating id)) + (when (and pending? (ff/enabled? ::ff/community.edit-account-selection)) + (edit-shared-addresses id)) (show-qr id) (share-community id)]]) (defn join-request-sent-options [id token-gated? request-id] - [(conj (first (not-joined-options id token-gated?)) + [(conj (first (not-joined-options id token-gated? request-id)) (assoc (cancel-request-to-join id request-id) :add-divider? true))]) (defn banned-options [id token-gated?] - (not-joined-options id token-gated?)) + (let [pending? false] + (not-joined-options id token-gated? pending?))) (defn joined-options [id token-gated? muted? muted-till] [[(view-members id) (view-rules id) (when token-gated? (view-token-gating id)) + (when (ff/enabled? ::ff/community.edit-account-selection) + (edit-shared-addresses id)) (mark-as-read id) (mute-community id muted? muted-till) (community-notification-settings id) @@ -171,7 +186,7 @@ joined (joined-options id token-permissions muted muted-till) request-id (join-request-sent-options id token-permissions request-id) banList (banned-options id token-permissions) - :else (not-joined-options id token-permissions)))) + :else (not-joined-options id token-permissions request-id)))) (defn community-options-bottom-sheet [id] diff --git a/src/status_im/contexts/communities/actions/leave/events.cljs b/src/status_im/contexts/communities/actions/leave/events.cljs index 9bfbf249c7..eb19fbc354 100644 --- a/src/status_im/contexts/communities/actions/leave/events.cljs +++ b/src/status_im/contexts/communities/actions/leave/events.cljs @@ -29,10 +29,11 @@ {:type :positive :text (i18n/label :t/left-community {:community community-name})}]] [:dispatch [:activity-center.notifications/fetch-unread-count]] + [:dispatch [:hide-bottom-sheet]] [:dispatch [:navigate-back]]]}))) (rf/reg-event-fx :communities/leave - (fn [{:keys [db]} [community-id]] + (fn [{:keys [db]} [community-id on-success]] (let [community-chat-ids (map #(str community-id %) (keys (get-in db [:communities community-id :chats])))] {:effects/push-notifications-clear-message-notifications community-chat-ids @@ -41,5 +42,8 @@ [{:method "wakuext_leaveCommunity" :params [community-id] :js-response true - :on-success #(rf/dispatch [:communities/left %]) + :on-success (fn [^js response] + (when (fn? on-success) + (on-success)) + (rf/dispatch [:communities/left response])) :on-error #(log/error "failed to leave community" community-id %)}]}))) diff --git a/src/status_im/contexts/communities/events.cljs b/src/status_im/contexts/communities/events.cljs index 4c8f564145..18076f3eff 100644 --- a/src/status_im/contexts/communities/events.cljs +++ b/src/status_im/contexts/communities/events.cljs @@ -9,10 +9,11 @@ [schema.core :as schema] [status-im.constants :as constants] [status-im.contexts.chat.messenger.messages.link-preview.events :as link-preview.events] + status-im.contexts.communities.actions.accounts-selection.events status-im.contexts.communities.actions.addresses-for-permissions.events + status-im.contexts.communities.actions.airdrop-addresses.events status-im.contexts.communities.actions.community-options.events status-im.contexts.communities.actions.leave.events - [status-im.contexts.communities.utils :as utils] [status-im.navigation.events :as navigation] [taoensso.timbre :as log] [utils.i18n :as i18n] @@ -29,13 +30,10 @@ {:db (assoc-in db [:communities id] (assoc community :last-opened-at (max last-opened-at previous-last-opened-at))) - :fx [[:dispatch [:communities/initialize-permission-addresses id]] - (when (not joined) + :fx [(when (not joined) [:dispatch [:chat.ui/spectate-community id]]) (when (nil? token-permissions-check) - [:dispatch [:communities/check-permissions-to-join-community id]]) - (when joined - [:dispatch [:communities/get-revealed-accounts id]])]})))) + [:dispatch [:communities/check-permissions-to-join-community id]])]})))) (rf/reg-event-fx :communities/handle-community handle-community) @@ -157,97 +155,6 @@ :on-success #(rf/dispatch [:communities/fetched-collapsed-categories-success %]) :on-error #(log/error "failed to fetch collapsed community categories" %)}]})) -(defn initialize-permission-addresses - [{:keys [db]} [community-id]] - (when community-id - (let [accounts (utils/sorted-non-watch-only-accounts db) - addresses (set (map :address accounts))] - {:db (update-in db - [:communities community-id] - assoc - :previous-share-all-addresses? true - :share-all-addresses? true - :previous-permission-addresses addresses - :selected-permission-addresses addresses - :airdrop-address (:address (first accounts)))}))) - -(rf/reg-event-fx :communities/initialize-permission-addresses - initialize-permission-addresses) - -(defn update-previous-permission-addresses - [{:keys [db]} [community-id]] - (when community-id - (let [accounts (utils/sorted-non-watch-only-accounts db) - selected-permission-addresses (get-in db - [:communities community-id - :selected-permission-addresses]) - selected-accounts (filter #(contains? selected-permission-addresses (:address %)) - accounts) - current-airdrop-address (get-in db [:communities community-id :airdrop-address]) - share-all-addresses? (get-in db [:communities community-id :share-all-addresses?])] - {:db (update-in db - [:communities community-id] - assoc - :previous-share-all-addresses? share-all-addresses? - :previous-permission-addresses selected-permission-addresses - :airdrop-address (if (contains? selected-permission-addresses - current-airdrop-address) - current-airdrop-address - (:address (first selected-accounts))))}))) - -(rf/reg-event-fx :communities/update-previous-permission-addresses - update-previous-permission-addresses) - -(defn toggle-selected-permission-address - [{:keys [db]} [address community-id]] - (let [selected-permission-addresses - (get-in db [:communities community-id :selected-permission-addresses]) - updated-selected-permission-addresses - (if (contains? selected-permission-addresses address) - (disj selected-permission-addresses address) - (conj selected-permission-addresses address))] - {:db (assoc-in db - [:communities community-id :selected-permission-addresses] - updated-selected-permission-addresses) - :fx [(when community-id - [:dispatch - [:communities/check-permissions-to-join-community community-id - updated-selected-permission-addresses :based-on-client-selection]])]})) - -(rf/reg-event-fx :communities/toggle-selected-permission-address - toggle-selected-permission-address) - -(defn toggle-share-all-addresses - [{:keys [db]} [community-id]] - (let [share-all-addresses? (get-in db [:communities community-id :share-all-addresses?]) - next-share-all-addresses? (not share-all-addresses?) - accounts (utils/sorted-non-watch-only-accounts db) - addresses (set (map :address accounts))] - {:db (update-in db - [:communities community-id] - assoc - :share-all-addresses? next-share-all-addresses? - :selected-permission-addresses addresses) - :fx [(when (and community-id next-share-all-addresses?) - [:dispatch - [:communities/check-permissions-to-join-community community-id - addresses :based-on-client-selection]])]})) - -(rf/reg-event-fx :communities/toggle-share-all-addresses - toggle-share-all-addresses) - -(rf/reg-event-fx :communities/reset-selected-permission-addresses - (fn [{:keys [db]} [community-id]] - (when community-id - {:db (update-in db - [:communities community-id] - assoc - :selected-permission-addresses - (get-in db [:communities community-id :previous-permission-addresses]) - :share-all-addresses? - (get-in db [:communities community-id :previous-share-all-addresses?])) - :fx [[:dispatch [:communities/check-permissions-to-join-community community-id]]]}))) - (rf/reg-event-fx :communities/get-community-channel-share-data (fn [_ [chat-id on-success]] (let [{:keys [community-id channel-id]} (data-store.chats/decode-chat-id chat-id)] @@ -286,10 +193,6 @@ :url %}])] {:fx [[:dispatch [:communities/get-community-channel-share-data chat-id on-success]]]}))) -(rf/reg-event-fx :communities/set-airdrop-address - (fn [{:keys [db]} [address community-id]] - {:db (assoc-in db [:communities community-id :airdrop-address] address)})) - (defn community-fetched [{:keys [db]} [community-id community]] (when community @@ -425,16 +328,17 @@ [:dispatch [:chat/navigate-to-chat chat-id]])]}))) (defn get-revealed-accounts - [{:keys [db]} [community-id]] + [{:keys [db]} [community-id on-success]] (let [{:keys [joined fetching-revealed-accounts] - :as community} (get-in db [:communities community-id])] - (when (and community joined (not fetching-revealed-accounts)) + :as community} (get-in db [:communities community-id]) + pending? (get-in db [:communities/my-pending-requests-to-join community-id])] + (when (and community (or pending? joined) (not fetching-revealed-accounts)) {:db (assoc-in db [:communities community-id :fetching-revealed-accounts] true) :json-rpc/call [{:method "wakuext_getRevealedAccounts" :params [community-id (get-in db [:profile/profile :public-key])] :js-response true - :on-success [:communities/get-revealed-accounts-success community-id] + :on-success [:communities/get-revealed-accounts-success community-id on-success] :on-error (fn [err] (log/error {:message "failed to fetch revealed accounts" :community-id community-id @@ -448,19 +352,22 @@ [:catn [:cofx :schema.re-frame/cofx] [:args - [:schema [:catn [:community-id [:? :string]]]]]] + [:schema + [:catn + [:community-id [:? :string]] + [:on-success [:? :schema.re-frame/event]]]]]] [:maybe [:map [:db map?] [:json-rpc/call :schema.common/rpc-call]]]]) (rf/reg-event-fx :communities/get-revealed-accounts-success - (fn [{:keys [db]} [community-id revealed-accounts-js]] + (fn [{:keys [db]} [community-id on-success revealed-accounts-js]] (when-let [community (get-in db [:communities community-id])] (let [revealed-accounts (reduce (fn [acc {:keys [address] :as revealed-account}] - (assoc acc address (dissoc revealed-account :address))) + (assoc acc address revealed-account)) {} (data-store.communities/<-revealed-accounts-rpc revealed-accounts-js)) @@ -468,7 +375,9 @@ (-> community (assoc :revealed-accounts revealed-accounts) (dissoc :fetching-revealed-accounts))] - {:db (assoc-in db [:communities community-id] community-with-revealed-accounts)})))) + {:db (assoc-in db [:communities community-id] community-with-revealed-accounts) + :fx [(when (vector? on-success) + [:dispatch (conj on-success revealed-accounts)])]})))) (rf/reg-event-fx :communities/get-revealed-accounts-failed (fn [{:keys [db]} [community-id]] diff --git a/src/status_im/contexts/communities/events_test.cljs b/src/status_im/contexts/communities/events_test.cljs index 62726cf245..cb1f171258 100644 --- a/src/status_im/contexts/communities/events_test.cljs +++ b/src/status_im/contexts/communities/events_test.cljs @@ -7,99 +7,6 @@ (def community-id "community-id") -(deftest initialize-permission-addresses-test - (let [initial-db {:db {:wallet {:accounts {"0x1" {:address "0x1" - :position 0} - "0x2" {:address "0x2" - :position 1}}}}} - expected-db {:db (update-in (:db initial-db) - [:communities community-id] - assoc - :share-all-addresses? true - :previous-share-all-addresses? true - :previous-permission-addresses #{"0x1" "0x2"} - :selected-permission-addresses #{"0x1" "0x2"} - :airdrop-address "0x1")}] - (is (match? expected-db (events/initialize-permission-addresses initial-db [community-id]))))) - -(deftest toggle-selected-permission-address-test - (let [initial-db {:db {:communities {community-id {:selected-permission-addresses #{"0x1" "0x2"}}}}}] - (is (match? {:db {:communities {community-id {:selected-permission-addresses #{"0x1"}}}}} - (events/toggle-selected-permission-address initial-db ["0x2" community-id]))) - (is (match? {:db {:communities {community-id {:selected-permission-addresses #{"0x1" "0x2" "0x3"}}}}} - (events/toggle-selected-permission-address initial-db ["0x3" community-id]))))) - -(deftest update-previous-permission-addresses-test - (let [wallet {:accounts {"0x1" {:address "0x1" - :position 0} - "0x2" {:address "0x2" - :position 1} - "0x3" {:address "0x3" - :position 2}}}] - (let [initial-db {:db {:wallet wallet - :communities {community-id {:previous-permission-addresses #{"0x1" "0x2"} - :selected-permission-addresses #{"0x1" "0x2" - "0x3"} - :share-all-addresses? true - :previous-share-all-addresses? false - :airdrop-address "0x1"}}}} - expected-db {:db {:wallet wallet - :communities {community-id {:previous-permission-addresses #{"0x1" "0x2" - "0x3"} - :selected-permission-addresses #{"0x1" "0x2" - "0x3"} - :share-all-addresses? true - :previous-share-all-addresses? true - :airdrop-address "0x1"}}}}] - (is - (match? expected-db - (events/update-previous-permission-addresses initial-db [community-id])))) - (let [initial-db {:db {:wallet wallet - :communities {community-id {:previous-permission-addresses #{"0x1" "0x2"} - :selected-permission-addresses #{"0x2" "0x3"} - :airdrop-address "0x1"}}}} - expected-db {:db {:wallet wallet - :communities {community-id {:previous-permission-addresses #{"0x2" "0x3"} - :selected-permission-addresses #{"0x2" "0x3"} - :airdrop-address "0x2"}}}}] - (is - (match? expected-db - (events/update-previous-permission-addresses initial-db [community-id])))))) - -(deftest toggle-share-all-addresses-test - (let [initial-db {:db {:wallet {:accounts {"0x1" {:address "0x1" - :position 0} - "0x2" {:address "0x2" - :position 1}}} - :communities {community-id {:share-all-addresses? true - :previous-share-all-addresses? true - :previous-permission-addresses #{"0x1" "0x2"} - :selected-permission-addresses #{"0x1" "0x2"} - :airdrop-address "0x1"}}}} - expected-db (update-in initial-db - [:db :communities community-id] - assoc - :previous-share-all-addresses? true - :share-all-addresses? - false)] - (is (match? expected-db (events/toggle-share-all-addresses initial-db [community-id])))) - (let [initial-db {:db {:wallet {:accounts {"0x1" {:address "0x1" - :position 0} - "0x2" {:address "0x2" - :position 1}}} - :communities {community-id {:share-all-addresses? false - :previous-share-all-addresses? false - :previous-permission-addresses #{"0x1"} - :selected-permission-addresses #{"0x1"} - :airdrop-address "0x1"}}}} - expected-db (update-in initial-db - [:db :communities community-id] - assoc - :selected-permission-addresses #{"0x1" "0x2"} - :previous-share-all-addresses? false - :share-all-addresses? true)] - (is (match? expected-db (events/toggle-share-all-addresses initial-db [community-id]))))) - (deftest fetch-community (testing "with community id" (testing "update fetching indicator in db" @@ -243,12 +150,14 @@ (is (match? nil (events/get-revealed-accounts {:db {:communities {community-id community}}} [community-id])))) + (testing "given a already :fetching-revealed-accounts community" (is (match? nil (events/get-revealed-accounts {:db {:communities {community-id (assoc community :fetching-revealed-accounts true)}}} [community-id])))) + (testing "given joined community" (let [community (assoc community :joined true) db {:communities {community-id community} @@ -258,7 +167,26 @@ (:db effects))) (is (match? {:method "wakuext_getRevealedAccounts" :params [community-id "profile-public-key"]} - (-> effects :json-rpc/call first (select-keys [:method :params])))))))) + (-> effects :json-rpc/call first (select-keys [:method :params])))))) + + (testing "given there is a pending request" + (let [pub-key "profile-public-key" + community (assoc community :joined false) + db {:communities {community-id community} + :profile/profile {:public-key pub-key} + :communities/my-pending-requests-to-join {community-id [:anything]}} + on-success [:some-event-id] + actual (events/get-revealed-accounts {:db db} [community-id on-success]) + expected {:db (assoc-in db + [:communities community-id :fetching-revealed-accounts] + true) + :json-rpc/call [{:method "wakuext_getRevealedAccounts" + :params [community-id pub-key] + :js-response true + :on-success [:communities/get-revealed-accounts-success + community-id on-success] + :on-error fn?}]}] + (is (match? expected actual)))))) (deftest handle-community (let [community {:id community-id :clock 2}] @@ -267,24 +195,22 @@ (is (match? community-id (-> effects :db :communities (get community-id) :id))) (is (match? - [[:dispatch [:communities/initialize-permission-addresses community-id]] - [:dispatch [:chat.ui/spectate-community community-id]] + [[:dispatch [:chat.ui/spectate-community community-id]] [:dispatch [:communities/check-permissions-to-join-community community-id]]] (filter some? (:fx effects)))))) + (testing "given a joined community" (let [community (assoc community :joined true) effects (events/handle-community {} [community])] (is (match? - [[:dispatch [:communities/initialize-permission-addresses community-id]] - [:dispatch [:communities/check-permissions-to-join-community community-id]] - [:dispatch [:communities/get-revealed-accounts community-id]]] + [[:dispatch [:communities/check-permissions-to-join-community community-id]]] (filter some? (:fx effects)))))) + (testing "given a community with token-permissions-check" (let [community (assoc community :token-permissions-check :fake-token-permissions-check) effects (events/handle-community {} [community])] (is (match? - [[:dispatch [:communities/initialize-permission-addresses community-id]] - [:dispatch [:chat.ui/spectate-community community-id]]] + [[:dispatch [:chat.ui/spectate-community community-id]]] (filter some? (:fx effects)))))) (testing "given a community with lower clock" (let [effects (events/handle-community {:db {:communities {community-id {:clock 3}}}} [community])] diff --git a/src/status_im/contexts/communities/overview/events.cljs b/src/status_im/contexts/communities/overview/events.cljs index 24b1d1a5fa..c0af93896a 100644 --- a/src/status_im/contexts/communities/overview/events.cljs +++ b/src/status_im/contexts/communities/overview/events.cljs @@ -4,8 +4,6 @@ [utils.i18n :as i18n] [utils.re-frame :as rf])) - - (rf/reg-event-fx :communities/check-permissions-to-join-community-success (fn [{:keys [db]} [community-id based-on-client-selection? result]] (let [token-permissions-check (cond-> result @@ -118,41 +116,19 @@ :category-id category-id :collapse? collapse?})}]})) -(defn request-to-join-with-signatures-and-addresses - [{:keys [db]} [community-id signatures]] - (let [{:keys [airdrop-address selected-permission-addresses]} (get-in db [:communities community-id])] - {:fx [[:json-rpc/call - [{:method "wakuext_requestToJoinCommunity" - :params [{:communityId community-id - :signatures signatures - :addressesToReveal selected-permission-addresses - :airdropAddress airdrop-address}] - :js-response true - :on-success [:communities/requested-to-join] - :on-error [:communities/requested-to-join-error community-id]}]]]})) - -(rf/reg-event-fx :communities/request-to-join-with-signatures-and-addresses - request-to-join-with-signatures-and-addresses) - -(defn sign-data-with-addresses - [_ [community-id password sign-params]] - {:fx [[:json-rpc/call - [{:method "wakuext_signData" - :params [(map #(assoc % :password password) sign-params)] - :on-success [:communities/request-to-join-with-signatures-and-addresses community-id] - :on-error [:communities/requested-to-join-error community-id]}]]]}) - -(rf/reg-event-fx :communities/sign-data-with-addresses sign-data-with-addresses) - (defn request-to-join-with-addresses [{:keys [db]} [{:keys [community-id password]}]] (let [pub-key (get-in db [:profile/profile :public-key]) - addresses-to-reveal (get-in db [:communities community-id :selected-permission-addresses])] - {:fx [[:json-rpc/call - [{:method "wakuext_generateJoiningCommunityRequestsForSigning" - :params [pub-key community-id addresses-to-reveal] - :on-success [:communities/sign-data-with-addresses community-id password] - :on-error [:communities/requested-to-join-error community-id]}]]]})) + addresses-to-reveal (get-in db [:communities/all-addresses-to-reveal community-id]) + airdrop-address (get-in db [:communities/all-airdrop-addresses community-id])] + {:fx [[:effects.community/request-to-join + {:community-id community-id + :password password + :pub-key pub-key + :addresses-to-reveal addresses-to-reveal + :airdrop-address airdrop-address + :on-success [:communities/requested-to-join] + :on-error [:communities/requested-to-join-error community-id]}]]})) (rf/reg-event-fx :communities/request-to-join-with-addresses request-to-join-with-addresses) diff --git a/src/status_im/contexts/communities/overview/view.cljs b/src/status_im/contexts/communities/overview/view.cljs index 08432c64cd..18048c4b78 100644 --- a/src/status_im/contexts/communities/overview/view.cljs +++ b/src/status_im/contexts/communities/overview/view.cljs @@ -120,7 +120,7 @@ [id color] [quo/button {:on-press (if config/community-accounts-selection-enabled? - #(rf/dispatch [:open-modal :community-account-selection + #(rf/dispatch [:open-modal :community-account-selection-sheet {:community-id id}]) #(rf/dispatch [:open-modal :community-requests-to-join {:id id}])) :accessibility-label :show-request-to-join-screen-button @@ -168,7 +168,7 @@ :padding? true}] [quo/button {:on-press (if config/community-accounts-selection-enabled? - #(rf/dispatch [:open-modal :community-account-selection + #(rf/dispatch [:open-modal :community-account-selection-sheet {:community-id id}]) #(rf/dispatch [:open-modal :community-requests-to-join {:id id}])) :accessibility-label :join-community-button diff --git a/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs b/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs index 6388a948a2..f4e48cae07 100644 --- a/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs +++ b/src/status_im/contexts/preview/quo/drawers/bottom_actions.cljs @@ -63,6 +63,10 @@ {:key :admin} {:key :member}]}) +(def description-top-descriptor + {:key :description-top-text + :type :text}) + (def description-descriptor {:key :description-text :type :text}) @@ -73,28 +77,32 @@ (defn view [] - (let [state (reagent/atom {:actions :two-actions - :description :bottom - :description-text description - :error-message "Error message" - :button-one-label button-one - :button-two-label button-two - :button-one-props {:on-press (button-press 1) - :type :primary} - :button-two-props {:on-press (button-press 2) - :type :grey} - :blur? false - :role :owner - :scroll? false})] + (let [state (reagent/atom {:actions :two-actions + :description :bottom + :description-text description + :description-top-text "Eligible to join as" + :error-message "Error message" + :button-one-label button-one + :button-two-label button-two + :button-one-props {:on-press (button-press 1) + :type :primary} + :button-two-props {:on-press (button-press 2) + :type :grey} + :blur? false + :role :owner + :scroll? false})] (fn [] [preview/preview-container {:state state - :descriptor (merge descriptor - (case (:description @state) - :top role-descriptor - :bottom description-descriptor - :top-error error-descriptor - nil)) + :descriptor (cond-> descriptor + (= (:description @state) :top) + (conj role-descriptor description-top-descriptor) + + (= (:description @state) :bottom) + (conj description-descriptor) + + (= (:description @state) :top-error) + (conj error-descriptor)) :blur? (:blur? @state) :show-blur-background? true :blur-dark-only? true diff --git a/src/status_im/feature_flags.cljs b/src/status_im/feature_flags.cljs index df9fd4398e..f58d279259 100644 --- a/src/status_im/feature_flags.cljs +++ b/src/status_im/feature_flags.cljs @@ -10,10 +10,12 @@ (defonce ^:private feature-flags-config (reagent/atom - {::wallet.edit-default-keypair true - ::wallet.bridge-token (enabled-in-env? :FLAG_BRIDGE_TOKEN_ENABLED) - ::wallet.network-filter (enabled-in-env? :FLAG_NETWORK_FILTER_ENABLED) - ::profile.new-contact-ui (enabled-in-env? :FLAG_NEW_CONTACT_UI_ENABLED)})) + {::wallet.edit-default-keypair true + ::wallet.bridge-token (enabled-in-env? :FLAG_BRIDGE_TOKEN_ENABLED) + ::wallet.remove-account (enabled-in-env? :FLAG_REMOVE_ACCOUNT_ENABLED) + ::wallet.network-filter (enabled-in-env? :FLAG_NETWORK_FILTER_ENABLED) + ::profile.new-contact-ui (enabled-in-env? :FLAG_NEW_CONTACT_UI_ENABLED) + ::community.edit-account-selection (enabled-in-env? :FLAG_EDIT_ACCOUNT_SELECTION_ENABLED)})) (defn feature-flags [] @feature-flags-config) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index 46f9a4911c..10effc27e0 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -13,6 +13,8 @@ [status-im.contexts.chat.messenger.messages.view :as chat] [status-im.contexts.chat.messenger.photo-selector.view :as photo-selector] [status-im.contexts.communities.actions.accounts-selection.view :as communities.accounts-selection] + [status-im.contexts.communities.actions.addresses-for-permissions.view :as addresses-for-permissions] + [status-im.contexts.communities.actions.airdrop-addresses.view :as airdrop-addresses] [status-im.contexts.communities.actions.request-to-join.view :as join-menu] [status-im.contexts.communities.actions.share-community-channel.view :as share-community-channel] [status-im.contexts.communities.discover.view :as communities.discover] @@ -122,9 +124,23 @@ :options options/transparent-screen-options :component share-community-channel/view} - {:name :community-account-selection + ;; Note: the sheet screen is used when selecting addresses to share when + ;; joining a community. The non-sheet screen is used when editing shared + ;; addresses after the join request was sent. + {:name :community-account-selection-sheet :options {:sheet? true} :component communities.accounts-selection/view} + {:name :community-account-selection + :options {:insets {:top? true}} + :component communities.accounts-selection/view} + + {:name :addresses-for-permissions + :options {:insets {:top? true}} + :component addresses-for-permissions/view} + + {:name :address-for-airdrop + :options {:insets {:top? true}} + :component airdrop-addresses/view} {:name :lightbox :options options/lightbox diff --git a/src/status_im/subs/communities.cljs b/src/status_im/subs/communities.cljs index 2ad049ff2c..dc8fe91a99 100644 --- a/src/status_im/subs/communities.cljs +++ b/src/status_im/subs/communities.cljs @@ -350,57 +350,6 @@ (fn [communities [_ community-id]] (get-in communities [community-id :intro-message]))) -(re-frame/reg-sub :communities/permissioned-balances-by-address - :<- [:communities/permissioned-balances] - (fn [balances [_ community-id account-address]] - (get-in balances [community-id (keyword account-address)]))) - -(re-frame/reg-sub - :communities/selected-permission-addresses - (fn [[_ community-id]] - [(re-frame/subscribe [:communities/community community-id])]) - (fn [[{:keys [selected-permission-addresses]}] _] - selected-permission-addresses)) - -(re-frame/reg-sub - :communities/share-all-addresses? - (fn [[_ community-id]] - [(re-frame/subscribe [:communities/community community-id])]) - (fn [[{:keys [share-all-addresses?]}] _] - share-all-addresses?)) - -(re-frame/reg-sub - :communities/unsaved-address-changes? - (fn [[_ community-id]] - [(re-frame/subscribe [:communities/community community-id])]) - (fn [[{:keys [share-all-addresses? previous-share-all-addresses? - selected-permission-addresses previous-permission-addresses]}] _] - (or (not= share-all-addresses? previous-share-all-addresses?) - (not= selected-permission-addresses previous-permission-addresses)))) - -(re-frame/reg-sub - :communities/selected-permission-accounts - (fn [[_ community-id]] - [(re-frame/subscribe [:wallet/accounts-without-watched-accounts]) - (re-frame/subscribe [:communities/selected-permission-addresses community-id])]) - (fn [[accounts selected-permission-addresses]] - (filter #(contains? selected-permission-addresses (:address %)) accounts))) - -(re-frame/reg-sub - :communities/airdrop-address - (fn [[_ community-id]] - [(re-frame/subscribe [:communities/community community-id])]) - (fn [[{:keys [airdrop-address]}] _] - airdrop-address)) - -(re-frame/reg-sub - :communities/airdrop-account - (fn [[_ community-id]] - [(re-frame/subscribe [:wallet/accounts-with-customization-color]) - (re-frame/subscribe [:communities/airdrop-address community-id])]) - (fn [[accounts airdrop-address]] - (first (filter #(= (:address %) airdrop-address) accounts)))) - (re-frame/reg-sub :communities/token-images-by-symbol (fn [[_ community-id]] diff --git a/src/status_im/subs/communities_test.cljs b/src/status_im/subs/communities_test.cljs index f4b97213cd..48b131d659 100644 --- a/src/status_im/subs/communities_test.cljs +++ b/src/status_im/subs/communities_test.cljs @@ -358,48 +358,6 @@ :img-src token-image-eth}]]} (rf/sub [sub-name community-id]))))) -(h/deftest-sub :communities/airdrop-account - [sub-name] - (testing "returns airdrop account" - (swap! rf-db/app-db assoc-in [:communities community-id :airdrop-address] "0x1") - (swap! rf-db/app-db assoc - :wallet - {:accounts {"0x1" {:address "0x1" - :color :blue - :name "account1"} - "0x2" {:address "0x2" - :color :orange - :name "account2"}}}) - (is (match? {:address "0x1" - :network-preferences-names #{} - :name "account1" - :color :blue - :customization-color :blue} - (rf/sub [sub-name community-id]))))) - -(h/deftest-sub :communities/selected-permission-accounts - [sub-name] - (testing "returns selected permission accounts" - (swap! rf-db/app-db assoc-in - [:communities community-id :selected-permission-addresses] - #{"0x1" "0x3"}) - (swap! rf-db/app-db assoc - :wallet - {:accounts {"0x1" {:address "0x1" :color :blue :name "account1"} - "0x2" {:address "0x2" :color :orange :name "account2"} - "0x3" {:address "0x3" :color :purple :name "account3"}}}) - (is (match? [{:address "0x1" - :color :blue - :customization-color :blue - :network-preferences-names #{} - :name "account1"} - {:address "0x3" - :color :purple - :customization-color :purple - :network-preferences-names #{} - :name "account3"}] - (rf/sub [sub-name community-id]))))) - (h/deftest-sub :communities/community-color [sub-name] (testing "returns the community color" diff --git a/src/status_im/subs/community/account_selection.cljs b/src/status_im/subs/community/account_selection.cljs new file mode 100644 index 0000000000..d300176057 --- /dev/null +++ b/src/status_im/subs/community/account_selection.cljs @@ -0,0 +1,69 @@ +(ns status-im.subs.community.account-selection + (:require + [re-frame.core :as re-frame])) + +;; This sub is particularly useful in context tags in page-nav or drawer-top +;; components. It should be preferred instead of subscribing to the whole +;; community. +(re-frame/reg-sub :communities/for-context-tag + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id])]) + (fn [[{:keys [name images color]}]] + {:name name + :logo (get-in images [:thumbnail :uri]) + :color color})) + +(re-frame/reg-sub :communities/addresses-to-reveal + :<- [:communities/all-addresses-to-reveal] + (fn [addresses-by-id [_ community-id]] + (get addresses-by-id community-id))) + +(re-frame/reg-sub :communities/airdrop-address + :<- [:communities/all-airdrop-addresses] + (fn [addresses-by-id [_ community-id]] + (get addresses-by-id community-id))) + +(re-frame/reg-sub :communities/share-all-addresses? + :<- [:communities/selected-share-all-addresses] + (fn [flags-by-id [_ community-id]] + (get flags-by-id community-id))) + +(re-frame/reg-sub :communities/can-edit-shared-addresses? + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/community community-id]) + (re-frame/subscribe [:communities/my-pending-request-to-join community-id])]) + (fn [[{:keys [joined]} pending-requests]] + (boolean (or joined (seq pending-requests))))) + +(re-frame/reg-sub :communities/permissions-check-for-selection + :<- [:communities/permissions-checks-for-selection] + (fn [permissions [_ id]] + (get permissions id))) + +(re-frame/reg-sub :communities/highest-role-for-selection + (fn [[_ community-id]] + [(re-frame/subscribe [:communities/permissions-check-for-selection community-id])]) + (fn [[permissions-check] _] + (get-in permissions-check [:check :highestRole :type]))) + +(re-frame/reg-sub :communities/accounts-to-reveal + (fn [[_ community-id]] + [(re-frame/subscribe [:wallet/accounts-without-watched-accounts]) + (re-frame/subscribe [:communities/addresses-to-reveal community-id])]) + (fn [[accounts addresses] _] + (filter #(contains? addresses (:address %)) + accounts))) + +(re-frame/reg-sub :communities/airdrop-account + (fn [[_ community-id]] + [(re-frame/subscribe [:wallet/accounts-without-watched-accounts]) + (re-frame/subscribe [:communities/airdrop-address community-id])]) + (fn [[accounts airdrop-address] _] + (->> accounts + (filter #(= (:address %) airdrop-address)) + first))) + +(re-frame/reg-sub :communities/permissioned-balances-by-address + :<- [:communities/permissioned-balances] + (fn [balances [_ community-id account-address]] + (get-in balances [community-id (keyword account-address)]))) diff --git a/src/status_im/subs/community/account_selection_test.cljs b/src/status_im/subs/community/account_selection_test.cljs new file mode 100644 index 0000000000..2c789c666a --- /dev/null +++ b/src/status_im/subs/community/account_selection_test.cljs @@ -0,0 +1,75 @@ +(ns status-im.subs.community.account-selection-test + (:require + [cljs.test :refer [is testing]] + matcher-combinators.test + [re-frame.db :as rf-db] + status-im.subs.communities + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def community-id "0x1") + +(h/deftest-sub :communities/for-context-tag + [sub-name] + (reset! rf-db/app-db + {:communities + {community-id {:name "foo" + :images {:thumbnail {:uri "data:image/png;"}} + :color :orange}}}) + + (is (match? {:name "foo" + :logo "data:image/png;" + :color :orange} + (rf/sub [sub-name community-id])))) + +(h/deftest-sub :communities/can-edit-shared-addresses? + [sub-name] + (testing "joined community and there are pending requests" + (reset! rf-db/app-db + {:communities {community-id {:id community-id :joined true}} + :communities/my-pending-request-to-join {community-id {:id :request-id-1}}}) + + (is (true? (rf/sub [sub-name community-id])))) + + (testing "joined community and there are no pending requests" + (reset! rf-db/app-db + {:communities {community-id {:id community-id :joined true}} + :communities/my-pending-request-to-join {community-id {}}}) + + (is (true? (rf/sub [sub-name community-id])))) + + (testing "not joined community and there are pending requests" + (reset! rf-db/app-db + {:communities {community-id {:id community-id :joined false}} + :communities/my-pending-request-to-join {community-id {:id :request-id-1}}}) + + (is (false? (rf/sub [sub-name community-id])))) + + (testing "not joined community and there are no pending requests" + (reset! rf-db/app-db + {:communities {community-id {:id community-id :joined false}} + :communities/my-pending-request-to-join {community-id {}}}) + + (is (false? (rf/sub [sub-name community-id]))))) + +(h/deftest-sub :communities/airdrop-account + [sub-name] + (let [airdrop-account {:address "0xA" :position 1}] + (reset! rf-db/app-db + {:communities/all-airdrop-addresses {community-id "0xA"} + :wallet {:accounts {"0xB" {:address "0xB" :position 0} + "0xA" airdrop-account}}}) + + (is (match? airdrop-account (rf/sub [sub-name community-id]))))) + +(h/deftest-sub :communities/accounts-to-reveal + [sub-name] + (reset! rf-db/app-db + {:communities/all-addresses-to-reveal {community-id #{"0xC" "0xB"}} + :wallet {:accounts {"0xB" {:address "0xB" :position 0} + "0xA" {:address "0xA" :position 1} + "0xC" {:address "0xC" :position 2}}}}) + + (is (match? [{:address "0xB" :position 0} + {:address "0xC" :position 2}] + (rf/sub [sub-name community-id])))) diff --git a/src/status_im/subs/root.cljs b/src/status_im/subs/root.cljs index 9fed957aa4..3512a1d09f 100644 --- a/src/status_im/subs/root.cljs +++ b/src/status_im/subs/root.cljs @@ -6,6 +6,7 @@ status-im.subs.biometrics status-im.subs.chats status-im.subs.communities + status-im.subs.community.account-selection status-im.subs.contact status-im.subs.general status-im.subs.messages @@ -150,6 +151,11 @@ (reg-root-key-sub :contract-communities :contract-communities) (reg-root-key-sub :communities/permissioned-balances :communities/permissioned-balances) (reg-root-key-sub :communities/permissions-check :communities/permissions-check) +(reg-root-key-sub :communities/all-addresses-to-reveal :communities/all-addresses-to-reveal) +(reg-root-key-sub :communities/all-airdrop-addresses :communities/all-airdrop-addresses) +(reg-root-key-sub :communities/selected-share-all-addresses :communities/selected-share-all-addresses) +(reg-root-key-sub :communities/permissions-checks-for-selection + :communities/permissions-checks-for-selection) (reg-root-key-sub :communities/channel-permissions-check :communities/channel-permissions-check) ;;activity center diff --git a/translations/en.json b/translations/en.json index c9a1c727a7..38d751f7fc 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2279,6 +2279,19 @@ "sync-devices-complete-title": "Device sync complete!", "sync-devices-complete-sub-title": "Your devices are now in sync", "synced-with": "Synced with", + "confirm-and-leave": "Confirm and leave", + "membership-requirements-not-met": "Membership requirements not met", + "edit-shared-addresses": "Edit shared addresses", + "leave-community-farewell": "We’ll be sad to see you go but remember, you can come back at any time! All shared addresses will be unshared.", + "pending-join-request-farewell": "We’ll be sad to see you go but remember, you can request to join this community at any point. All shared addresses will be unshared.", + "all-changes-will-be-discarded": "All changes in shared addresses for permissions will be discarded.", + "cancel-request": "Cancel request", + "discard": "Discard", + "discard-changes?": "Discard changes?", + "confirm-and-cancel": "Confirm and cancel", + "you-will-be-a": "You’ll be a", + "you-are-a": "You’re a", + "you-are-a-role": "You’re a {{role}}", "you-eligible-to-join": "You’re eligible to join", "you-eligible-to-join-as": "You’re eligible to join as {{role}}", "eligible-to-join-as": "Eligible to join as",